操作系统常见面试题总结(上)
操作系统面试上篇聚焦四条主线:CPU 特权级与系统调用、进程与线程的边界、调度与负载、死锁。这组题几乎是每场后端面试的开场题,答好的关键不是背定义,而是讲清机制、触发条件和代价。
考察地图
- 特权级与系统调用:用户态/内核态为何存在、系统调用的完整过程、系统调用与上下文切换的关系。考察对 CPU 特权级和内核入口机制的理解。
- 进程与线程:二者区别、进程状态转换、上下文切换成本。考察对「资源边界」与「执行现场」的划分是否清晰。
- 调度与负载:经典调度算法的取舍、load average 与 CPU 使用率的分辨。考察调度器权衡思路和线上指标解读能力。
- 死锁:定义、四个必要条件、四类处理策略。考察并发推理能力。
- 建议自行查阅:操作系统的定义与六大功能、有了进程为什么还需要线程、PCB/TCB 字段清单、线程同步方式(互斥锁/读写锁/信号量/条件变量)、IPC 选型、fork/exec/wait 语义、手写死锁 Demo——多为记忆或编程练习,面试中通常只作铺垫。
特权级与系统调用
什么是用户态和内核态?为什么要区分?
结论:两者描述 CPU 执行代码时的特权级别。应用代码跑在权限受限的用户态;内核代码跑在内核态,可执行特权指令、访问全部资源。区分的目的只有两个词:安全、稳定。
原理:x86 用 ring0~ring3 表示特权级,Linux 只用 ring0(内核)和 ring3(用户)。修改页表、开关中断、访问设备寄存器等指令被硬件标记为特权指令,用户态执行会直接触发异常。若没有这层隔离,任何一个越界写的程序都可能踩坏内核或其他进程的内存,进程隔离便无从谈起。从用户态进入内核态只有三个入口:系统调用(同步、主动)、硬件中断(异步)、异常(同步、被动,如缺页)。
延伸:「进入内核态」不等于「变成内核进程」——内核代码是代表当前线程执行的,使用的仍是该线程的内核栈,PID 不变。
追问:用户态/内核态切换比普通函数调用贵在哪些地方?提示:入口固定、要保存恢复状态、有权限与参数校验,还可能影响缓存局部性。
系统调用的过程了解吗?一定会发生线程上下文切换吗?
结论:系统调用是应用请求内核服务的受控入口,流程是「准备调用号和参数 → 执行 syscall 指令陷入内核 → 内核校验并分发执行 → 结果经寄存器返回」。它不一定伴随线程上下文切换。
原理:以 Linux x86-64 的 read() 为例:glibc 包装函数把系统调用号放进 rax、参数按 ABI 放入 rdi/rsi/rdx,执行 syscall;CPU 切到内核入口,保存必要现场后按调用号分发;内核检查 fd、缓冲区与权限,走 VFS→文件系统/协议栈→驱动;数据就绪则拷回用户缓冲并返回,未就绪则当前线程进入等待队列。是否发生线程切换取决于最后一步:内核快速处理完返回原线程,就只有特权级切换;线程因等 I/O 阻塞、调度器换人运行,才算上下文切换。反过来,时钟中断进了内核但调度器仍让原线程继续,也不算。
延伸:strace 可观察进程的系统调用序列,是排查线上问题的利器;出错时 glibc 会把内核错误码转成 -1 并设置 errno。
追问:为什么说系统调用「贵」但远没有传说中那么贵?提示:syscall 指令本身只需几十纳秒级,贵的是内核路径里的校验、拷贝与可能的调度。
进程与线程
进程和线程的区别是什么?
结论:进程是资源分配与隔离的基本单位,拥有独立虚拟地址空间;线程是 CPU 调度的基本单位,同一进程内的线程共享进程资源,只私有栈、寄存器、程序计数器等执行现场。
原理:按五个维度展开:地址空间(进程独立、线程共享);私有内容(进程持有 PID、打开文件表、页表,线程持有栈、PC、寄存器、TLS);通信方式(进程走管道/消息队列/共享内存/Socket 等 IPC,线程直接读写共享内存但需处理同步);创建与切换开销(进程更贵,切换可能伴随地址空间更换和 TLB 失效);故障影响(进程崩溃相互隔离,一个线程致命错误通常带走整个进程)。JVM 视角同样成立:线程共享堆和方法区,私有虚拟机栈、本地方法栈与程序计数器。
延伸:Linux 里进程和线程统一用 task_struct 表示,clone() 通过标志位决定共享哪些资源——共享地址空间即「线程」,不共享即「进程」,二者在数据结构上没有本质差异。
追问:既然同进程线程共享地址空间,线程切换为什么仍有成本?提示:寄存器与栈要换,调度簿记要做,CPU 缓存局部性也会被破坏。
进程有哪几种状态?阻塞和就绪的关键区别在哪?
结论:经典五态:创建、就绪、运行、阻塞、终止。就绪与阻塞的关键区别在于缺什么:就绪只缺 CPU,阻塞缺的是事件——CPU 空着它也跑不了。
原理:转换由事件驱动:就绪拿到 CPU 进入运行;运行态时间片用完回到就绪;运行中发起阻塞 I/O、等锁或等事件则进入阻塞;等待的事件完成后先回就绪队列而非直接运行,要等调度器再次选中——调度决策统一由调度器做,优先级和公平性才能生效。部分教材还有挂起态:阻塞强调「在等事件」,挂起强调「被换出到外存或被人为暂停」,二者正交,进程可以既阻塞又被挂起。
延伸:Linux 的 D 状态(不可中断睡眠)值得单记:进程在内核等磁盘 I/O 时连信号都不响应,D 状态进程大量堆积通常指向存储层故障。
追问:为什么事件完成后不直接抢占当前运行的进程?提示:唤醒只意味着「具备运行资格」,是否抢占由调度器按优先级与策略决定。
什么是上下文切换?为什么进程切换通常比线程切换更贵?
结论:上下文切换是 CPU 从一个执行实体切到另一个时,保存旧现场(寄存器、PC、栈指针)并恢复新现场的过程。进程切换更贵,核心原因是还要切换地址空间。
原理:同进程线程切换只需更换栈和寄存器等执行现场;跨进程切换除此之外要写页表基址寄存器(x86 的 CR3),TLB 中旧地址空间的映射随之作废,新进程前期访问会经历一波 TLB miss,CPU 缓存的局部性也被破坏——这些隐性成本往往大于切换指令本身。务必区分两个概念:系统调用、缺页、中断都会进内核,但只要内核处理后返回原线程,就没有发生线程上下文切换。观测可用 vmstat 的 cs 列与 pidstat -w。
延伸:线上若发现大量时间耗在上下文切换与锁等待上,继续加线程只会更糟,正确方向是减少竞争、拆分锁或降低并发度。
追问:PCID/ASID 这类硬件特性缓解了什么开销?提示:给 TLB 项打地址空间标签,切换进程时不必整体刷空 TLB。
调度与负载
进程调度算法有哪些?各自适合什么场景?
结论:经典算法分两类——非抢占的 FCFS、SJF,抢占的时间片轮转(RR)、优先级调度;工程上的集大成者是多级反馈队列(MLFQ),Linux 实际使用的是 CFS/EEVDF。
原理:FCFS 按到达顺序执行,简单但有护航效应:长任务挡路,短任务平均等待被拉长。SJF 优先短任务,平均等待最优,但无法预知任务长度,且长任务会饥饿。RR 给每个任务固定时间片轮流执行,时间片是双刃剑:太短放大切换开销,太长退化为 FCFS。优先级调度能表达紧急程度,但要配合老化机制防止低优先级饥饿。MLFQ 设多条优先级队列并按行为动态调整:用完整个时间片的 CPU 密集任务降级,常主动等 I/O 的交互任务保持高优先级——不需要预知未来就逼近了 SJF 的响应表现。
延伸:Linux 普通任务长期由 CFS 调度:按权重与虚拟运行时间 vruntime 分配 CPU,vruntime 最小者优先运行;内核 6.6 起 fair 调度类引入 EEVDF,用 lag 和虚拟截止时间改进任务选择。
追问:MLFQ 凭什么区分交互型与 CPU 密集型任务?提示:交互任务频繁主动让出 CPU,行为本身暴露了类型。
load average 和 CPU 使用率有什么区别?
结论:load average 是一段时间内「可运行 + 不可中断睡眠」任务的平均数量;CPU 使用率是 CPU 时间花在用户态、内核态、iowait、空闲等处的占比。load 高不等于 CPU 忙。
原理:Linux 的 load 统计 R(运行/可运行)和 D(不可中断睡眠)状态的任务,因此 load 高有两种截然不同的成因:一是大量任务争抢 CPU,此时 CPU 使用率同样高;二是大量任务在 D 状态等磁盘、网络存储或 Swap,此时 CPU 可能很闲——后者指向 I/O 瓶颈而非算力不足。解读 load 还必须结合逻辑 CPU 数:load 8 对 4 核是过载,对 64 核毫无压力。排查路径:uptime 看 1/5/15 分钟趋势判断突发还是持续,再用 top、vmstat 1、mpstat -P ALL 定位时间花在哪。
延伸:容器里看到的 load 是宿主机级指标,包含整机任务;容器内更应看 cgroup 的 throttling 数据。
追问:iowait 高说明什么、又不能说明什么?提示:只说明有 CPU 在空转等 I/O,单核场景下极易虚高,需结合磁盘延迟与 util 判断。
死锁
什么是死锁?四个必要条件是什么?
结论:死锁是一组线程互相等待对方持有的资源、等待关系成环,导致无人能自行推进。四个必要条件必须同时成立:互斥、占有并等待、不可剥夺、循环等待。
原理:互斥——资源一次只能一个持有者;占有并等待——持有 A 的同时申请 B;不可剥夺——资源只能由持有者主动释放;循环等待——等待链首尾相接。经典画面是交叉持锁:线程 1 持锁 A 申请锁 B,线程 2 持锁 B 申请锁 A。注意死锁不是「等得久」:普通阻塞的等待链不成环,锁释放即可继续;成环后没有外力介入永远不会解开。四个条件是「必要」条件的用法在于:只要稳定破坏其中任意一条,就能从结构上预防死锁。
延伸:写 Demo 复现时线程持锁后常加 sleep——它不是死锁的原因,只是把交错窗口拉大,让环更容易形成。
追问:资源分配图中「有环」是否一定死锁?提示:每类资源仅一个实例时环等价于死锁;多实例时有环只是可能死锁。
死锁有哪些处理思路?工程上最常用哪招?
结论:四类策略——预防、避免、检测与解除。工程上最常用的是预防中的「固定加锁顺序」,数据库则偏好「检测 + 回滚」。
原理:预防是静态破坏必要条件:全局限定加锁顺序直接破坏循环等待;一次性申请全部资源破坏占有并等待;tryLock 超时主动放弃变相破坏不可剥夺。避免是动态判断分配后系统是否仍处于安全状态,代表是银行家算法——理论价值大于实用价值,业务系统极少真正实现。检测允许死锁发生,再检查等待图中是否成环:Java 用 jstack 或 ThreadMXBean.findDeadlockedThreads(),InnoDB 内置等待图检测。解除是事后补救:杀进程、回滚事务、抢占资源后让应用重试。数据库事务天然支持回滚,所以敢用检测路线;应用线程没有回滚语义,只能以预防为主。
延伸:缩小临界区、持锁期间不做 RPC 与磁盘 I/O 等慢操作,既降低死锁概率也降低锁竞争,是收益最高的日常编码习惯。
追问:为什么固定加锁顺序能破坏循环等待?提示:锁形成全序关系后,等待链不可能首尾相接,可用数学归纳直接证明无环。

