操作系统八股不靠死背:跟着一个程序走完它的一生
hello 我是逆境

第一次准备操作系统面试时,许多人会陷进同一个坑。
进程有五种状态,背过;死锁有四个条件,也背过;页面置换算法能一口气报出 FIFO、LRU、LFU。可面试官稍微换个问法:“线程为什么比进程轻?”“缺页异常到底算不算错误?”脑子就像刚被清空的缓存,只剩几个缩写在打转。
林舟第一次面试就是这样。
面试官问:“浏览器里输入一个网址后,操作系统做了什么?”
他准备了一百多道题,却不知道该从哪一道开始答。进程、内存、网卡、文件描述符,原来不是四本互不相干的说明书,而是同一件事的四个侧面。
这篇文章换一种学法。我们跟着一个程序,从双击启动开始,看它怎样得到 CPU,怎样申请内存,怎样和其他进程说话,怎样读文件、收网络数据,最后怎样退出。每个知识点后面都留了一段“面试回答”。先把画面想明白,再把这段话说顺,比单独背定义牢靠得多。
先记住这张总图:
程序启动
↓
操作系统创建进程
↓
调度器分配 CPU ─────→ 线程运行、切换、同步
↓
建立虚拟地址空间 ───→ 页表、TLB、缺页、页面置换
↓
发起系统调用 ───────→ 文件、网络、设备 I/O
↓
中断通知 CPU ───────→ 进程被唤醒,继续执行
↓
进程退出,系统回收资源
第一站:双击图标之后,程序变成了进程
硬盘上的程序像一份还没开工的剧本。它包含代码和数据,但不会自己运行。双击图标后,操作系统把代码映射进内存,准备栈和堆,创建进程控制块,再把入口地址交给 CPU。剧本这才真正开演。
1. 程序和进程有什么区别?
程序是存放在磁盘上的可执行文件,是静态的;进程是程序的一次运行,是动态的。同一个浏览器程序可以启动多个进程,每个进程都有自己的运行状态和资源。
操作系统用 PCB,也就是进程控制块,记录进程编号、状态、寄存器信息、调度信息、内存映射和打开的文件等内容。可以把 PCB 理解成进程在操作系统里的档案袋。进程暂停后还能接着运行,靠的就是里面保存的现场。
面试回答:程序是静态的指令和数据集合,进程是程序的一次执行实例。进程拥有独立的虚拟地址空间和操作系统分配的资源,内核通过 PCB 保存它的状态、调度和资源信息。
记忆时抓住一句:程序是菜谱,进程是正在做的那锅菜。
2. 一个进程通常怎样创建另一个进程?
在 Unix/Linux 中,常见组合是 fork()、exec() 和 wait()。
fork() 先复制出一个子进程。这里并不会立刻把父进程的所有内存完整复制一遍,系统通常使用写时复制。父子进程先共享物理页面,谁要修改,谁再得到一份副本。
子进程如果要运行另一个程序,会调用 exec(),用新程序替换自己的地址空间。父进程可以调用 wait() 等待子进程结束并回收它的退出信息。
面试回答:Linux 中父进程通常通过
fork()创建子进程,再由子进程调用exec()加载新程序。fork()常借助写时复制降低内存开销,父进程通过wait()或waitpid()获取子进程退出状态并完成回收。
3. 进程有哪些状态?

先想象林舟在医院排队:
新建 → 就绪 → 运行 → 终止
↑ ↓
└── 阻塞
挂完号、等医生叫号,是就绪:什么都准备好了,只差 CPU。医生让他先去等化验结果,是阻塞:即使现在把医生,也就是 CPU,让给他,他也没法继续。
运行中的进程时间片用完,会回到就绪状态;等待的 I/O 完成后,阻塞进程也会先回到就绪队列,而不是直接霸占 CPU。
面试回答:常见状态有新建、就绪、运行、阻塞和终止。就绪表示运行条件已经具备,只等 CPU;阻塞表示正在等待 I/O、锁或某个事件。时间片用完会从运行转为就绪,等待事件完成会从阻塞转为就绪。
4. 什么是孤儿进程和僵尸进程?
父进程先退出,子进程还在运行,这个子进程叫孤儿进程。它通常会被系统中的其他进程接管并负责回收,并不是“没人管就一定出错”。
子进程已经退出,父进程却没有调用 wait() 读取它的退出状态,内核只好保留一小条进程表记录,这叫僵尸进程。僵尸进程不再执行,也几乎不占普通内存,但大量堆积会耗尽进程号或进程表项。
面试回答:孤儿进程是父进程先退出、子进程仍在运行,系统会安排其他进程接管回收;僵尸进程是子进程已经结束,但父进程还没有读取其退出状态,因此内核暂时保留进程表项。父进程调用
wait()可以回收僵尸进程。
第二站:进程开了一家公司,线程是里面真正干活的人
进程拿到了办公室、文件柜和仓库,但真正执行指令的是线程。一个进程至少有一个线程,也可以让多个线程共同处理任务。
5. 进程和线程有什么区别?
进程主要解决“资源归谁”,线程主要解决“谁来执行”。
不同进程通常拥有独立的虚拟地址空间。一个进程写坏了自己的内存,正常情况下不会直接改坏另一个进程。相同进程里的线程共享代码段、堆、全局变量和打开的文件,但各自保存栈、程序计数器和寄存器现场。
共享带来速度,也带来麻烦。两个线程都能碰同一个变量,就可能发生数据竞争;一个线程非法访问内存,还可能拖垮整个进程。
面试回答:进程是资源分配和隔离的基本单位,线程是 CPU 调度的基本单位。同一进程的线程共享地址空间、堆和文件等资源,但每个线程有独立的栈、程序计数器和寄存器状态。线程通信方便、切换通常更轻,但需要处理同步问题,而且单个线程崩溃可能影响整个进程。
6. 为什么线程切换通常比进程切换快?
切换两个同进程线程时,地址空间通常不变,页表不用跟着换。切换进程时,除了寄存器和调度状态,往往还要切换地址空间;缓存和 TLB 的有效内容也可能受到更大影响。
“线程切换一定很便宜”也不准确。只要切换,就要保存现场、执行调度代码,还可能破坏 CPU 缓存的局部性。线程开得太多,系统会忙着换人,真正做事的时间反而变少。
面试回答:同一进程的线程共享地址空间,切换时通常只需保存和恢复寄存器、程序计数器、栈指针等线程上下文。进程切换还可能涉及页表和地址空间切换,对 TLB、CPU 缓存的影响更明显,因此通常成本更高。
7. 什么是上下文切换?
林舟正在写代码,电话响了。他先记下“改到第 83 行、变量值是多少”,接完电话才能从原处继续。CPU 做的事情很像。
操作系统暂停一个进程或线程时,会保存程序计数器、寄存器、栈指针等执行现场;选择下一个任务后,再恢复它之前的现场。这就是上下文切换。
成本分成两部分:保存、恢复和调度本身是直接成本;缓存失效、TLB 命中率下降、流水线重新填充是容易被忽视的间接成本。
面试回答:上下文切换是 CPU 从一个任务切换到另一个任务时,保存前一个任务的执行现场并恢复后一个任务现场的过程。它会消耗内核时间,还可能降低缓存和 TLB 的命中率,所以频繁切换会影响性能。
8. 并发和并行有什么区别?
一名厨师交替炒两道菜,是并发。两名厨师同时各炒一道菜,是并行。
并发强调一段时间内多个任务都在推进,单核 CPU 通过快速切换也能做到。并行强调同一时刻有多个任务真正执行,通常需要多核 CPU。多线程程序可以并发,但在单核机器上不能让 CPU 计算真正并行。
面试回答:并发是多个任务在同一时间段内交替推进,并行是多个任务在同一时刻同时执行。单核可以通过调度实现并发,真正的 CPU 并行通常需要多个核心。
9. 什么是协程?它为什么轻量?
协程可以理解为线程里面由程序自己管理的小任务。它的创建、暂停和恢复通常在用户态完成,不必每次都请内核调度,因此能以较低成本承载大量 I/O 任务。
但协程不是魔法。协程如果直接执行阻塞式系统调用,底层线程仍可能被卡住。成熟的协程运行时会配合非阻塞 I/O、事件循环或多个工作线程,避免一个协程堵住整条执行链。
面试回答:协程是用户态的轻量级执行单元,通常由语言运行时或程序调度,切换时主要保存少量用户态上下文,因此创建和切换成本较低。它适合高并发 I/O 场景,但仍要处理阻塞系统调用以及协程与内核线程的映射问题。
第三站:应用程序不能随便碰硬件,它要先找操作系统
普通程序运行在用户态,像住客待在酒店房间里。它不能自己拆电梯、改电表。要读磁盘或发送网络数据,必须去前台,也就是通过系统调用进入内核。
10. 用户态和内核态有什么区别?
CPU 提供不同的权限级别。用户态只能执行受限指令,不能直接访问任意物理内存和硬件设备;内核态拥有更高权限,可以管理进程、内存、文件系统和设备。
这种隔离不是为了给程序添麻烦,而是为了控制故障范围。普通程序写飞一个指针,通常只会伤到自己;如果所有程序都能随意操作硬件,一次错误就可能让整台机器失控。
面试回答:用户态权限受限,应用不能直接执行特权指令或访问硬件;内核态拥有管理 CPU、内存和设备的权限。应用通过系统调用请求内核服务,权限隔离能提高系统的安全性和稳定性。
11. 什么是系统调用?
open()、read()、write()、fork() 这类接口背后都可能进入内核。程序准备好参数,执行专门的陷入指令,CPU 切到内核态;内核检查参数和权限,完成服务,再把结果返回用户态。
普通函数调用只是在当前权限级别里跳到另一段代码。系统调用涉及受控的权限切换,所以通常比普通函数调用贵。
面试回答:系统调用是用户程序请求内核服务的受控入口。调用时程序通过陷入指令从用户态进入内核态,内核校验参数并执行相应服务,再返回用户态。它与普通函数调用的主要区别是会跨越权限边界。
12. 中断、异常和系统调用有什么区别?
这三个概念常被揉成一团,可以按“谁触发、何时触发”来分。
- 中断通常由外部硬件异步触发。例如网卡收到数据后通知 CPU。
- 异常由当前指令同步触发。例如除零、非法访问和缺页。
- 系统调用由程序主动发起,是一次有意安排的同步陷入。
处理结束后,CPU通常会回到被打断的位置,或者由内核决定终止出错的进程。
面试回答:中断一般来自外部设备,与当前指令异步;异常由当前指令执行产生,与指令同步;系统调用是应用主动触发的受控异常,用来请求内核服务。
13. 从用户态切到内核态,等于发生了一次进程切换吗?
不等于。
进程调用 read() 进入内核时,仍可能是同一个进程在执行,只是 CPU 权限级别变了,这叫模式切换。如果读取的数据还没准备好,内核把该进程阻塞,再调度另一个进程运行,这时才发生上下文切换。
面试回答:用户态和内核态切换只是 CPU 权限级别发生变化,不一定更换当前进程;上下文切换则意味着 CPU 改为执行另一个进程或线程。系统调用会发生模式切换,但只有任务被阻塞或抢占时才可能继续发生上下文切换。
第四站:只有几颗 CPU,几百个线程该让谁先跑?
就绪队列里总有人等着。调度器像机场塔台,决定谁上跑道、能跑多久、什么时候让位。
14. 常见的 CPU 调度算法有哪些?
先来先服务 FCFS 按到达顺序执行,实现简单,但前面如果是一个超长任务,后面的短任务会一起等。
短作业优先 SJF 优先运行预计时间短的任务,平均等待时间较低,不过很难准确预测任务长度,长任务还可能一直排不上。
时间片轮转 RR 给每个任务一小段 CPU 时间,时间用完就排回队尾,适合交互系统。优先级调度让重要任务先运行,却要防止低优先级任务饿死。多级反馈队列会根据任务的等待和运行表现调整队列,实际系统常使用这类综合思路。
面试回答:常见算法有 FCFS、SJF、时间片轮转、优先级调度和多级反馈队列。FCFS 简单但可能让短任务久等;SJF 平均等待时间较低但可能造成长任务饥饿;时间片轮转强调公平和响应速度;多级反馈队列通过不同优先级和时间片兼顾交互任务与计算任务。
15. 时间片是不是越小越好?
时间片很小,每个任务很快就能轮到,交互响应会变好;代价是上下文切换变得频繁。时间片很大,切换开销下降,但后面的任务要等更久,时间片轮转也会逐渐接近先来先服务。
所以时间片是在响应速度和切换成本之间取平衡,没有脱离工作负载的“最佳固定值”。
面试回答:时间片过小会增加上下文切换和调度开销,过大又会降低交互响应速度,并让轮转调度接近 FCFS。合理大小要结合任务耗时、响应要求和切换成本确定。
16. 什么是饥饿?老化怎样解决它?
如果高优先级任务不断到来,低优先级任务可能一直拿不到 CPU,这叫饥饿。老化的做法是让等待越久的任务逐步提高优先级,最终获得执行机会。
面试回答:饥饿是任务长期得不到 CPU 或资源,但系统整体仍在运行。老化会根据等待时间逐渐提高任务优先级,从而减少永久等待。
17. 线程数是不是越多越好?
不是。每个线程都要占用栈空间和内核资源,线程过多还会增加调度、上下文切换和锁竞争。
CPU 密集型任务很少等待,线程数通常接近可用核心数。I/O 密集型任务经常阻塞,可以用更多线程覆盖等待时间。理论上常见一个估算式:
线程数 ≈ CPU 核数 × (1 + 等待时间 / 计算时间)
它只适合给出起点。最终数量要靠压测,看吞吐量、延迟、CPU 使用率和上下文切换是否合适。
面试回答:线程数过多会增加内存占用、上下文切换和锁竞争。CPU 密集型任务一般接近核心数,I/O 密集型任务可以适当增加线程,用其他任务覆盖等待时间。生产参数应结合阻塞比例和压测结果确定。
第五站:进程住在不同房间,怎样互相递纸条?
进程之间默认不能直接读写对方内存,这是隔离带来的安全感。真要合作,就得使用操作系统提供的 IPC,也就是进程间通信机制。
18. 常见的进程间通信方式有哪些?
常见方式有管道、命名管道、消息队列、共享内存、信号、信号量和 Socket。
选择时看三个问题:数据量多大,要不要跨机器,是否需要保留消息边界。共享内存适合同一台机器上的大量数据交换;Socket 可以跨主机;信号只适合传递少量事件通知。
面试回答:IPC 方式包括管道、命名管道、消息队列、共享内存、信号、信号量和 Socket。共享内存通常复制较少、吞吐高,但需要额外同步;消息队列保留消息边界;Socket 既能本机通信,也能跨网络通信。
19. 普通管道和命名管道有什么区别?
普通管道通常由有亲缘关系的进程通过继承文件描述符使用。它传输的是字节流,本身没有消息边界,数据读走后就不在管道里了。
命名管道 FIFO 在文件系统中有名字,不相关的进程也可以通过路径打开它。这个“文件”主要是通信入口,数据仍由内核中的管道机制传递。
面试回答:普通管道通常用于有亲缘关系的进程,通过继承文件描述符建立联系;命名管道在文件系统中有路径,没有亲缘关系的进程也能打开。两者通常都是半双工字节流通信。
20. 为什么共享内存通常很快?
使用管道或消息队列时,数据通常要经过内核维护的缓冲区。共享内存则把同一组物理页面映射到多个进程的地址空间,一个进程写完,另一个进程可以直接读,减少了数据复制和系统调用。
快不代表省心。两个进程同时修改一块内存,照样会写乱,因此还要配合互斥锁、信号量或其他同步机制。
面试回答:共享内存让多个进程映射同一组物理页面,数据不必在进程之间反复复制,因此吞吐量通常较高。但它只解决共享问题,不保证访问顺序,必须配合同步机制避免竞争。
21. Socket 通信的大致流程是什么?
服务端先创建 Socket,绑定地址,开始监听,再通过 accept() 接收连接。客户端创建 Socket 后调用 connect()。连接建立后,双方使用 read()/write() 或 send()/recv() 收发数据,最后关闭描述符。
服务端:socket → bind → listen → accept → 收发数据 → close
客户端:socket → connect ─────────→ 收发数据 → close
accept() 返回的是一个新的已连接 Socket。原来的监听 Socket 还留在门口,继续接待后面的客户端。
面试回答:TCP 服务端依次执行
socket、bind、listen和accept,客户端执行socket和connect。accept会返回用于本次连接的新描述符,监听描述符继续负责接收新连接。
第六站:大家都能改共享数据,麻烦马上来了
两个线程同时执行 count++,结果未必加了两次。因为这行代码通常包含读取、加一、写回多个步骤。线程可能在任何一步被切走。
22. 什么是竞态条件和临界区?
程序结果依赖线程执行的先后顺序,并且这个顺序没有得到控制,就出现了竞态条件。访问共享资源的那段代码叫临界区。
解决思路不是“让线程跑快一点”,而是确保同一时刻只有合适数量的线程进入临界区,或者使用原子操作让更新不可分割。
面试回答:竞态条件是多个执行单元并发访问共享状态时,结果依赖不可预测的执行顺序。临界区是访问共享资源的代码区域,通常使用锁、信号量或原子操作保证正确性。
23. 互斥锁和信号量有什么区别?
互斥锁像只有一把钥匙的卫生间。谁加锁,通常就应该由谁解锁,它主要保护临界区。
信号量更像停车场入口的计数牌。初始值为 N,就允许 N 个任务同时占用资源;减到 0 后,后来者要等待。二值信号量虽然也能做互斥,但它还常用于线程间通知,不一定强调“加锁者负责解锁”的所有权。
面试回答:互斥锁主要用于临界区互斥,通常有所有权,持锁线程负责解锁;信号量是计数器,可以表示多个同类资源,也可以用于任务间同步。二值信号量能实现类似互斥的效果,但语义和所有权规则不同。
24. 互斥锁和自旋锁怎么选?
互斥锁抢不到时,线程通常进入睡眠,等锁可用后再被唤醒。睡眠和唤醒要经过调度,适合等待时间较长的情况。
自旋锁抢不到时不会睡,而是在 CPU 上循环尝试。临界区极短、预计马上能拿到锁时,自旋可能比睡眠再唤醒划算。如果持锁者迟迟不放,自旋线程就会白白烧 CPU。
单核机器上尤其要小心:等待者一直自旋,持锁者却需要同一颗 CPU 才能继续运行,这显然不合算。
面试回答:互斥锁竞争失败后通常阻塞线程,适合等待时间较长的场景;自旋锁会持续占用 CPU 重试,适合临界区很短、竞争较轻且多核的场景。选择依据是预期等待时间与线程睡眠、唤醒成本的比较。
25. 条件变量是做什么的?
互斥锁只能回答“现在能不能进门”,条件变量处理的是“我要等的事情发生了吗”。
以消费者为例。队列为空时,它不能抱着锁睡觉,否则生产者永远没机会放入数据。正确流程是:检查条件,不满足就调用 wait();wait() 会原子地释放互斥锁并让线程睡眠。收到通知后,线程重新抢锁,再检查一次条件。
为什么用 while 而不是 if?线程可能虚假唤醒,也可能醒来后发现数据已经被另一个消费者拿走。
lock(mutex);
while (queue_is_empty()) {
wait(condition, mutex);
}
take_item();
unlock(mutex);
面试回答:条件变量让线程在某个条件不满足时休眠,并与互斥锁配合使用。
wait会原子地释放锁并进入等待,被唤醒后重新加锁。醒来后要用while再次检查条件,以处理虚假唤醒和其他线程先一步修改状态的情况。
26. 什么是原子操作?CAS 又是什么?
原子操作不会暴露执行到一半的状态。其他线程看到的要么是操作前,要么是操作后。
CAS 会比较内存值与预期值,相等才写入新值,否则失败并由调用者决定是否重试。它是很多无锁数据结构的基础。竞争激烈时,大量线程反复重试同一个位置,也会消耗不少 CPU。
CAS 还有 ABA 问题:一个值从 A 变成 B,又变回 A。CAS 只看到它现在仍是 A,却不知道中间被改过。常见办法是在值旁边增加版本号。
面试回答:原子操作在并发观察下不可分割。CAS 比较当前值和预期值,相等时才更新,失败时通常重试。它避免了传统互斥锁的阻塞,但高竞争下会空转,还可能遇到 ABA 问题,可通过版本号或带标记的引用解决。
第七站:两个人各拿一把钥匙,谁也不肯先放
线程 A 拿着数据库锁,等文件锁;线程 B 拿着文件锁,等数据库锁。两边都很有耐心,于是程序永远停在那里。
27. 什么是死锁?
死锁是多个任务相互等待对方占有的资源,导致所有相关任务都无法继续。它不是“程序运行很慢”,而是一组等待关系形成了无法自行打破的闭环。
面试回答:死锁是多个进程或线程互相等待对方持有的资源,等待关系无法解除,导致相关任务永久阻塞。
28. 死锁产生的四个必要条件是什么?
四个条件要同时出现:
- 互斥:资源一次只能给一个任务使用。
- 请求并保持:任务持有已有资源,同时继续申请新资源。
- 不可剥夺:资源不能被系统强行收回,只能由持有者释放。
- 循环等待:任务之间形成首尾相接的等待环。
背的时候不要只背四个名词。想象两个人各拿一把钥匙,再等对方手里的钥匙:钥匙不能共享,手里的不肯放,别人不能抢,等待又刚好绕成一圈。
面试回答:死锁的四个必要条件是互斥、请求并保持、不可剥夺和循环等待。只要破坏其中任意一个条件,就能从机制上预防死锁。
29. 怎样预防、避免、检测和处理死锁?
预防是提前破坏必要条件。工程中最常见的办法是规定统一的加锁顺序,避免循环等待;也可以一次申请全部资源,或者获取失败时释放已有资源。
避免是每次分配前判断系统是否仍处于安全状态,银行家算法就是经典例子。检测与恢复则允许死锁发生,定期检查资源等待图,发现后终止任务或回收资源。业务代码里还常用超时锁,让线程等一阵后放弃并重试。
面试回答:死锁预防通过固定加锁顺序、减少嵌套锁或申请失败时释放资源来破坏必要条件;死锁避免会在分配前检查安全状态;也可以允许死锁发生后检测等待环,再终止任务或抢占资源。工程中常用固定锁顺序、缩小锁范围和超时机制。
30. 死锁、活锁和饥饿有什么区别?
死锁中的任务都停住了。活锁中的任务还在积极改变状态,例如两个人在走廊里不断同时向左、同时向右,谁都没走过去。饥饿则是某个任务一直抢不到资源,其他任务仍在正常推进。
面试回答:死锁是相互等待导致任务阻塞;活锁是任务没有阻塞,也在不断响应对方,但始终没有有效进展;饥饿是某个任务长期得不到调度或资源。随机退避可缓解活锁,公平调度和老化可减少饥饿。
第八站:程序以为自己住独栋,实际房间是操作系统安排的
每个进程看到的地址通常从一套规整的虚拟空间开始,好像整台机器的内存都归自己。背后真正的物理内存却是操作系统按页分配的,甚至有些数据暂时还在磁盘上。
31. 什么是虚拟内存?
虚拟内存给每个进程提供独立的虚拟地址空间,再由页表映射到物理内存。程序使用虚拟地址,不需要知道数据落在哪根内存条上。
这样做有几个直接好处:进程彼此隔离;程序得到相对连续的地址视图;操作系统能按需分配物理页;同一物理页也可以被多个进程共享。程序使用的虚拟地址范围可以很大,但这不表示机器凭空多出了同样多的物理内存。
面试回答:虚拟内存把进程使用的虚拟地址映射到物理地址,为每个进程提供独立的地址空间。它实现了内存隔离、按需分配和页面共享,也让程序不必直接管理零散的物理内存。
32. 虚拟地址怎样转换成物理地址?

分页系统会把虚拟地址分成“虚拟页号”和“页内偏移”。CPU 先用虚拟页号查询 TLB。命中后得到物理页框号,再和原来的页内偏移拼成物理地址。
TLB 未命中,硬件或内核需要查询页表。页表项有效,就把映射装进 TLB;页表项无效或页面不在内存,便触发缺页异常。
虚拟地址 = 虚拟页号 + 页内偏移
↓
TLB 命中?──是──→ 物理页框号
│否
↓
查页表
↓
物理地址 = 物理页框号 + 原页内偏移
页表本身可能很大,所以现代系统常使用多级页表:没用到的地址范围,不必为它提前建立完整的末级页表。
面试回答:CPU 用虚拟页号查询 TLB,命中后直接得到物理页框号;未命中时查询页表,成功后通常会缓存到 TLB。若页表中没有有效映射,则触发缺页异常。最终把物理页框号与页内偏移组合成物理地址。
33. TLB 是什么?为什么进程切换可能影响它?
页表放在内存里。如果每次访问数据前还要先访问几次页表,速度会很难看。TLB 是 CPU 内部缓存页表项的小型高速缓存,专门加速地址转换。
不同进程的同一个虚拟地址可以映射到不同物理页。切换地址空间时,CPU 必须避免误用上一个进程的映射。系统可能刷新部分 TLB,也可以借助 ASID、PCID 一类地址空间标识保留不同进程的条目。
面试回答:TLB 缓存近期使用的虚拟页到物理页映射,减少查询多级页表的内存访问。进程切换会更换地址空间,旧映射不能直接混用,因此可能需要失效相关条目;支持地址空间标识的处理器可以减少完全刷新的开销。
34. 分页和分段有什么区别?
分页按固定大小切分地址空间和物理内存,例如 4 KB 一页。程序员通常感觉不到页面边界,它方便操作系统分配和换入换出。
分段按照代码、数据、栈这类逻辑单元划分,每段长度可以不同,更符合程序结构,但容易留下外部碎片。现代通用操作系统的内存管理以分页为主;有些体系结构仍保留分段机制,但使用方式已经弱化。
面试回答:分页按固定大小管理内存,便于分配和页面置换,但可能产生内部碎片;分段按逻辑单元划分,长度不固定,便于保护和共享,却容易产生外部碎片。现代系统通常以分页为主要机制。
35. 内部碎片和外部碎片有什么区别?
分到手的房间里有一角永远用不上,是内部碎片。例如程序只需要 6 KB,却按 4 KB 页面拿到 8 KB,最后一页的剩余空间可能被浪费。
楼里空房总面积够,但散落在各层,凑不出一间连续的大房,是外部碎片。固定大小的分页避免了物理内存分配中的外部碎片,却仍可能产生页内浪费。
面试回答:内部碎片是已分配内存块内部没有被使用的空间;外部碎片是空闲总量足够,但分布零散,无法满足连续空间请求。分页主要产生内部碎片,变长分区和分段容易产生外部碎片。
36. 什么是缺页异常?它一定是程序出错吗?
不一定。
进程第一次访问尚未分配物理页的虚拟地址,或者访问的页面暂时不在内存,CPU 会触发缺页异常。内核检查这次访问是否合法:合法就建立映射,必要时从磁盘读入数据,然后重新执行刚才的指令;非法访问才会向进程报告错误,严重时终止进程。
如果数据已经在内存中,只需补充页表映射,常被称为次缺页;如果必须从磁盘读取,属于主缺页,延迟会高得多。
面试回答:缺页异常表示当前虚拟页没有可直接使用的页表映射,不一定是错误。合法访问可能触发按需分配、文件映射加载或换入,内核处理后重新执行指令;非法地址或权限错误才会导致进程收到异常信号。
37. 常见的页面置换算法有哪些?
物理内存满了,新页面要进来,系统就得挑一个旧页面出去。
- FIFO 淘汰最早进入内存的页面,实现简单,却可能淘汰仍在频繁使用的页,还可能出现分配更多页框、缺页次数反而增加的 Belady 异常。
- LRU 淘汰最长时间没有访问的页面,符合局部性原理,但精确维护访问顺序成本较高。
- LFU 淘汰访问次数少的页面,早期积累的高频历史可能让过时页面长期留下。
- Clock 使用访问位近似 LRU,在效果和实现成本之间折中。
- OPT 淘汰未来最久不会访问的页面,理论最优,但真实系统无法预知未来,主要用于比较。
面试回答:常见算法有 FIFO、LRU、LFU、Clock 和理论最优 OPT。FIFO 简单但可能出现 Belady 异常;LRU 利用时间局部性但精确实现较贵;Clock 通过访问位近似 LRU,因此在实际系统中更容易落地。
38. 什么是内存抖动?
一个程序正在处理的数据集合叫工作集。如果活跃进程的工作集总量超过物理内存,系统会不停把页面换出,又很快把它们换回来。磁盘忙得厉害,CPU 却总在等缺页处理,这就是内存抖动。
解决方法包括减少并发进程、给进程更多内存、改善访问局部性,或者限制某些任务的内存使用。
面试回答:内存抖动是工作集超过可用物理内存后,大量页面频繁换入换出,系统时间主要消耗在缺页和 I/O 上。可以通过降低并发度、增加内存或改善程序局部性缓解。
39. 什么是写时复制 COW?
fork() 后,父子进程的很多页面内容完全相同。立即复制一遍很浪费,尤其是子进程马上还要调用 exec()。
写时复制让父子进程先共享同一物理页,并把页表权限设为只读。某一方尝试写入时触发缺页异常,内核才复制该页,让双方分别修改。
面试回答:写时复制让多个地址空间先共享相同物理页,只有某一方写入时才触发异常并复制页面。它减少了
fork()等场景中的无效复制,但写入发生时仍会产生复制和缺页处理成本。
40. malloc() 返回地址后,物理内存已经分配了吗?
不一定。内存分配器可能先从自己已有的空闲块里切一块,也可能向内核扩展虚拟地址空间。由于按需分页,真正的物理页常在程序第一次读写,也就是“首次触碰”时才分配。
在常见实现中,小块分配可能来自进程堆,大块分配可能使用 mmap(),但具体阈值和策略属于分配器实现,不能当成所有系统都固定不变的规则。
面试回答:
malloc()主要负责获得一段可用的虚拟地址范围,不保证调用结束时每一页都已经对应物理内存。物理页通常在首次访问触发缺页时按需分配,具体使用堆还是mmap取决于内存分配器实现。
41. 栈和堆有什么区别?
线程栈保存函数调用帧、局部变量和返回地址,通常由编译器和运行时自动管理,分配快,但容量有限。每个线程有自己的栈。
堆用于动态分配,多个线程共享所属进程的堆。它更灵活,管理成本也更高,还会面对碎片、并发分配和对象生命周期问题。
别把“栈一定在物理内存、堆一定在磁盘”这类说法背进去。两者都处在进程的虚拟地址空间中,底层页面都由操作系统管理。
面试回答:栈主要保存函数调用现场和局部变量,由线程独享并自动管理;堆用于动态内存分配,在进程内共享,由分配器或运行时管理。栈分配通常更快但空间有限,堆更灵活,也有额外的管理和同步成本。
42. mmap() 有什么作用?
mmap() 可以把文件或匿名内存映射到进程的虚拟地址空间。映射文件后,程序像访问普通内存一样访问文件内容,内核按页加载,并通过页缓存管理数据。
多个进程映射同一个文件,可以共享对应物理页。它适合随机访问、大文件处理和进程间共享,但第一次访问仍可能缺页;映射也不意味着磁盘数据已经全部读进内存。
面试回答:
mmap()把文件或匿名对象映射进虚拟地址空间,访问时由缺页机制按需建立页面。它能减少显式read/write和用户缓冲区管理,也便于页面共享,但仍有缺页、同步和映射管理成本。
第九站:文件、磁盘和网卡,在程序眼里先变成一个数字
Linux 程序打开文件后拿到的常常只是 3、4、5 这样的整数。普通文件、管道和 Socket 都能用相似接口操作,这也是“一切皆文件”最实用的一面。
43. 什么是文件描述符?
文件描述符是进程文件描述符表中的索引。表项会关联到内核维护的打开文件对象,其中包含当前偏移、打开标志以及指向 inode 或 Socket 的信息。
默认情况下,0 是标准输入,1 是标准输出,2 是标准错误。文件描述符只在所属进程的上下文中有意义;两个进程都拿到数字 3,不代表它们一定打开了同一个对象。
面试回答:文件描述符是进程访问已打开文件、管道或 Socket 的整数句柄,本质上是进程文件描述符表的索引。表项进一步指向内核的打开文件对象和底层 inode 或设备对象。
44. inode 保存什么?为什么一般不保存文件名?
inode 保存文件类型、权限、所有者、大小、时间以及数据块位置等元数据。目录则保存“文件名到 inode 编号”的映射。
文件名属于目录关系,同一个 inode 可以有多个硬链接名称,所以名字放在目录项里更合理。打开路径时,内核逐级查目录,最终找到 inode。
面试回答:inode 保存文件元数据和数据块索引,通常不保存文件名。文件名记录在目录项中,目录项把名称映射到 inode,因此多个硬链接可以通过不同名字指向同一个 inode。
45. 硬链接和软链接有什么区别?
硬链接是给同一个 inode 再起一个名字。删除其中一个名字,只要链接计数还不为零,文件数据就仍然存在。硬链接通常不能跨文件系统,也通常不允许普通用户给目录创建硬链接。
软链接是一个独立文件,内容是目标路径。它有自己的 inode,可以跨文件系统,也能指向目录;目标路径失效后,软链接会变成“断链”。
面试回答:硬链接和原文件指向同一 inode,删除一个名称不影响其他硬链接;软链接有独立 inode,保存目标路径。硬链接通常不能跨文件系统,软链接可以,但目标删除或路径变化后会失效。
46. 文件被删除了,为什么已经打开它的进程还能继续读?
删除文件通常删除的是目录项,并减少 inode 的链接计数。进程已经打开文件时,内核中的打开文件对象仍然引用该 inode。
只有链接计数降为零,并且没有进程继续打开它时,文件数据块才会真正回收。这也解释了一个常见现象:日志文件删掉后,磁盘空间没有立刻回来,因为某个进程还握着旧文件描述符。
面试回答:删除文件主要是移除目录项并减少链接计数。已打开的文件仍被内核对象引用,所以进程可以继续访问;当链接计数为零且最后一个打开引用关闭后,数据空间才真正释放。
47. 页缓存、缓冲 I/O 和直接 I/O 是什么关系?
普通文件 I/O 通常经过页缓存。第一次读取可能访问磁盘,之后再次读取相同数据,内核可以直接从内存返回;写入也可能先进入页缓存,再由内核在合适时机回写磁盘。
直接 I/O 尽量绕过页缓存,让数据在用户缓冲区和设备之间传输。数据库常自己管理缓存,可能使用直接 I/O 避免双重缓存。不过它对缓冲区地址、长度和文件偏移经常有对齐要求,小而零散的访问未必更快。
面试回答:缓冲 I/O 通常使用内核页缓存,可以合并访问并提高重复读取速度;直接 I/O 尽量绕过页缓存,适合应用自己管理缓存的场景。直接 I/O 不是天然更快,它减少了一层缓存,也带来对齐限制和更直接的设备等待。
48. 什么是 DMA?
没有 DMA 时,如果设备每送来一点数据都要 CPU 亲自搬到内存,CPU 会累得顾不上计算。DMA 让设备控制器与内存直接传输一批数据。
CPU 负责设置传输地址和长度,随后可以去做别的事。传输完成后,设备通过中断通知 CPU。CPU 不是完全不参与,只是不再逐字节搬运。
面试回答:DMA 允许设备控制器直接在设备和内存之间传输数据。CPU 负责初始化任务,完成后由设备中断通知,从而减少 CPU 参与数据搬运的开销。
49. 什么是零拷贝?
传统文件发送可能经历这样的路径:
磁盘 → 内核页缓存 → 用户缓冲区 → Socket 缓冲区 → 网卡
应用只是转发数据,却把数据搬进用户态又搬回内核。sendfile() 等机制可以让内核直接把文件页缓存的数据交给 Socket 发送,减少用户态与内核态之间的复制和切换。配合 DMA 后,CPU 参与的数据搬运还能进一步减少。
“零拷贝”是工程称呼,不保证整条硬件链路真的一次内存复制都没有。它说的是尽量取消不必要的 CPU 拷贝。
面试回答:零拷贝通过
sendfile、内存映射或缓冲区重用等方式,减少数据在用户态与内核态之间的复制和系统调用。它不一定完全没有硬件层面的数据传输,重点是减少 CPU 搬运和上下文切换。
50. 阻塞、非阻塞、同步和异步分别在说什么?
阻塞与非阻塞关注“调用暂时不能完成时,线程要不要在这里等”。阻塞调用会让线程睡眠;非阻塞调用通常立刻返回“现在还没准备好”。
同步与异步关注“谁负责等待并完成操作”。同步 I/O 需要调用方在某个时刻亲自完成等待和数据处理;异步 I/O 提交请求后可以先离开,内核或运行时在完成后通知调用方。
非阻塞不等于异步。线程循环调用非阻塞 read(),仍然是自己不停检查结果;事件完成后由系统通知,才更接近异步模型。
面试回答:阻塞与非阻塞描述调用未就绪时是否让当前线程等待;同步与异步描述 I/O 完成过程由调用方等待,还是提交后由系统完成并通知。非阻塞调用可以仍然是同步 I/O,两组概念不能直接画等号。
第十站:一名服务员怎样照看一万张桌子?
如果每个网络连接都安排一个线程,连接数上万时,线程栈和调度开销会先把系统压住。I/O 多路复用的想法是:让一个线程同时关注很多描述符,只处理已经就绪的那些。
51. 什么是 I/O 多路复用?
程序把多个文件描述符交给内核等待。某些描述符可读或可写时,内核返回就绪结果,程序再逐个处理。
它的价值不在于让一次 I/O 变快,而在于减少“一个连接配一个阻塞线程”的资源消耗,适合连接多、单个连接大部分时间在等待的场景。
面试回答:I/O 多路复用允许一个线程等待多个文件描述符的就绪事件。内核返回已经可读或可写的描述符,程序再执行实际 I/O。它适合大量连接、活跃连接比例较低的网络服务。
52. select、poll 和 epoll 有什么区别?

select 使用固定大小的位图表达描述符集合,数量受实现限制。每次调用都要把集合交给内核,返回后应用还要遍历全部描述符。
poll 使用数组结构,摆脱了位图带来的固定上限,但每次仍要传入整个监听集合,并线性检查就绪项。
epoll 把关注的描述符长期登记在内核中。就绪事件进入就绪队列,epoll_wait() 主要返回活跃项,不必每次从头扫描全部连接。因此在连接很多、活跃比例低时,它通常更合适。
面试回答:
select有描述符数量和位图操作限制,poll没有固定的小上限,但两者每次调用都要提交并扫描整个集合。epoll在内核中维护关注集合和就绪队列,等待时主要返回活跃描述符,更适合大量连接、少量活跃的场景。
53. epoll 的 LT 和 ET 模式有什么区别?
LT 是水平触发。只要接收缓冲区里还有数据,下次等待时还会继续通知,比较稳妥。
ET 是边缘触发。状态从“无数据”变成“有数据”时通知一次;如果这次没有读干净,后面可能等不到新的边缘。因此 ET 通常配合非阻塞描述符,收到通知后循环读,直到返回 EAGAIN。
面试回答:LT 只要描述符仍处于就绪状态就会重复通知;ET 主要在就绪状态发生变化时通知。ET 通常要求使用非阻塞 I/O,并在一次事件处理中读写到
EAGAIN,否则可能遗漏剩余数据的处理机会。
54. epoll 一定比 select 和 poll 快吗?
不一定。连接数量少,或者绝大多数连接每次都很活跃时,epoll 的注册、事件维护开销未必有明显优势。程序性能还受业务处理、锁竞争、数据复制和系统调用次数影响。
面试里说“epoll 的时间复杂度永远是 O(1)”也太草率。等待和返回仍然要处理实际发生的就绪事件,成本至少与本次返回的活跃事件数量有关。
面试回答:
epoll的优势主要出现在监听集合很大、活跃比例较低时。它避免每次线性扫描所有描述符,但仍要处理就绪事件,也有注册和维护成本;连接较少或大量连接同时活跃时,优势可能不明显。
第十一站:系统变慢了,别一上来就怪 CPU
面试最后常出现排障题。这里没有一个万能命令,面试官想听的是:你会先看现象,再提出假设,用数据逐步缩小范围。
55. CPU 使用率很高,可能是什么原因?
用户态 CPU 高,可能是死循环、计算任务、频繁 GC、序列化或加密开销。内核态 CPU 高,可能是系统调用过多、网络包处理、中断或锁竞争。大量自旋和频繁上下文切换也会把 CPU 吃掉,却没有产生多少业务结果。
排查时先定位进程,再定位线程,然后采集线程栈或性能火焰图。与此同时看用户态与内核态比例、上下文切换、中断和负载情况。先拿证据,别凭感觉重启。
面试回答:CPU 高可能来自计算密集任务、死循环、频繁 GC、自旋锁、系统调用、中断或上下文切换。排查时先定位高 CPU 进程和线程,再结合线程栈、性能采样、用户态与内核态占比以及上下文切换数据找到热点。
56. CPU 使用率不高,为什么系统负载仍然很高?
Linux 的负载平均值不只统计正在使用 CPU 的任务,还可能包含处于不可中断睡眠、通常是在等待磁盘或某些内核资源的任务。
所以大量线程堵在慢磁盘上时,CPU 可能挺闲,负载数字却很高。此时应该继续看 I/O 延迟、磁盘队列、不可中断任务和内存换页,而不是只盯着 CPU 百分比。
面试回答:负载表示正在运行和等待某些资源的任务数量,不等同于 CPU 使用率。大量任务等待磁盘 I/O 或处于不可中断睡眠时,可能出现 CPU 不高但负载很高,需要结合 I/O 延迟、队列长度和进程状态分析。
57. 内存占用不断上涨,怎样区分泄漏和缓存?
先看上涨的是进程私有内存、共享内存,还是操作系统页缓存。Linux 会用空闲内存做缓存,看见“空闲内存少”不等于发生泄漏;缓存通常能在有压力时回收。
如果某个进程的私有常驻内存持续增长,业务量下降后也不回落,再结合堆分析看到对象或分配块失去正常释放路径,才更像泄漏。大量换页、主缺页和回收压力则说明系统已经真正缺内存。
面试回答:先区分进程私有内存、共享内存和可回收页缓存。页缓存增长通常是正常利用空闲内存;进程私有常驻内存长期增长且无法随业务回落,才需要结合堆快照或分配分析判断泄漏。还要观察换页和内存回收压力。
58. 一个程序从源代码到真正运行,经历了什么?
以编译型程序为例,源代码先经过预处理、编译和汇编生成目标文件,再由链接器把目标文件和库组合成可执行文件。
运行时,加载器建立进程地址空间,把可执行文件和动态库映射进去,准备栈、堆和运行时信息。调度器让线程获得 CPU 后,CPU 从程序入口开始执行。程序读文件、申请更多内存或访问网络时,再通过系统调用进入内核。
源代码
↓ 预处理、编译、汇编
目标文件
↓ 链接
可执行文件
↓ 加载、映射、创建进程
CPU 执行
面试回答:源代码经过预处理、编译、汇编和链接形成可执行文件。运行时,操作系统创建进程和虚拟地址空间,加载器映射程序及动态库并准备运行环境,调度器分配 CPU,线程从入口地址开始执行。
面试时怎样把答案说得像自己真的懂
八股最怕两个极端:只背一句定义,追问就断;从盘古开天讲起,三分钟还没落到问题上。
比较稳的回答顺序是:
一句定义 → 工作过程 → 优缺点或适用场景 → 一个容易说错的边界
例如面试官问共享内存:
共享内存是把同一组物理页面映射到多个进程,所以进程可以直接读写,少了管道那样的数据复制,通常吞吐量较高。但它不负责同步,多个进程并发修改时,还要配合互斥锁或信号量。
这段话不到半分钟,定义、原理、优势和风险都有了。面试官想继续,就可以追问页表映射或进程同步;不想继续,也已经拿到了完整答案。
再比如问缺页异常,开头不要只说“页面不在内存”。可以这样答:
缺页异常表示当前访问没有可直接使用的页表映射,它不一定是错误。合法访问可能触发按需分配或从磁盘换入,内核处理后会重新执行原指令;地址非法或权限不对,才会让进程收到错误。
这里顺手挡住了一个常见误区:异常不等于程序一定写错。
最后复习:把 58 道题压缩成 12 句话
临近面试时,不必从头逐字背。先检查下面这 12 句话能不能顺着解释下去:
- 进程负责资源和隔离,线程负责执行和调度。
- 就绪是只差 CPU,阻塞是在等事件或资源。
- 系统调用会进入内核态,但不一定发生进程切换。
- 上下文切换既有保存现场的成本,也会影响缓存和 TLB。
- 共享内存减少复制,但同步要自己解决。
- 互斥锁保护临界区,信号量管理资源数量或执行顺序。
- 死锁需要互斥、请求并保持、不可剥夺、循环等待同时成立。
- 虚拟内存通过页表把虚拟页映射到物理页,TLB 负责加速。
- 缺页异常可能只是按需分配,不一定是错误。
- 文件描述符是进程表项的索引,inode 保存文件元数据。
- 零拷贝减少不必要的 CPU 复制,不代表数据凭空消失。
- epoll 适合连接多、活跃少的场景,不是任何情况下都最快。
如果某一句只能原样复述,不能举例,也解释不了“为什么”,就回到对应章节再看一遍。能把这 12 句展开,你已经把操作系统最常见的面试主线连起来了。

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)