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 服务端依次执行 socketbindlistenaccept,客户端执行 socketconnectaccept 会返回用于本次连接的新描述符,监听描述符继续负责接收新连接。


第六站:大家都能改共享数据,麻烦马上来了

两个线程同时执行 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. 死锁产生的四个必要条件是什么?

四个条件要同时出现:

  1. 互斥:资源一次只能给一个任务使用。
  2. 请求并保持:任务持有已有资源,同时继续申请新资源。
  3. 不可剥夺:资源不能被系统强行收回,只能由持有者释放。
  4. 循环等待:任务之间形成首尾相接的等待环。

背的时候不要只背四个名词。想象两个人各拿一把钥匙,再等对方手里的钥匙:钥匙不能共享,手里的不肯放,别人不能抢,等待又刚好绕成一圈。

面试回答:死锁的四个必要条件是互斥、请求并保持、不可剥夺和循环等待。只要破坏其中任意一个条件,就能从机制上预防死锁。

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 程序打开文件后拿到的常常只是 345 这样的整数。普通文件、管道和 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. selectpollepoll 有什么区别?

在这里插入图片描述

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 句话能不能顺着解释下去:

  1. 进程负责资源和隔离,线程负责执行和调度。
  2. 就绪是只差 CPU,阻塞是在等事件或资源。
  3. 系统调用会进入内核态,但不一定发生进程切换。
  4. 上下文切换既有保存现场的成本,也会影响缓存和 TLB。
  5. 共享内存减少复制,但同步要自己解决。
  6. 互斥锁保护临界区,信号量管理资源数量或执行顺序。
  7. 死锁需要互斥、请求并保持、不可剥夺、循环等待同时成立。
  8. 虚拟内存通过页表把虚拟页映射到物理页,TLB 负责加速。
  9. 缺页异常可能只是按需分配,不一定是错误。
  10. 文件描述符是进程表项的索引,inode 保存文件元数据。
  11. 零拷贝减少不必要的 CPU 复制,不代表数据凭空消失。
  12. epoll 适合连接多、活跃少的场景,不是任何情况下都最快。

如果某一句只能原样复述,不能举例,也解释不了“为什么”,就回到对应章节再看一遍。能把这 12 句展开,你已经把操作系统最常见的面试主线连起来了。

在这里插入图片描述

Logo

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

更多推荐