进程和线程之间有什么区别
进程(Process) 是操作系统进行资源分配的基本单位。每个进程有独立的地址空间、文件描述符、堆栈;可以把它理解为一个正在运行的程序实例;
线程(Thread) 是CPU调度和执行的基本单位。线程存在于进程的内部,同一个进程的多个线程共享该进程的地址空间和资源;但每个线程有自己独立的程序计数器、寄存器集合和栈;
核心区别:
- 资源开销不同:创建和销毁一个进程需要大量的资源;进程需要分配和回收独立的地址空间、文件句柄等系统资源;而线程只需要少量的私有状态;
- 隔离性和通信方式不同:进程之间的地址空间相互隔离,一个进程崩溃往往不会影响其他进程,但也意味着进程间通信(IPC)成本较高,需要借助管道、消息队列、共享内存、信号量、套接字等机制;而同一进程的不同线程共享地址空间,可以直接读写同一内存,读写效率很高,但相应的需要通过锁、信号量等同步机制防止数据竞态;
- 上下文切换代价不同:切换进程时需要切换整个地址空间、刷新页表等,代价很大;线程切换因为共享地址空间,只需要保存和恢复少量的寄存器和栈指针;
- 健壮性:因为进程之间隔离,所以多进程更加健壮;而线程因为共享内存,一个线程的非法内存访问可能会导致整个进程的崩溃;
并行和并发有什么区别
并行指的是多个任务在物理上同一时刻同时执行;这依赖于多核处理器或者多台机器等硬件条件,让不同的任务分配到不同的计算单元上同时执行;
并发指的是多个任务在逻辑上同时推进;它并不要求任务在物理时间上同时执行,而是通过快速的上下文切换,让多个任务交替使用CPU,宏观上看起来像是同时执行。
解释一下用户态和核心态,什么场景下会发生切换?
用户态和内核态是为了保护系统资源和实现权限控制而设计的两种不同的CPU运行级别;
- 用户态:程序只能访问受限的资源和指令集,不能直接访问操作系统的核心部分,也不能直接访问硬件资源;
- 内核态:操作系统的特权级别,允许程序执行特权指令和访问操作系统核心部分;程序可以直接操控硬件设备,可以执行任何CPU指令;
切换场景:
- 系统调用:唯一一种主动触发方式,用户态的程序需要请求操作系统提供的服务时,发起系统调用,CPU会切换至内核态,执行对应的内核函数,完成后切回用户态;
- 异常:当CPU在执行用户态指令时遇到错误或特殊情况,会被动地从用户态切到内核态;内核会根据异常类型决定是修复问题还是终止进程;
- 外部中断:当外部硬件设备(比如网卡收到了数据包)会通过中断控制器打断正在执行用户态程序的CPU,切换到内核态,跳转到对应的中断服务程序进行处理,这也是被动的;其中定时器就是非常重要的外部中断,操作系统正是靠它来实现任务调度:定时器周期性地触发中断,打断当前进程,让调度器获得执行机会来决定下一个运行哪个进程;
进程调度算法你了解多少
- 先到先服务(FCFS):按照任务进入就绪队列的顺序运行,非抢占式,前面的任务会阻塞后面的任务直至完成;如果任务时间过长会导致后面的任务迟迟无法执行;
- 最短时间优先:分为非抢占式的最短作业优先(SJF)和抢占式的最短剩余时间优先(SRTF);理论上平均等待时间最短,但现实中无法准确衡量任务的真实执行时间;如果短任务大量发生会饿死长任务;
- 时间片轮转(RR):给每个任务分配一个固定大小的时间片;时间片消耗完则执行下一任务;
- 优先级调度(PR):给任务安排不同的优先级,优先级高的任务先执行;如果优先级高的任务过多会饿死优先级低的任务;会引入衰老机制提升低优先级任务的优先级;
- 多级队列:给不同任务队列划分优先级,不同优先级内可执行不同的调度算法;
- 多级反馈队列:进一步加入优先级反馈调节机制,尽可能保证任务都能执行;
现代Linux曾长期使用的CFS采用加权公平调度的思想,使用红黑树按任务的虚拟运行时间进行排序,选取虚拟运行时间最小的进程进行执行;通过nice值影响虚拟运行时间的增长,保证每个任务都能被执行到;从Linux 6.6开始,CFS被EEVDF(Earliest Eligible Virtual Deadline First)替代,在保留公平性的基础上引入了虚拟截止时间,对延迟敏感型任务的响应更好;
进程间有哪些通信方式
- 管道:本质上是内核维护的一个缓冲区,只能接收无类型的字节流数据,没有边界;是一种半双工通信,即只能单向发送信息;管道分为匿名管道和命名管道,匿名管道只能用于父子进程间的通信;而命名管道有文件路径,可以用于没有亲缘关系的进程间通信;
- 消息队列:是内核维护的一个消息链表,可以传输包含类型的数据;消息队列的生命周期独立于进程,进程退出后消息还在;每条消息有大小限制,不适合传输大数据;
- 共享内存:两个或多个进程将自己虚拟内存中的一块映射到同一块物理内存上,不需要经过内核的拷贝,是最快的通信方式;但因为多个进程都可以读写,会有数据竞态问题,常配合信号量使用;
- 信号量:本质是内核维护的一个计数器,有两种操作,P操作是减,V操作是加;如果值是1的话则类似于互斥量;
- 信号:一种异步通知机制,内核或进程向目标进程发送一个信号编号,目标进程可以选择默认处理、忽略或自定义信号处理函数;
- 套接字:不仅可以用于本地进程间通信,基于TCP的网络套接字是不同机器间通信的最广泛方式;
解释一下进程同步和互斥,以及如何实现
互斥:同一时间只能由一个进程访问某个共享资源,其他进程不能访问和使用;
同步:多个并发的进程之间协调和管理它们的执行顺序,确保按照一定的顺序执行;
实现机制从底层到高层大致有:
- 硬件层面的原子操作:比如比较并交换(CAS)、测试并设置(TAS),自旋锁就是基于这些原子操作实现的;
- 互斥锁:获取不到锁的线程会睡眠让出CPU;
- 信号量:可以同时实现互斥和同步。用作互斥时初值设为1,P操作借出资源后只有V操作归还资源才能让下一个进程使用;用作同步时可以通过设置初值控制并发数量或执行顺序;
- 条件变量:通常配合互斥锁使用,让某个线程在某个条件不满足时睡眠,条件满足时唤醒,以此控制线程的执行顺序达成同步;
什么是死锁,如何避免死锁?
死锁就是两个或多个持有资源的进程在执行过程中,互相等待资源而陷入僵局的一种情况;比如A线程持有资源1需要资源2,而B线程持有资源2需要资源1,互相等待对方释放而互相卡死;
死锁的四个必要条件(缺一不可):
- 互斥条件:每个资源同一时刻只能被一个进程拥有;
- 持有且等待:进程已经持有了一个资源,同时又在请求其他被占用资源,并且在等待期间不会主动释放;
- 不可剥夺条件:进程占有的资源无法被强行夺走;
- 循环等待条件:存在一条进程-资源的循环等待链,就像A等B,B等C,C等A;
预防(提前打破条件):
- 打破持有且等待:要求进程在请求资源时直接占有全部所需资源;
- 打破不可剥夺:要求进程在请求别的资源时主动放弃已占有资源;
- 打破循环等待:给资源加上编号,严格按照编号递增顺序进行资源的申请;
避免(动态判断是否安全):
- 银行家算法:在每次分配资源前,先模拟分配后的状态,检查系统是否仍处于安全状态(即存在一个安全序列使所有进程都能顺利完成);如果不安全则拒绝本次分配,从而避免进入死锁;
介绍一下几种典型的锁
- 自旋锁:基于原子操作CAS和TAS实现的锁,线程如果申请锁失败则会在CPU上忙等;适合临界区很短、持锁时间极短的场景;
- 互斥锁:线程在获取锁失败后会被阻塞挂起,进入睡眠状态,直至锁被释放后唤醒;适合临界区较长的场景;
- 读写锁:自旋锁和互斥锁都是排他的,每个时刻只能由一个线程进入临界区;而读写锁把锁拆分为读锁和写锁——读锁与读锁兼容,多线程可以同时读;读锁与写锁互斥;写锁与写锁互斥,同一时刻只能有一个线程写;
- 乐观锁与悲观锁:悲观锁认为所有的操作都有竞态风险,所以在申请资源之前先加锁;乐观锁则认为所有的操作都是合法的,如果遇到不合法的情况再重试;可以通过给资源加版本号来验证是否合法;
讲一讲你理解的虚拟内存
是什么:
虚拟内存是操作系统为进程提供的一种抽象:让每个进程都认为自己独占了一块连续的、完整的、从零开始的地址空间,这块地址空间就是虚拟地址空间;
但实际上这部分地址空间在真实的物理内存中可能并不是连续的,而是通过一套地址翻译机制,将虚拟内存地址转化为真实的物理地址,然后才能访问真实物理地址中的数据;
为什么:
- 保证内存安全:如果没有这种隔离,进程A就可以直接修改进程B的空间中的数据,这是非常危险的;而虚拟内存则将进程间互相隔离;
- 物理内存有限:物理内存是有上限的,而虚拟内存允许程序运行所需的内存总量超过物理内存大小;核心原理是局部性原理,即在任意时刻程序实际活跃的部分只是地址空间中的一小部分,可以只把活跃的部分映射到物理内存中而把其他部分暂存磁盘,需要用时再交换进内存;
- 简化编程:操作系统提供的虚拟内存让所有程序都可以无需关心物理内存的实际布局;
怎么做:
操作系统通过分页机制来实现虚拟内存;将虚拟地址空间和物理内存都划分为固定大小的块,虚拟内存中的叫页,物理内存的叫页框;虚拟地址被拆分为两部分,高位是页号,低位是页内偏移;翻译的时候通过页号在页表中找到对应的物理页框号,再加上页内偏移最终得到物理地址;
由于每次访问内存都需要查页表,开销很大,CPU引入了TLB(快表) 作为页表的高速缓存;TLB缓存了最近使用过的页号到页框号的映射,命中TLB则直接得到物理地址,无需访问内存中的页表;TLB的命中率通常在95%以上,大幅降低了地址翻译的开销;
你知道的线程同步的方式有哪些?
- 互斥锁:最基本的线程同步方式;当一个线程占有锁的时候,其他申请锁的线程会被阻塞挂起,进入睡眠状态,直至锁被释放由操作系统唤醒;
- 自旋锁:当一个线程占有锁时,其他申请锁的线程会忙等,一直在CPU上空转;
- 读写锁:现实中往往读多写少,而全都是读的情况并不会有数据竞态;所以对锁进行拆分——读锁与读锁兼容,多线程可以并发读;读锁与写锁互斥;写锁与写锁互斥,同一时刻只能有一个线程修改数据;
- 条件变量:用于线程间通信,允许一个线程等待某个条件满足,而其他线程可发出信号通知等待线程;
- 信号量:可以调度多线程访问共享资源的一个计数器;
有哪些页面置换算法
页面置换就是当需要将磁盘中保存的页刷进内存,而内存没有更多空间时,需要将已经存在的页置换出去;
- 最优页面置换(OPT):选择未来最长时间不会被访问的页面置换出去,理论上最优,但现实很难预测未来的情况,所以只是个理论基准;
- 先进先出置换(FIFO):维护一个队列,按照进入内存的时间,将最老的页换出去;
- 最近最少使用算法(LRU):每次换出的是过去最长时间没有被访问过的页面;通常使用哈希表 + 双向链表实现,哈希表提供 O(1) 查找,双向链表维护访问顺序实现 O(1) 淘汰;
- 最近最不常使用(LFU):和LRU类似,但换出的是过去访问次数最少的页面,通过维护页面的计数器实现;但有可能被短时间大量访问之后不再访问的页面污染;
- 时钟算法(Clock):也叫二次机会法,是结合FIFO和类似于LRU的算法;使用一个环形链表管理页面,每个页面维护一个访问位,页面被访问时置为1;当需要置换页面时,指针遍历环形链表,如果当前访问位为1,则将它置0给它第二次机会;如果当前页面访问位为0,说明最近都没人用它,就将它置换出去;
熟悉哪些Linux命令
- 文件和目录操作:
- mkdir:创建目录, -p递归创建多级目录;
- cd:切换目录;
- cp:复制目录;
- ls:列出目录内容,-l显示详细信息(权限,大小等),-h以人类可读的单位显示文件大小;
- pwd:当前工作目录;
- rm:删除文件或目录, -rf递归删除所有;
- 文件内容查看:
- cat:查看文件内容;
- head:查看文件前几行;
- tail:查看文件后几行;
- 文件编辑:
- vim/nano:编辑器;
- 权限管理:
- chmod:更改文件或目录的访问权限;
- 磁盘管理:
- df:查看磁盘空间使用情况;
- 网络管理:
- ifconfig/ip addr:查看和配置网络接口;
- ping:测试网络连接;
- netstat/ss:查看网络状态和统计信息;
- 进程管理:
- ps:查看当前运行的进程;
- kill:杀死某进程;
Linux中如何查看一个进程,如何杀死一个进程,如何查看某个端口有没有被占用
- 查看进程一般使用 ps 命令查看当前运行中的进程,比如ps aux可以累出所有进程以及详细信息,可以加上 | grep 进行筛选;
- 杀死进程使用 kill 命令,kill PID 就是优雅停止, 而kill -9 PID 则是操作系统强制杀死进程;
- 查看端口占用可以用lsof -i:端口号来查看特定端口;但是也常常使用 ss -tulnp | grep 端口号来查看占用端口的服务与其进程PID;
说一下 select、poll、epoll
这三种都是 Linux 的 IO 多路复用机制。所谓 IO 多路复用,就是用一个线程/进程同时监视多个 fd 的 IO 状态,当任意 fd 就绪时再进行实际的读写操作,避免了为每个连接创建独立线程或在单个 fd 上阻塞等待而影响其他 fd 的问题。
- select
- select 是最早的 IO 多路复用实现。调用时将关注的 fd 放入位图(fd_set)传给内核,内核遍历所有 fd 检查状态,有就绪则返回。存在几个问题:一是 fd_set 既用于注册也用于返回状态,内核会直接修改位图内容,因此每次调用前都要重新设置;二是内核侧 O(n) 轮询所有 fd;三是 fd 数量受 FD_SETSIZE 限制,默认 1024;四是每次调用都要将整个 fd_set 从用户态拷贝到内核态。
- poll
- poll 引入了 pollfd 结构体,将注册(events)和返回状态(revents)分开管理,解决了 select 每次都要重新设置位图的问题,同时去掉了 fd 数量上限。但内核侧仍然是 O(n) 遍历,且每次调用仍需将整个 pollfd 数组拷贝到内核态。
- epoll
epoll 用空间换时间,由内核维护一棵红黑树和一个就绪链表。通过 epoll_ctl 增量地将 fd 注册到红黑树中,避免每次重复注册和大量数据拷贝;同时为每个 fd 关联回调函数,当 fd 就绪时由内核回调将其加入就绪链表,epoll_wait 只需取走就绪链表中的 fd,不再轮询,时间复杂度与就绪 fd 数相关而非总 fd 数。
epoll 还支持两种触发模式:LT(水平触发,默认)在 fd 就绪期间持续通知;ET(边缘触发)仅在状态变化时通知一次,需配合非阻塞 IO 循环读取直到 EAGAIN,性能更高但编程复杂度也更高。nginx、Redis 等高性能框架普遍采用 epoll + ET 模式。