从「一切皆文件」到 IO 多路复用——fd 到底是什么,IO 到底在干什么
背八股背到 epoll,却发现连 fd 和 IO 是什么都说不清。这篇从 Unix 的文件抽象出发,一步步讲到阻塞 IO 的困境,再到 select/poll/epoll 的设计与内核实现。
背八股背到 epoll,却发现连 fd 和 IO 是什么都说不清。这篇从 Unix 的文件抽象出发,一步步讲到阻塞 IO 的困境,再到 select/poll/epoll 的设计与内核实现。
进程和线程之间有什么区别 进程(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上忙等;适合临界区很短、持锁时间极短的场景; 互斥锁:线程在获取锁失败后会被阻塞挂起,进入睡眠状态,直至锁被释放后唤醒;适合临界区较长的场景; 读写锁:自旋锁和互斥锁都是排他的,每个时刻只能由一个线程进入临界区;而读写锁把锁拆分为读锁和写锁——读锁与读锁兼容,多线程可以同时读;读锁与写锁互斥;写锁与写锁互斥,同一时刻只能有一个线程写; 乐观锁与悲观锁:悲观锁认为所有的操作都有竞态风险,所以在申请资源之前先加锁;乐观锁则认为所有的操作都是合法的,如果遇到不合法的情况再重试;可以通过给资源加版本号来验证是否合法; 讲一讲你理解的虚拟内存 是什么: 虚拟内存是操作系统为进程提供的一种抽象:让每个进程都认为自己独占了一块连续的、完整的、从零开始的地址空间,这块地址空间就是虚拟地址空间; ...
从加电开始,一步步建立对操作系统本质的认识:内核不是一个运行中的程序,而是被无数次触发执行拼出来的基础设施。