这是一份面向后端面试的操作系统核心复习稿,共 8 个模块、14 道问答。先看每节知识点,再用 Q/A/W 自测:A 是可以直接说给面试官的回答,W 用具体场景把机制与取舍讲透。

一、进程、线程与协程(大厂高频)

知识点

  • 进程:运行中的程序实例,也是资源隔离与分配的基本单位。进程拥有独立虚拟地址空间、打开文件等资源,进程间默认不能直接读写彼此内存。
  • 线程:进程内的执行流,也是内核调度 CPU 的基本单位。同一进程的线程共享地址空间和文件描述符,但各自保留栈、寄存器和程序计数器。
  • 协程:由用户态运行时调度的轻量任务,切换通常不进入内核。协程能把等待 I/O 的任务挂起,但阻塞系统调用若未被运行时接管,仍可能堵住承载它的整个线程。
  • 隔离与通信成本:进程隔离强,故障通常不会直接破坏另一个进程的内存,但通信和切换要经过更多内核机制;线程共享数据方便,也更容易发生数据竞争。
  • 并发与并行:并发指多个任务在时间上交替推进,并行指多个任务在同一时刻分别占用不同处理器执行;单核可以并发,多核才可能真正并行。

面试问答

  • Q1:进程、线程和协程有什么区别?(追问率极高)

A: 进程负责资源隔离,线程是内核调度的执行流,协程由用户态运行时调度。进程切换和通信成本较高,但隔离更强;线程共享进程资源,通信方便,却要处理同步;协程切换轻,适合大量 I/O 等待任务,但绕不过阻塞调用和 CPU 核数的限制。

W: 假设服务要维护十万个网络连接。每个连接建一个进程,地址空间和内核对象开销太大;每个连接建一个线程,栈和调度压力也会升高。协程可以在一个线程上挂起大量等待中的请求,只让可运行任务占 CPU。遇到计算密集任务时,仍要把工作分到多个线程或进程才能利用多核。

  • Q2:线程上下文切换为什么有成本?

A: 切换时内核要保存当前线程的寄存器、程序计数器和栈指针,再恢复下一个线程的状态。直接保存的数据不一定很多,更大的成本常来自缓存和 TLB 热度被打散。若发生跨进程切换,地址空间变化还可能带来更多页表与地址翻译开销。

W: 两个线程轮流处理同一批热点数据时,缓存还能复用;若调度器频繁换到访问完全不同数据的任务,后一个任务会不断把前一个任务的缓存行挤出去。等原任务再次运行,又要从更慢的缓存层级或内存取数。因此线程数超过可并行工作的需求后,吞吐可能不升反降。


二、进程状态与 CPU 调度(高频)

知识点

  • 基本状态变迁:进程通常在就绪、运行、阻塞之间转换。等待 CPU 时处于就绪态,等待 I/O 或事件时处于阻塞态,事件完成后回到就绪队列,不会直接保证立刻运行。
  • Linux 常见状态:R 表示运行或可运行,S 表示可中断睡眠,D 表示不可中断睡眠,T 表示停止,Z 表示僵尸;状态可从 /proc/<pid>/stat 或 ps 查看。
  • 周转与响应目标:批处理任务关心周转时间,交互任务关心响应时间,系统还要兼顾吞吐、公平和饥饿;不存在对所有负载都最优的调度算法。
  • 常见调度算法:先来先服务实现简单但会出现护航效应;短作业优先平均等待时间低,却需要估计运行时间;时间片轮转改善交互响应,时间片过小会增加切换开销。
  • 多级反馈队列:新任务先进入高优先级短时间片队列,用完时间片会逐级下沉;长时间未运行的任务可提升优先级,兼顾交互任务与防饥饿。
  • 优先级反转:高优先级任务等待低优先级任务持有的锁,而中优先级任务持续抢占低优先级任务,导致高优先级任务间接被拖延;可用优先级继承或优先级上限协议缓解。

面试问答

  • Q1:常见 CPU 调度算法怎么取舍?(高频)

A: 先来先服务很简单,但长任务会拖住短任务;短作业优先能降低平均等待时间,却难准确预估运行时长,还可能让长任务饥饿;时间片轮转响应快,时间片太短又会频繁切换。多级反馈队列根据任务行为调整优先级,是交互系统常见的综合方案。

W: 一条耗时十秒的计算任务先进入队列,后面十个十毫秒请求若按先来先服务,只能全部等十秒。时间片轮转让短请求很快获得 CPU,但时间片若短到接近切换成本,CPU 会忙于保存和恢复现场。调度器只能根据负载在响应、吞吐与公平之间做取舍。

  • Q2:什么是优先级反转,怎么解决?

A: 高优先级任务需要一把被低优先级任务持有的锁,此时若中优先级任务持续抢占低优先级任务,高优先级任务反而长期无法运行,这就是优先级反转。常用解法是优先级继承,让持锁者临时提升到等待者的优先级,尽快完成临界区并释放锁。

W: 设低优先级线程拿锁后被高优先级线程唤醒,高优先级线程马上因等锁阻塞。若中优先级线程不断占用 CPU,低优先级线程得不到运行机会,也就无法释放锁。优先级继承把低优先级线程临时抬高,让它先收尾;解锁后再恢复原优先级,等待链便能继续推进。


三、进程间通信(高频)

知识点

  • 管道:匿名管道是内核中的字节流,常用于有亲缘关系的进程;命名管道在文件系统中有名字,可让无亲缘关系的本机进程通信。管道天然适合单向顺序传输,不保留应用消息边界。
  • 消息队列:内核按消息组织数据,发送方与接收方不必同时工作;相比管道更容易表达消息边界和优先级,但数据仍要进入内核,容量也受系统限制。
  • 共享内存:多个进程映射同一组物理页,建立映射后可直接读写,适合大块、高频数据交换;内核不替业务保证一致性,必须另配互斥锁、信号量或无锁协议。
  • 信号:信号适合通知进程发生了某个异步事件,携带信息有限,处理函数中可安全调用的函数也受限制,不适合传输大块业务数据。
  • 套接字:Unix 域套接字适合本机双向通信,网络套接字还能跨主机;它们接口统一、扩展性好,但协议栈与数据复制会增加成本。

面试问答

  • Q1:进程间通信方式怎么选?(高频)

A: 少量单向字节流可用管道,有明确消息边界可用消息队列,大块高频数据优先考虑共享内存,事件通知用信号。需要双向通信或未来可能跨机器时用套接字。共享内存通常数据路径最短,但同步最难,不能只看吞吐,还要看隔离、可靠性和开发成本。

W: 图像进程每帧向推理进程传几十兆数据,若每次都经管道或消息队列复制,内存带宽可能先成为瓶颈。共享内存能让双方访问同一批页,但生产者写到一半时消费者不能开读,所以还要设计状态位和同步。若消息很小且要跨机器,套接字反而更省开发成本。


四、同步、互斥与死锁(大厂高频)

知识点

  • 竞态条件:多个执行流并发访问共享状态,最终结果依赖不可预测的执行顺序。count++ 通常包含读取、计算、写回多步,不能因为一行代码就视为原子操作。
  • 互斥锁与自旋锁:互斥锁竞争失败后可让线程睡眠,适合临界区较长的场景;自旋锁让线程循环等待,避免睡眠与唤醒,但等待过久会空耗 CPU,也不能在持锁时做可能睡眠的操作。
  • 信号量:计数信号量允许最多 N 个执行流同时获得资源,二值信号量可表达互斥;它还可通过等待与通知协调执行顺序。
  • 条件变量:条件变量用于等待某个谓词成立,必须和互斥锁配合,并在循环中重新检查条件,避免虚假唤醒或条件在重新获得锁前已被其他线程改变。
  • 死锁四条件:互斥、请求并保持、不可剥夺、循环等待同时成立才可能死锁;破坏任一条件都能预防死锁,工程中常通过固定加锁顺序破坏循环等待。
  • 死锁与活锁:死锁中的任务互相等待而不再推进;活锁中的任务仍在运行并反复让步,却始终完成不了工作。随机退避可降低活锁持续发生的概率。

面试问答

  • Q1:互斥锁、自旋锁和信号量怎么选?

A: 只允许一个线程进入临界区时可用互斥锁或自旋锁。临界区短、不能睡眠且预计很快释放时才考虑自旋锁;等待可能较长时用互斥锁,让线程休眠避免烧 CPU。信号量适合限制并发资源数,比如连接池有 N 个槽位,它表达的是配额,不只是互斥。

W: 线程为一把要等待十毫秒的锁持续自旋,这段时间会占满一个处理器,却没有完成业务;改用互斥锁后,内核能把 CPU 让给持锁者。反过来,临界区只有几十条指令,睡眠和唤醒的成本可能超过等待本身。选锁要比较预期等待时间、能否睡眠和竞争强度。

  • Q2:线上死锁怎么排查和避免?(场景题)

A: 先拿线程栈或锁等待信息,画出谁持有什么锁、又在等谁,确认等待环。代码中要统一加锁顺序,缩小临界区,不在持锁期间做网络调用;确实要拿多把锁时,可一次性申请、超时退出或失败后随机退避。数据库死锁还要结合事务与锁记录分析。

W: 线程 A 拿用户锁再等订单锁,线程 B 拿订单锁再等用户锁,等待图形成一个环,谁都无法主动释放已有锁。若所有代码都先拿用户锁再拿订单锁,等待关系只能朝同一方向延伸,环无法形成。超时能让系统恢复,但没有消除顺序不一致这个原因。


五、虚拟内存与页面置换(大厂高频)

知识点

  • 虚拟地址空间:每个进程看到独立、连续的虚拟地址,由页表映射到物理页。它提供隔离,也允许不同进程共享同一物理页,并让程序只把当前需要的页面放入内存。
  • 多级页表:虚拟地址被分段作为各级索引,低层页表可按需创建。若地址空间大而实际只用少量区域,多级结构无需为全部虚拟页预先分配页表项,代价是页表遍历层数增加。
  • TLB:转址旁路缓存保存近期虚拟页到物理页框的映射。命中时可跳过大部分页表遍历;上下文切换、工作集过大或访问离散都会降低命中率。
  • 缺页异常:页表项不存在或权限不符时,CPU 陷入内核。匿名页首次访问可能只需分配物理页,文件页可能从页缓存获得;需要磁盘读取的主缺页比无需磁盘的次缺页慢得多。
  • 写时复制:fork() 后父子进程可先共享只读物理页,某一方写入时才触发缺页并复制该页;它降低了创建进程时立即复制整个地址空间的成本。
  • 页面置换:理想算法需要未来信息,无法实现;LRU 利用近期历史但精确维护昂贵,时钟算法用访问位近似 LRU。工作集超过可用内存时会频繁换页,形成抖动。

面试问答

  • Q1:虚拟内存、页表和 TLB 怎么配合?(追问率极高)

A: 程序访问虚拟地址时,CPU 先查 TLB;命中就得到物理页框,未命中再按多级页表查找,并把结果回填 TLB。页表负责地址映射和权限,虚拟内存提供隔离、共享与按需分配。若页表项无效或权限不符,就会触发缺页异常,交给内核处理。

W: 一个 64 位进程的虚拟地址空间很大,但实际可能只用了代码、堆和少量映射区。单级页表若为每个虚拟页都预留表项,会浪费大量内存;多级页表只为已用区域展开下层结构。逐级查询会增加访存次数,所以 CPU 再用 TLB 缓存热点映射,缩短常见路径。

  • Q2:缺页异常发生后会做什么?

A: CPU 先保存现场并进入内核,内核检查地址是否合法。非法访问会向进程报告错误;合法但尚未驻留的页面,会分配空闲页或选择牺牲页,必要时把脏页写回,再从文件或交换区装入数据,更新页表后重试指令。频繁主缺页会把性能拖到磁盘速度。

W: 服务内存工作集突然超过物理内存后,刚换出的页很快又被访问,只能再次从磁盘读回;为了腾位置,另一个仍会被访问的页又被换出。CPU 大部分时间都在等待换页,业务吞吐下降,这就是抖动。排查时要把缺页率、交换活动和进程驻留内存一起看。


六、文件系统与链接(高频)

知识点

  • inode 与目录项:inode 保存文件类型、权限、属主、大小、时间戳和数据块位置等元数据,不保存文件名;目录项负责把文件名映射到 inode 号。
  • 硬链接:多个目录项可指向同一个 inode,内容和元数据属于同一文件对象。删除一个名字只是减少链接计数,计数归零且没有进程继续打开时,文件数据才可回收。
  • 硬链接边界:硬链接通常不能跨文件系统,因为 inode 号只在所属文件系统内有意义;系统也通常禁止普通用户给目录创建硬链接,以免目录树出现环。
  • 软链接:符号链接是独立文件,内容是一段目标路径。它可跨文件系统、可指向目录,也可能在目标移动或删除后变成悬空链接。
  • 文件描述符:文件描述符是进程文件描述符表中的整数索引,指向内核的打开文件描述;dup() 或 fork() 可让多个描述符共享文件偏移和状态标志。
  • 页缓存:普通文件读写通常经过页缓存。读命中可避免磁盘访问,写操作常先修改缓存页再异步回写;需要持久性保证时要按协议使用 fsync() 等接口。

面试问答

  • Q1:inode、硬链接和软链接是什么关系?(高频)

A: 目录项把文件名映射到 inode,inode 再记录元数据和数据位置。硬链接是另一个指向同一 inode 的目录项,删除一个名字不等于删除文件。软链接拥有自己的 inode,内容保存目标路径,因此能跨文件系统,但目标消失后会悬空。

W: 文件 a 有一个硬链接 b 时,两个名字查到的是同一 inode。删掉 a 只会让链接计数减一,b 仍能访问数据。软链接 c 保存的若是路径 a,删除 a 后解析路径会失败;即使后来新建同名 a,c 又会指向这个新文件,因为它认路径,不认原 inode。


七、I/O 模型与多路复用(大厂高频)

知识点

  • 阻塞 I/O:调用在数据未就绪时让线程睡眠,数据就绪后还要完成内核到用户空间的复制才返回。模型简单,但大量连接各占一个线程会增加内存和调度压力。
  • 非阻塞 I/O:调用在数据未就绪时立即返回,应用可稍后重试。若直接循环轮询,会持续消耗 CPU,实际常与事件通知机制组合。
  • I/O 多路复用:应用通过 select、poll 或 epoll 等接口等待多个描述符的就绪事件,再对就绪对象执行读写;等待阶段可由少量线程管理大量连接。
    • epoll 默认采用水平触发;使用边缘触发时,应把描述符设为非阻塞,并持续读写到返回 EAGAIN,否则可能遗漏缓冲区中尚未处理的数据。
  • 信号驱动 I/O:应用注册异步通知,描述符就绪时内核发送信号;信号处理与事件管理复杂,网络服务中使用较少。
  • 异步 I/O:应用提交操作后继续执行,内核在数据复制也完成后通知结果。它与“就绪后仍由应用调用读写”的多路复用不同。
  • 同步与阻塞:阻塞描述等待期间线程是否停住,同步描述 I/O 完成工作由谁推进;阻塞、非阻塞和多路复用通常都属于同步 I/O,不能把非阻塞直接等同于异步。

面试问答

  • Q1:五种 I/O 模型有什么区别?(高频)

A: 阻塞 I/O 会等到数据就绪并复制完成;非阻塞 I/O 未就绪就返回,单独轮询会烧 CPU;多路复用统一等待多个描述符,再由应用读写;信号驱动通知就绪;异步 I/O 则由内核完成操作后通知结果。差别在等待方式和谁完成数据搬运。

W: 网络数据分两段:先到内核缓冲区,再复制到应用缓冲区。epoll 只告诉应用第一段已经满足,应用仍要调用 read() 完成第二段,所以它属于同步 I/O。真正的异步 I/O 是提交读请求后先做别的工作,等内核连复制也完成,再收到完成通知。

  • Q2:select、poll、epoll 有什么区别?(追问率极高)

A: select 用位图传集合,glibc 的 FD_SETSIZE 是 1024;poll 改用数组,不受该上限限制,但仍要扫描候选项。epoll 在内核维护关注列表和就绪列表,直接返回就绪事件,适合连接多而活跃少的场景。

W: 十万连接只有一百个活跃时,select 和 poll 每轮都要让应用与内核检查整批候选项,很多工作花在沉默连接上。epoll 注册关注关系后,事件到来时进入就绪列表,应用主要处理那一百个活跃连接。不过所有连接都持续活跃时,真正的数据处理仍然躲不开,优势会缩小。


八、零拷贝与 I/O 排查(生产高频)

知识点

  • 传统文件发送:常规 read() 加 write() 会让文件数据从页缓存复制到用户缓冲区,再复制到套接字缓冲区,同时发生用户态与内核态切换;大文件传输会消耗 CPU 与内存带宽。
  • mmap:mmap() 把文件页映射进进程虚拟地址空间,应用通过内存访问页缓存,省去 read() 把数据再复制到用户缓冲区的步骤;首次访问仍可能触发缺页,修改与持久化也要遵守映射标志和回写语义。
  • sendfile:sendfile() 在两个文件描述符之间由内核搬运数据,避免数据往返用户空间;具体是否还发生内核内部复制取决于内核、设备和协议能力,因此“零拷贝”不是任何路径都零复制。
  • DMA:直接内存访问允许磁盘控制器和网卡在设备与内存之间传输数据,CPU 负责配置与完成处理,不必逐字节搬运;CPU 仍要处理协议、描述符和中断或轮询。
  • I/O 排查链路:先用 top、vmstat 判断 CPU 是否在等待 I/O,再用 iostat -xz 看设备延迟、队列和吞吐,用 pidstat -d 找到高读写进程,最后关联文件、请求和应用日志。
  • 指标边界:高 iowait 表示 CPU 空闲期间存在未完成 I/O,不单独等于磁盘已饱和;设备 %util 对可并行处理请求的现代存储也不能单独作为饱和结论。

面试问答

  • Q1:什么是零拷贝,mmap 和 sendfile 呢?

A: 零拷贝旨在减少内核与用户空间之间的复制和切换。mmap 把文件页映射到用户地址空间,适合应用处理内容;sendfile 让内核直接传送文件数据,适合静态文件。它不保证物理路径完全没有复制,还要看系统实现和设备能力。

W: 静态文件服务若先 read() 到用户缓冲区,再 write() 到套接字,应用没有修改正文,却付出了两次跨边界调用和两段内存复制。sendfile 把这条路径留在内核,省掉用户空间中转。若应用必须解析或改写正文,仍要访问数据,此时映射文件更合适。

  • Q2:服务器 I/O 等待高怎么排查?(排查题)

A: 先确认是设备慢、请求量大还是内存换页。用 vmstat 看阻塞任务和交换活动,用 iostat -xz 看设备延迟、队列与吞吐,再用 pidstat -d 定位进程。找到进程后追到文件和请求,不能只凭 iowait 下结论。

W: 批处理突然扫大表时,磁盘吞吐可能升高但单次延迟仍正常,这是请求量变大;磁盘故障时吞吐不高,延迟和队列却会拉长;内存不足则常伴随换入换出,业务读写只是受害者。三种现象都可能表现为响应变慢,必须沿系统、设备、进程、请求逐层缩小范围。