操作系统常见面试题总结(下)
操作系统面试下篇围绕三条主线:虚拟内存如何隔离与翻译地址、I/O 模型如何高效搬运数据、文件系统如何组织与持久化数据。能否解释「epoll 为什么撑得住十万连接」「write 返回后数据到底在哪」,就看这一篇的功底。
考察地图
- 虚拟内存与内存管理:虚拟内存的价值、多级页表与 TLB、缺页异常、页面置换。考察对「虚拟地址 → 物理地址」整条翻译链路的理解。
- I/O 模型:一次阻塞 read 的完整旅程、select/poll/epoll、零拷贝三条路线。考察对「数据经过哪几层、在哪个阶段阻塞」的把握。
- 文件系统:inode/dentry 与链接、write 与 fsync 的持久化边界。考察对存储栈的实证理解。
- 建议自行查阅:内存管理五大职责、VSZ/RSS/PSS 指标、内存碎片与伙伴系统/SLAB、分页与分段的对比、Swap/工作集/抖动、Overcommit 与 OOM Killer、open() 流程、文件的块分配方式、日志文件系统、磁盘调度算法(FCFS/SSTF/SCAN/LOOK)——偏记忆或专题性强,按需补充。
虚拟内存与内存管理
什么是虚拟内存?没有它会怎样?
结论:虚拟内存是给每个进程的一层地址抽象:进程看到连续、私有的虚拟地址,数据落到哪块物理内存由内核与 MMU 决定。它一次性解决了隔离、利用率和编程模型三个问题。
原理:机制上,CPU 中的 MMU 按页表把虚拟地址翻译成物理地址。由此换来四项能力:隔离——各进程页表私有,相同的虚拟地址映射到不同物理页,互相踩不到;按需调页——不必把进程全部数据装进内存,访问到再加载;离散分配——物理页可以散落各处,拼接交给页表;受控共享——动态库、共享内存让多个进程映射同一批物理页。没有虚拟内存时程序直接操作物理地址:任何程序都能改到内核内存,两个程序地址冲突就互相覆盖,且必须全量常驻内存。
延伸:虚拟内存还能让可用空间「超过」物理内存——暂时不用的页换出到磁盘,代价是一旦频繁换页,系统会急剧变慢。
追问:两个进程打印同一全局变量的地址可能完全相同,为什么互不干扰?提示:地址数值相同不等于物理页相同,页表是进程私有的。
为什么需要多级页表?TLB 又解决什么问题?
结论:多级页表解决「页表本身太占内存」,TLB 解决「多级页表查表太慢」。一个省空间,一个补时间,两者配合才可用。
原理:32 位地址配 4 KB 页需要 2^20 个页表项,每项 4 字节,单级页表每进程约 4 MB,且大部分项对应从未使用的地址。多级页表让顶层覆盖全地址空间、下级按需创建——某段虚拟地址没用过就不建对应下级页表,两级页表实际占用可降到十几 KB。代价是翻译要多走几层内存访问,于是引入 TLB:缓存近期「虚拟页 → 物理页」映射的硬件快表,命中即跳过页表查询,未命中才走多级页表并回填。TLB 的高命中率依赖局部性:程序一段时间内反复访问的页就那么几个。
延伸:页表映射变更后,内核必须按体系结构要求使对应 TLB 项失效(如 x86 的 invlpg),否则 CPU 继续使用旧映射——TLB 一致性是内核开发的经典坑。
追问:64 位系统为什么用 4~5 级页表而不是哈希页表?提示:多级结构对硬件页表遍历器友好,且天然支持按需创建。
什么是缺页异常(Page Fault)?它一定代表程序出错吗?
结论:Page Fault 是地址翻译或页级权限检查无法完成时、由当前访存指令触发的同步异常。它不等于出错——大量缺页是内核有意安排的正常流程。
原理:CPU 触发缺页后进入内核,内核检查地址是否落在合法 VMA 且权限匹配。可修复情形包括:匿名页首次访问(现场分配物理页)、文件页或 Swap 页未驻留(发起调页)、写 COW 页(先复制再写)——修好映射后重新执行那条指令,进程毫无感知。不可修复情形如访问未映射地址、写只读映射,内核向当前线程投递 SIGSEGV,进程按默认动作终止。按开销分:主缺页要从磁盘读页,涉及真实 I/O;次缺页只是建立映射或分配零页,便宜得多。
延伸:C/C++ 越界不保证立刻段错误——只要越界地址仍落在已映射且权限允许的页里,CPU 并不认识语言层的数组边界,结果往往是悄悄踩坏相邻数据。
追问:fork() 之后父子进程都不写数据,内存占用翻倍吗?提示:COW 让父子共享物理页并标只读,谁先写谁触发复制。
常见的页面置换算法有哪些?实际系统用哪个?
结论:教科书五种:OPT、FIFO、LRU、LFU、CLOCK。OPT 是理论上限但无法实现;实际系统多用 LRU 的低成本近似,Linux 采用活跃/非活跃双链表的改良方案。
原理:评价标准是缺页率。OPT 淘汰未来最久不用的页,需要预知未来,只作衡量基准。FIFO 淘汰最早进入的页,实现简单但误伤热点页,还存在 Belady 异常——分配的页框变多、缺页反而变多,根源是它只看进入顺序、不看访问热度。LRU 淘汰最久未访问的页,贴合时间局部性,效果接近 OPT,但精确实现要维护时间戳或链表,每次访问都要更新,成本过高。LFU 按访问频率淘汰,早期热点会赖着不走,需配合计数衰减。CLOCK 是 LRU 的近似:页面排成环,访问位为 1 则清零并给第二次机会,为 0 才淘汰,用一个硬件标志位换来近似效果。
延伸:工程实现从不照搬教科书:Linux 用 active/inactive 链表加 refault 检测;InnoDB Buffer Pool 把 LRU 分新老两段,防止全表扫描把真热点挤出去。置换失控的极端是抖动(Thrashing):物理内存装不下活跃进程的工作集,页面反复经 Swap 换入换出,CPU 大量时间耗在换页而非业务上。
追问:LRU 为什么应付不了「顺序扫描一遍大文件」?提示:每个页只访问一次,会把整个工作集挤出内存——这正是 InnoDB 改造 LRU 的动机。
I/O 模型
一次阻塞 read() 从发起到返回经历了什么?
结论:以 read(fd, buf, count) 为例:glibc 包装 → syscall 陷入内核 → 校验与分发 → 数据就绪则拷贝到用户缓冲返回;未就绪则线程睡眠等待 → 设备中断唤醒 → 线程重新被调度后才返回用户态。
原理:完整链路可能涉及三种内核入口:系统调用(进内核)、硬件中断(磁盘/网卡报告数据到达)、上下文切换(线程睡眠让出 CPU、唤醒后重新获得)。但也可能一步都不卡:读普通文件时数据若已在 Page Cache,内核直接拷贝返回,无中断、无切换、无等待——这解释了为什么「读文件」有时微秒级、有时毫秒级,差别全在数据此刻位于哪一层。两个细节:返回值可能小于 count(partial read),调用方要循环读;慢系统调用可能被信号打断返回 EINTR,需要处理重启。
延伸:这条链路是理解一切 I/O 模型的底图:非阻塞、多路复用、异步 I/O 改写的都是「等待数据就绪」这个阶段的方式。
追问:read 一个 socket 和 read 一个普通文件,内核路径上最大的区别是什么?提示:文件有 Page Cache 可命中,socket 数据只能等网卡收包进内核缓冲。
select、poll 和 epoll 有什么区别?
结论:select/poll 每次调用都把全部监听 fd 交给内核,返回后应用线性扫描找就绪者;epoll 用 epoll_ctl 把监听集合常驻内核,epoll_wait 只返回就绪事件。连接多而活跃少时,epoll 优势巨大。
原理:三者都属于 I/O 多路复用,解决「一个线程同时等很多 fd 的就绪事件」,且都是同步 I/O——内核只通知可读可写,读写动作仍由应用自己完成。select 用定长 fd_set 位图,受 FD_SETSIZE(通常 1024)限制,fd 编号超限无法监听。poll 换成 pollfd 数组解除数量限制,但每次仍整体传入、整体扫描,O(n) 成本没变。epoll 在内核维护 interest list,fd 就绪时由回调挂入 ready list,等待时只取就绪项,复杂度降到与活跃数相关。epoll 还分 LT/ET:LT 只要缓冲区有数据就反复通知;ET 只在状态变化边沿通知一次,必须配非阻塞 fd 并循环读到 EAGAIN。
延伸:epoll 并非处处更快——连接很少或全部活跃时,回调与就绪链表的维护成本可能盖过收益;它的主场是海量长连接服务端(Nginx、Redis、Netty)。
追问:ET 模式为什么必须配非阻塞 fd?提示:边缘触发只通知一次,阻塞读没读尽就再无下次通知,连接直接卡死。
什么是零拷贝?mmap、sendfile、splice 怎么选?
结论:零拷贝不是零次拷贝,而是尽量消除 CPU 在内核缓冲与用户缓冲之间的无用搬运。选型口诀:要改数据用 mmap,文件到 socket 原样转发用 sendfile,任意 fd 之间转发再考虑 splice。
原理:传统 read + write 发文件有 4 次拷贝:磁盘→内核缓冲(DMA)、内核→用户(CPU)、用户→socket 缓冲(CPU)、socket→网卡(DMA)。中间两次 CPU 拷贝纯属浪费——应用没改数据,只是让用户态当了二传手,还搭进 4 次特权级切换。mmap + write 把文件映射进地址空间,应用直接读写 Page Cache,省掉内核→用户那次拷贝,适合发送前要解析修改的场景。sendfile 让数据全程留在内核:Page Cache 直接送 socket,网卡支持 SG-DMA 时 payload 连 socket 缓冲都不进——Kafka 发送日志段走的就是这条路。splice 借 pipe 在内核搬页引用,更通用,但路径里要有管道,文件到 socket 常需两次调用。
延伸:零拷贝有失效场景:TLS 加密、压缩、内容过滤都要应用真正摸到数据,payload 留不住内核路径;小文件上映射与缺页的固定成本也可能反超收益。
追问:除了拷贝次数,sendfile 还省了什么?提示:read+write 两次系统调用共 4 次特权级切换,sendfile 一次调用只有 2 次。
文件系统
文件、目录、inode、dentry 是什么关系?硬链接和软链接的区别?
结论:目录项把文件名映射到 inode 号;inode 存文件元数据(类型、权限、大小、时间戳、数据块位置)但不存文件名;dentry 是 VFS 在内存中的路径查找缓存。硬链接是指向同一 inode 的另一个目录项;软链接是内容为目标路径的独立文件。
原理:文件名存在目录的数据里而非 inode 里,所以一个 inode 可以被多个目录项引用——这正是硬链接的本质,ls -l 第二列的链接计数即引用数;删除文件只是计数减一,归零才真正释放数据。硬链接不能跨文件系统(inode 号只在各自文件系统内有意义),也不允许链目录(防目录树成环)。软链接有自己的独立 inode,内容是一段路径字符串,访问时按路径重新解析,因此能跨文件系统、能链目录,但源文件被删就成了悬空链接。同一文件系统内 mv 只改目录项不动数据,也是这套结构的直接推论。
延伸:dentry 还能缓存「不存在」的查找结果(negative dentry);df -h 满了但 du 找不到大文件时,先查被删除但仍被进程打开的文件(lsof +L1)——计数未归零,空间就不会释放。
追问:为什么硬链接不能跨文件系统而软链接可以?提示:inode 号是文件系统局部编号,路径字符串则与具体文件系统无关。
write() 成功后数据一定落盘了吗?
结论:不一定。buffered I/O 的 write() 返回成功,通常只代表数据进入 Page Cache 并被标脏,离持久化到介质还隔着内核回写、磁盘缓存好几层。
原理:数据路径是:用户缓冲 → Page Cache(脏页)→ 内核回写线程或 fsync 触发 → 磁盘控制器缓存 → 介质。要拿到持久化保证必须显式同步:fsync() 等文件数据与关键元数据都落盘才返回;fdatasync() 只同步数据与读取必需的元数据(如文件大小);O_SYNC/O_DSYNC 打开标志让每次写都带同步语义。两个易漏点:其一,write() 可能部分写入,调用方必须检查返回值循环写;其二,新建文件或 rename 之后若要求掉电不丢,还需对父目录 fd 做一次 fsync——目录项的持久化与文件数据是分开的两件事。
延伸:频繁 fsync 是吞吐杀手,每次都要等真实落盘;数据库用 redo log 把随机写转成顺序写、再加组提交,正是在持久化与性能之间找平衡。
追问:fsync 返回成功就万无一失了吗?提示:还要看磁盘控制器的易失缓存与 flush/barrier 语义,企业盘与消费盘表现不同。

