操作系统
基础知识
什么是操作系统
-
操作系统本质上是⼀个运⾏在计算机上的软件程序 ,⽤于管理计算机硬件和软件资源。
-
操作系统存在屏蔽了硬件层的复杂性。 操作系统就像是硬件使⽤的负责⼈,统筹着各种相关事项。
-
操作系统的内核(Kernel)是操作系统的核⼼部分,它负责系统的内存管理,硬件设备的管 理,⽂件系统的管理以及应⽤程序的管理。 内核是连接应⽤程序和硬件的桥梁,决定着系统的性能和稳定性
什么是系统调用
把进程在系统上的运⾏分为两个级别:
-
⽤户态(user mode) : ⽤户态运⾏的进程可以直接读取⽤户程序的数据。
-
内核态(kernel mode):内核态运⾏的进程⼏乎可以访问计算机的任何资源,不受限制。
我们运⾏的⽤户程序中,凡是与内核态级别的资源有关的操作(如⽂件管理、进程控制、 内存管理等),都必须通过系统调⽤⽅式向操作系统提出服务请求,并由操作系统代为完成。
线程与进程
进程,线程,协程的区别是什么?
一、定义与本质(核心定位)
1.进程是正在运行的程序实例,进程里包含了线程,每个线程执行不同任务
2.不同进程使用不同内存空间,同一进程的所有线程共享内存空间
3.进程是资源分配的最小单位,线程是cpu调度的最小单位
4.线程更轻量,上下文切换成本较低
协程是用户态的轻量级线程 ,由程序员或框架手动调度 ,而非操作系统内核,它依托线程运行,一个线程可创建上万协程,切换开销极小,且因是协作式调度,无需加锁就能避免线程安全问题,特别适合高并发的 IO 密集型场景。
二、资源占用与开销(核心差异)
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 资源占用 | 大(独立内存 / 资源) | 中(共享进程资源) | 极小(几 KB 栈空间) |
| 切换开销 | 大(内核态切换) | 中(内核态切换) | 极小(用户态切换) |
| 创建数量 | 百级(受系统限制) | 千级(线程池常用) | 百万级(无系统限制) |
| 通信方式 | 管道 / 套接字 / 共享内存 | 共享变量(需加锁) | 通道 / 队列(无需加锁) |
三、调度方式(谁来控制切换)
-
进程 / 线程 :由操作系统内核抢占式调度------ 内核根据时间片、优先级强制切换(比如线程 A 运行到一半,内核可暂停它,让线程 B 执行),程序员无法干预核心调度逻辑。
-
协程 :由用户程序协作式调度 ------ 只有协程主动调用
yield/await等方法 "让出" CPU,才会切换到其他协程,不会被强制打断,调度逻辑完全由代码控制。
进程的调度算法
先到先服务(FCFS)调度算法 : 从就绪队列中选择⼀个最先⼊队的进程为之分配资源,使 它⽴即执⾏并⼀直执⾏到完成或发⽣某事件⽽被阻塞放弃占⽤ CPU 时再重新调度进程。
短作业优先(SJF)的调度算法 : 从就绪队列中选出⼀个估计运⾏时间最短的进程为之分配资源, 使它⽴即执⾏并⼀直执⾏到完成或发⽣某事件⽽被阻塞放弃占⽤ CPU 时再重新调度进程。
时间⽚轮转调度算法 : 时间⽚轮转调度是⼀种最古⽼,最简单,最公平且使⽤最⼴的算法,⼜称 RR(Round robin)调度。每个进程被分配⼀个时间段,称作它的时间⽚,即该进程允许运⾏的时 间。
优先级调度算法 : 为每个进程分配优先级,⾸先执⾏具有最⾼优先级的进程,依此类推。具有相同 优先级的进程以 FCFS ⽅式执⾏。可以根据内存要求,时间要求或任何其他资源要求来确定优 先级
多级反馈队列调度算法 :前⾯介绍的⼏种进程调度的算法都有⼀定的局限性。如短进程优先的 调度算法,仅照顾了短进程⽽忽略了⻓进程 。多级反馈队列调度算法既能使⾼优先级的作业得 到响应⼜能使短作业(进程)迅速完成。,因⽽它是⽬前被公认的⼀种较好的进程调度算法, UNIX 操作系统采取的便是这种调度算法。
时间片轮转调度算法
多级反馈队列 = 多个优先级队列 + 时间片轮转 + 降级机制
-
有 多个队列,优先级从高到低
-
优先级越高,时间片越短(越快完事)
-
新进程 → 进最高优先级队列
-
用完时间片还没结束 → 降到下一级队列
-
调度顺序:
-
先跑高优先级队列
-
高优先级空了,才跑低优先级
-
-
长进程会慢慢沉到低队列,但最终一定能运行 → 不会饿死
进程有哪⼏种状态?
创建状态(new) :进程正在被创建,尚未到就绪状态。
就绪状态(ready) :进程已处于准备运⾏状态,即进程获得了除了处理器之外的⼀切所需资源, ⼀旦得到处理器资源(处理器分配的时间⽚)即可运⾏。
运⾏状态(running) :进程正在处理器上上运⾏(单核 CPU 下任意时刻只有⼀个进程处于运⾏状 态)。
阻塞状态(waiting) :⼜称为等待状态,进程正在等待某⼀事件⽽暂停运⾏如等待某资源为可⽤ 或等待 IO 操作完成。即使处理器空闲,该进程也不能运⾏。
结束状态(terminated) :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运 ⾏
相比java中定义的线程状态,多了一个就绪状态,少了两个时间等待状态。
线程间的通信
同一进程的线程共享地址空间,因此主要通过共享内存进行数据的交换。同时需要多种工具来实现线程的同步/互斥。例如wait/notify等待/通知机制,Condition,CountDownLatch倒计时器实现实现线程间协作,semaphore信号量实现有限资源的并发访问,Thread.join实现线程间的顺序执行,interrupt中断机制优雅停止线程,Threadlocal本地变量保证线程私有数据传输等等。
进程间的通信常⻅的的有哪⼏种⽅式呢?
进程的虚拟内存空间是完全隔离的,因此所有进程通信都必须通过操作系统内核中转:
1.管道:管道使用内核中的一个固定大小的缓冲区(通常 4KB),可分为匿名管道和命名管道,数据以 "字节流" 的形式单向传输。
2.消息队列:消息队列是内核中的一个消息链表,进程可以向队列中 "发送带类型的消息",另一个进程可以 "按类型接收消息",数据以结构化消息体形式双向传输
3.共享内存:共享内存让多个进程共享同一块物理内存,因此效率极高,可以任何格式的数据双向通信。
4.信号量:信号量本质是个计数器,用于控制多个进程对共享资源的访问
5.信号:信号是一种异步通知机制,用于告诉进程 "某个事件发生了"
6.套接字:socket不仅可用于同一个机器的进程通信,还能实现跨机器的网络通信
管道的分类
匿名管道:没有文件名的管道,只能用于有亲缘关系的进程(父子进程、兄弟进程)。通常通过pipe系统调用创建。
-
父进程调用
pipe()创建管道,获得两个文件描述符:fd[0](读端)、fd[1](写端); -
父进程
fork()子进程,子进程继承这两个文件描述符; -
在读或写时关闭不需要的端(比如父进程关读端、子进程关写端),实现单向通信。
命名管道:有文件名的管道(在文件系统中可见),无亲缘关系的进程也能使用。
锁与死锁
什么是死锁
死锁描述的是这样⼀种情况:两个或多个线程,互相持有对方需要的资源,又都不释放自己的资源,大家无限等待下去,谁也动不了。
死锁的必要条件
互斥:资源必须处于⾮共享模式,即⼀次只有⼀个线程可以使⽤。如果另⼀进程申请该资源,那 么必须等待直到该资源被释放为⽌。
占有并等待:⼀个线程⾄少应该占有⼀个资源,并等待另⼀资源,⽽该资源被其他线程所占有。
不可抢占:资源不能被抢占。只能在持有资源的线程完成任务后,该资源才会被释放。
循环等待:有⼀组等待线程 被 P2 占有,......, {P0, P1,..., Pn} , P0 等待的资源被 P1 占有,P1 等待的资源 Pn-1 等待的资源被 Pn 占有,Pn 等待的资源被 P0 占有。
死锁的预防
静态分配策略
静态分配策略可以破坏死锁产⽣的占有并等待条件。指⼀个进程必须在执⾏前就申请到它所需要的全部资源,即它所要的资源都得到满⾜之后才开始执⾏。
层次分配策略
层次分配策略破坏了产⽣死锁的循环等待条件。在层次分配策略下,所有的资源被分成了多 个层次,⼀个进程若想获取某⼀层次的资源,它只能先申请较低⼀层的资源;当⼀个进程要释放某层 的⼀个资源时,必须先释放所占⽤的较⾼层的资源
死锁的避免
不破坏死锁的必要条件,而是在运行时 "动态判断",去躲开死锁风险
具体使用Dijkstra 的银⾏家算法判断。银⾏家算法中将系统的状态分为安全状态与不安全状态。当⼀个进程申请使⽤资源的时候,银⾏家算法 通过 先 假装分配给该进程资源,然后通过 安全性算法 判断分配后系统是否处于安全状态,若不安全则 试探分配作废,让该进程继续等待,若能够进⼊到安全的状态,则就 真的分配资源给该进程。
你是银行(操作系统)
有 100 万(系统总资源)
有 3 个人来借钱(进程):A 需要 80 万,B 需要 70 万,C 需要 60 万
现在:A 借 30 万,B 借 20 万,C 借 10 万
此时银行还剩 40 万。
这时 C 再来借 30 万。
死锁的检测
进程-资源分配图
操作系统中的每⼀时刻的系统状态都可以⽤进程-资源分配图来表示,进程-资源分配图是描述进程 和资源申请及分配关系的⼀种有向图,通过不断假设化简可⽤于检测系统是否处于死锁状态。
死锁的解决
1.⽴即结束所有进程的执⾏,重新启动操作系统 :这种⽅法简单,但以前所在的⼯作全部作废, 损失很⼤。
2.撤销涉及死锁的所有进程,解除死锁后继续运⾏ :这种⽅法能彻底打破死锁的循环等待条件, 但将付出很⼤代价,例如有些进程可能已经计算了很⻓时间,由于被撤销⽽使产⽣的部分结果也 被消除了,再重新执⾏时还要再次进⾏计算。
3.逐个撤销涉及死锁的进程,回收其资源直⾄死锁解除。
4.抢占资源 :不终止任何进程,仅从死锁的进程中 "强行拿走"部分关键资源
内存管理
操作系统的内存管理主要是做什么?
操作系统的内存管理主要负责内存的分配与回收(malloc 函数:申请内存,free 函数:释放内存),以及地址转换也就是将逻辑地址转换成相应的物理地址等功能
常⻅的⼏种内存分配管理机制
简单分为连续内容分配管理⽅式和⾮连续内存分配管理⽅式这两种。连续分配管理⽅式是指为⼀个⽤户程序分 配⼀个连续的内存空间,常⻅的如 块式管理 。同样地,⾮连续分配管理⽅式允许⼀个程序使⽤的内 存分布在离散或者说不相邻的内存中,常⻅的如⻚式管理 和 段式管理。
-
块式管理 : 将内存分为⼏个固定⼤⼩的块,每个块中只包含⼀个进程。如果程序运⾏需要内存的话,操作系统就分配给它⼀块,如果程序运⾏只需要很⼩的空间的话,分配的这块内存很⼤⼀部分⼏乎被浪费了。这些在每个块中未被利⽤的空间,我们称之为碎⽚。
-
⻚式管理 :把主存分为⼤⼩相等且固定的⼀⻚⼀⻚的形式,⻚较⼩,相⽐于块式管理的划分粒度更⼩,提⾼了内存利⽤率,减少了碎⽚。⻚式管理通过⻚表对应逻辑地址和物理地址。
-
段式管理 : ⻚式管理虽然提⾼了内存利⽤率,但是⻚式管理其中的⻚并⽆任何逻辑意义。 段式管理把主存分为⼀段段的,段是有实际逻辑意义的,在程序中可以体现 为代码段,数据段 等。 段式管理通过段表对应逻辑地址和物理地址。简单来说:⻚是物理单位,段是逻辑单位。⽽段的⼤⼩不固定,取决于我们当前运⾏的程序。
-
段⻚式管理机制 。段⻚式管理机制结合了段 式管理和⻚式管理的优点。简单来说段⻚式管理机制就是把主存先分成若⼲段,每个段⼜分成若⼲ ⻚,也就是说 段⻚式管理机制 中段与段之间以及段的内部的都是离散的。
分⻚机制和分段机制的共同点和区别
共同点 :
分⻚机制和分段机制都是为了提⾼内存利⽤率,减少内存碎⽚。
⻚和段都是离散存储的,所以两者都是离散分配内存的⽅式。但是,每个⻚和段中的内存是 连续的。
区别 :
⻚的⼤⼩是固定的,由操作系统决定;⽽段的⼤⼩不固定,取决于我们当前运⾏的程序。
分⻚仅仅是为了满⾜操作系统内存管理的需求,⽽段是逻辑信息的单位,在程序中可以体现 为代码段,数据段,能够更好满⾜⽤户的需要
逻辑(虚拟)地址和物理地址
我们编程⼀般只有可能和逻辑地址打交道,⽐如在 C 语⾔中,指针⾥⾯存储的数值就可以理解成为内存⾥的⼀个地址,这个地址也就是我们说的逻辑地址,逻辑地址由操作系统 决定。物理地址指的是真实物理内存中地址,更具体⼀点来说就是内存地址寄存器中的地址。
CPU 寻址了解吗?为什么需要虚拟地址空间
现代处理器使⽤的是⼀种称为虚拟寻址(Virtual Addressing) 的寻址⽅式。CPU需要使用被称为内存管理单元(Memory Management Unit, MMU) 的 硬件将虚拟地址翻译成物理地址。
没有虚拟地址空间的时候,程序直接访问和操作的都是物理内存 。这样有几个问题
-
不够安全。⽤户程序可以访问任意内存,这样就很容易破坏操作系 统,造成操作系统崩溃。
-
内存冲突。想要同时运⾏多个程序特别困难,多个程序对内存的赋值可能会相互覆盖,导致程序崩溃
总结来说:如果直接把物理地址暴露出来的话会带来严重问题,⽐如可能对操作系统造成伤害以及给 同时运⾏多个程序造成困难
快表和多级⻚表
快表 为了提⾼虚拟地址到物理地址的转换速度,操作系统在⻚表⽅案 基础之上引⼊了快表来加速虚拟 地址到物理地址的转换。我们可以把快表理解为⼀种特殊的⾼速缓冲存储器(Cache)。 由于采⽤⻚表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问⼀次⾼ 速缓冲存储器,⼀次主存,这样可加速查找并提⾼指令执⾏速度。
使⽤快表之后的地址转换流程是这样的:
-
根据虚拟地址中的⻚号查快表;
-
如果该⻚在快表中,直接从快表中读取相应的物理地址;
-
如果该⻚不在快表中,就访问内存中的⻚表,再从⻚表中得到物理地址,同时将⻚表中的该映射 表项添加到快表中;
-
当快表填满后,⼜要登记新⻚时,就按照⼀定的淘汰策略淘汰掉快表中的⼀个⻚。
快表和我们平时经常在我们开发的系统使⽤的缓存(⽐如 Redis)很像
多级页表(比如二级 / 三级页表)的核心思路是:将单级页表拆分成「多级索引」,只把进程「正在使用的页表部分」加载到内存,未使用的部分留在磁盘(或直接不分配) ,从而节省物理内存空间。多级页表节省了内存空间,但代价是:地址转换时需要多查几次页表,增加了 CPU 的访问时间,时间换空间。
什么是虚拟内存(Virtual Memory)?
虚拟内存是操作系统提供的一种抽象机制,让进程以为自己独占一大块连续的内存空间,但实际物理内存是分散、共享的,甚至可以用磁盘当 "临时内存"。通过 MMU 和页表映射到物理内存或磁盘。它实现了内存隔离、内存扩充、程序简化和系统安全,是现代操作系统的核心基础。
局部性原理
-
时间局部性 :程序中的某条指令⼀旦执⾏,不久以后该指令可能再次执⾏;如果某数据被访问过,不久后该数据可能再次被访问。产⽣时间局部性的典型原因,是由于在程序中存在着⼤量的循环操作。
-
空间局部性 :⼀旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,这是因为程序在⼀段时间内所访问的地址,可能集中在⼀定的范围之内。
虚拟存储器
基于局部性原理,在程序装⼊时,可以将程序的⼀部分装⼊内存,⽽将其他部分留在外存,就可以启 动程序执⾏。由于外存往往⽐内存⼤很多,所以我们运⾏的软件的内存⼤⼩实际上是可以⽐计算机系 统实际的内存⼤⼩⼤的。在程序执⾏过程中,当所访问的信息不在内存时,由操作系统将所需要的部 分调⼊内存,然后继续执⾏程序。另⼀⽅⾯,操作系统将内存中暂时不使⽤的内容换到外存上,从⽽ 腾出空间存放将要调⼊内存的信息。这样,计算机好像为⽤户提供了⼀个⽐实际内存⼤得多的存储器 ------虚拟存储器。
实际上,我觉得虚拟内存同样是⼀种时间换空间的策略,你⽤ CPU 的计算时间,⻚的调⼊调出花费 的时间,换来了⼀个虚拟的更⼤的空间来⽀持程序的运⾏。
虚拟内存技术的实现呢?
有请求分⻚存储管理和请求分段存储管理两种方式,不过不管是哪种实现⽅式,我们⼀般都需要:
-
⼀定容量的内存和外存:在载⼊程序的时候,只需要将程序的⼀部分装⼊内存,⽽将其他部分留 在外存,然后程序就可以执⾏了;
-
缺⻚中断:如果需执⾏的指令或访问的数据尚未在内存(称为缺⻚或缺段),则由处理器通知操 作系统将相应的⻚⾯或段调⼊到内存,然后继续执⾏程序;
-
虚拟地址空间 :逻辑地址到物理地址的变换
⻚⾯置换算法
OPT ⻚⾯置换算法(最佳⻚⾯置换算法) :最佳(Optimal, OPT)置换算法所选择的被淘汰⻚⾯ 将是以后永不使⽤的,或者是在最⻓时间内不再被访问的⻚⾯,这样可以保证获得最低的缺⻚ 率。但由于⼈们⽬前⽆法预知进程在内存下的若千⻚⾯中哪个是未来最⻓时间内不再被访问的, 因⽽该算法⽆法实现。⼀般作为衡量其他置换算法的⽅法。
FIFO(First In First Out) ⻚⾯置换算法(先进先出⻚⾯置换算法) : 总是淘汰最先进⼊内存 的⻚⾯,即选择在内存中驻留时间最久的⻚⾯进⾏淘汰。
LRU (Least Recently Used)⻚⾯置换算法(最近最久未使⽤⻚⾯置换算法) :LRU 算法赋 予每个⻚⾯⼀个访问字段,⽤来记录⼀个⻚⾯⾃上次被访问以来所经历的时间 T,当须淘汰⼀个 ⻚⾯时,选择现有⻚⾯中其 T 值最⼤的,即最近最久未使⽤的⻚⾯予以淘汰。
LFU (Least Frequently Used)⻚⾯置换算法(最少使⽤⻚⾯置换算法) : 该置换算法选择在 之前时期使⽤最少的⻚⾯作为淘汰⻚
IO模型
你了解哪些IO模型
-
阻塞I/O模型:应用程序发起I/O操作后会被阻塞,直到操作完成才返回结果。适用于对实时性要求不高的场景。
-
非阻塞I/O模型:应用程序发起I/O操作后立即返回,不会被阻塞,但需要不断轮询或者使用select/poll/epoll等系统调用来检查I/O操作是否完成。适合于需要进行多路复用的场景,例如需要同时处理多个socket连接的服务器程序。
-
I/O多路复用模型:通过select、poll、epoll等系统调用,应用程序可以同时等待多个I/O操作,当其中任何一个I/O操作准备就绪时,应用程序会被通知。适合于需要同时处理多个I/O操作的场景,比如高并发的服务端程序。
-
信号驱动I/O模型:应用程序发起I/O操作后,可以继续做其他事情,当I/O操作完成时,操作系统会向应用程序发送信号来通知其完成。适合于需要异步I/O通知的场景,可以提高系统的并发能力。
-
异步I/O模型:应用程序发起I/O操作后可以立即做其他事情,当I/O操作完成时,应用程序会得到通知。异步I/O模型由操作系统内核完成I/O操作,应用程序只需等待通知即可。适合于需要大量并发连接和高性能的场景,能够减少系统调用次数,提高系统效率。
说说IO多路复用
IO 多路复用,就是一个进程 / 线程,同时监听多个文件描述符(FD) ,当其中任意一个 FD 就绪(可读 / 可写)时,就通知程序去处理,从而实现单线程处理大量并发连接。
select、poll、epoll 的区别是什么
1 . select(最早的 I/O 多路复用实现)
(1)工作原理(分 4 步)
select 用「位图(Bitmap)」存储要监听的 FD 集合(读 / 写 / 异常三类),流程如下:
-
用户态准备 FD 集合:比如要监听 10 个 FD 的 "读事件",就把这 10 个 FD 对应的位图位置设为 1;
-
拷贝 FD 集合到内核态 :调用
select()时,把整个位图从用户态拷贝到内核态; -
内核轮询所有 FD:内核遍历位图里的所有 FD,判断是否就绪(比如有数据可读);
-
返回就绪 FD,用户态再遍历 :内核把就绪的 FD 位图位置标记为 1,把整个位图拷贝回用户态;用户态再次遍历所有 FD,找到标记为 1 的就绪 FD 并处理。
(2)核心问题(3 个致命缺陷)
结合原理看问题,每个缺陷都对应原理的某一步:
-
FD 数量限制 :位图长度是固定的(Linux 内核默认
FD_SETSIZE=1024),最多只能监听 1024 个 FD------ 你的协同编辑场景如果有 2000 个 WebSocket 连接,select 直接用不了; -
效率极低(两次全量遍历):
-
内核要遍历所有 FD(哪怕只有 1 个就绪,也要遍历 1024 个);
-
用户态拿到位图后,还要再遍历所有 FD 找就绪的;
-
-
重复拷贝开销大 :每次调用
select()都要把 FD 集合从用户态拷贝到内核态,再拷贝回来 ------1000 个 FD 的位图约 128 字节,看似小,但高频调用(比如每秒 1 万次)时,拷贝开销会成为瓶颈
poll(select 的 "小修小补")
(1)核心优化:用链表替代位图
poll 不再用固定长度的位图,而是用「链表存储 pollfd 结构体」(每个 pollfd 存一个 FD 及其监听的事件),链表没有长度限制,彻底解决了 FD 数量上限问题
epoll(Linux 2.6 后推出,高并发核心)
epoll 是对 select/poll 的彻底重构,从数据结构、拷贝机制、触发方式、回调逻辑四个维度做了核心优化,是 Nginx、Redis、Netty 等高性能中间件的首选。
(1)核心优化 1:数据结构升级(红黑树 + 就绪链表)
epoll 在内核中维护了两个核心数据结构,彻底解决 "全量遍历" 问题:
-
红黑树(RB-Tree):存储所有要监听的 FD;
-
红黑树的增删改查效率都是
O(logn),比如新增 1 个 FD、删除 1 个 FD,都很快; -
替代了 select/poll 的 "每次传所有 FD",FD 只需要注册一次,后续不用重复传。
-
-
就绪链表(Ready List):存储已就绪的 FD;
-
只有真正就绪的 FD 才会被加入这个链表;
-
用户态调用
epoll_wait()时,直接返回这个链表,无需遍历所有 FD,只需遍历就绪的几个 FD (效率从O(n)降到O(1))。
-
(2)核心优化 2:零拷贝(避免重复拷贝 FD 集合)
epoll 用「内存映射(mmap)」实现零拷贝:
-
调用
epoll_ctl()注册 FD 时,内核把 FD 信息直接存到内核的红黑树里; -
后续调用
epoll_wait()时,无需再把 FD 集合从用户态拷贝到内核态------ 内核直接从红黑树里取 FD,就绪链表也通过内存映射让用户态直接读取,全程无重复拷贝。
注:这里的 "零拷贝" 是指「FD 集合无需重复拷贝」,不是指数据传输的零拷贝(那是另一个概念)。
(3)核心优化 3:触发方式灵活(LT + ET)
epoll 支持两种触发方式,这是 select/poll 没有的:
-
水平触发(LT,Level Triggered,默认):
-
逻辑:FD 就绪后,若用户没处理完数据,下次
epoll_wait()仍会提醒这个 FD; -
类比:"外卖到了,你没拿,外卖员会每隔 5 分钟再提醒你一次";
-
优势:兼容 select/poll,编程简单,不容易出错;
-
劣势:重复通知,效率稍低。
-
-
边缘触发(ET,Edge Triggered):
-
逻辑:FD 就绪后,只提醒一次,不管用户有没有处理完数据;
-
类比:"外卖到了,外卖员只敲一次门,你没拿就走了";
-
优势:减少重复通知,效率更高(是高性能场景的首选);
-
要求:必须一次性把 FD 的数据读完(比如用循环读,直到读不到数据为止),否则会漏掉数据。
-
(4)核心优化 4:回调机制(内核主动通知,无需轮询)
这是 epoll 效率高的核心原因:
-
内核为每个注册到红黑树的 FD 绑定一个回调函数;
-
当 FD 就绪(比如有数据可读)时,内核自动触发回调函数,把这个 FD主动加入就绪链表;
-
用户态调用
epoll_wait()时,直接从就绪链表取 FD 即可 ------内核不用轮询所有 FD,用户态也不用轮询所有 FD,全程只有 "处理就绪 FD" 的有用操作。
(5)epoll 的三个核心 API(结合流程)
epoll 用三个 API 完成所有操作,逻辑清晰:
-
epoll_create():创建一个 epoll 实例(在内核中分配红黑树和就绪链表的空间); -
epoll_ctl():管理要监听的 FD(新增 / 删除 / 修改红黑树里的 FD);- 比如:
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event)把 sockfd 加入 epoll 的红黑树;
- 比如:
-
epoll_wait():等待 FD 就绪,返回就绪链表;- 比如:
epoll_wait(epfd, events, 1024, -1)阻塞等待,直到有 FD 就绪,返回就绪的 events 数组。
- 比如: