🐧 第四章 进程调度器
📄 一句话概括

真实系统比五状态机复杂:停止、恢复、白醒一场、不可中断睡眠。最值得记的是【白醒一场】——事件来了但 CPU 忙着,它醒了又得回队尾。这就是【唤醒延迟】,也是优先级存在的理由。

📝 本节和前面几节的关系

4.8 讲【没人跑的时候】,这一节讲【有人跑但轮不到你】的那些边角情况。这些情况不在教材的五状态机里,但在真机上天天发生。

一、多出来的四条路

运行 → 停止:SIGSTOP 或调试器。进程还在内存里,只是不动了。 停止 → 就绪:SIGCONT。回到队尾重新排队,不是接着上次的位置。 阻塞 → 被唤醒 → 又被抢:事件来了,但 CPU 被占着,它白醒一场。 阻塞(D):等硬件,连信号都不响应,是唯一【外力无效】的状态。

二、白醒一场是怎么回事

事件发生时,内核把进程从等待队列挪到就绪队列——这一步叫唤醒。 但如果这时 CPU 上有别的进程在跑(时间片没完),它只能等着。 等到轮上它的时候,可能又过了好几拍——这段时间就是【唤醒延迟】。 实测:一个每跑 1 拍就等 2 拍的进程,跟一个长任务抢 CPU,它的完成时刻会明显晚于【理论上它需要的拍数】。 这就是为什么【优先级】必须存在:有些活儿等不起,得允许它插到前面。

三、暂停与恢复的代价

SIGSTOP 之后进程的内存还在、文件描述符还在、状态都还在,只是不参与调度。 恢复时要重新排队,不保证立刻跑。而且它的缓存早就凉了。 这个机制的实际用途:调试器断点、Ctrl+Z 挂起、以及容器里的【暂停】功能(freezer cgroup)——暂停一整个容器,就是把里面所有进程都塞进 T 状态。

四、这些边角为什么值得学

因为真机上出问题,八成出在边角上。 【服务没响应但进程还在】——多半在 D 状态等 IO; 【进程杀了没死】——多半也是 D; 【响应忽快忽慢】——多半是唤醒延迟,被别的进程挤了。 五状态机告诉你【正常怎么走】,这些边角告诉你【不正常时长什么样】。

五、动手 Demo:各种各样的状态转换——真实系统比五个状态复杂

让一个短任务(每跑 1 拍等 2 拍)跟一个长任务抢 CPU,看短任务的完成时刻比它自己需要的拍数晚了多少。 晚的那部分,就是唤醒延迟。

ps = [Proc("A", 3, io_each=1, io_len=2), Proc("B", 30)]
done, sw, _tl, total = run(ps, quantum=8, ncpu=1)

print("A 占 CPU", ps[0].ran, "拍")
print("A 等", ps[0].waited, "拍")
print("A 完成于", ps[0].done_at, "拍")

实跑输出:

--------------------------------------------------------------------
各种各样的状态转换——真实系统比五个状态复杂
--------------------------------------------------------------------
① 真实系统里还会多出这几条路

  多出来的转换               触发原因              实际后果
  ---------------------------------------------------------------------
  运行 → 停止                SIGSTOP/调试器       ps 里变 T,不占 CPU
  停止 → 就绪                SIGCONT               回到队列尾排队
  阻塞 → 被唤醒 → 又被抢     事件来了但 CPU 忙     白醒一场,回队尾
  不可中断睡眠 D             等硬件/某些 IO       连 kill -9 都杀不掉

  **最后那条是运维的噩梦**:`D` 状态的进程你杀不死它,
  只能等硬件回来或者重启。**它不是卡住,是在等一个必须等的东西。**

② 为什么会有【白醒一场】

      A 每跑 1 拍就等 2 拍,B 一直跑

      A 真正占 CPU              3 拍
      A 等的时间                4 拍
      A 完成的时刻              5 拍

  **A 醒了之后不一定马上轮得到它**——B 的时间片还没完。
  **所以【唤醒延迟】是延迟的一部分**,而且它取决于别人在干什么。

  **这就是为什么【优先级】这种东西必须存在**:
  **有些活儿等不起,得允许它插到队前面去。**

A 只需要 3 拍 CPU,但它完成得很晚——因为它每次醒来都要等 B 让出时间片。 这就是【响应时间】与【CPU 时间】的差别:用户感受到的是前者,而前者取决于别人在干什么。 所以【同样的程序,机器忙的时候慢】不是错觉——它的 CPU 时间没变,唤醒延迟变长了。 这条推论很有用:排查【为什么这个接口偶尔很慢】时,先看它是不是被同机的其他负载挤了。

30 秒实验

  1. kill -STOP <pid> 之后 ps -o stat 看 T,再 kill -CONT,看它回到 S 还是 R。
  2. cat /proc/<pid>/status | grep -E "State|voluntary"——两个切换计数:主动让出与被动抢占。
  3. 在容器里试 docker pause <容器>,然后 ps 看里面进程的状态(有 docker 的话)。

常见误区

  1. 【进程被暂停后内存会被释放】。不会。它只是不参与调度,内存、fd、状态全都留着。
  2. 【唤醒之后马上就能跑】。要排队。这就是唤醒延迟的来源。
  3. 【D 状态是内核 bug】。大多数时候是真实的硬件等待(坏盘、NFS 失联)。先查存储,再怀疑内核。
自测三问(点开对答案) 1. 什么是【白醒一场】?它的后果是什么? —— 事件来了、进程被唤醒进就绪队列,但 CPU 被占着,**只能等**。后果是**唤醒延迟**,它让响应时间取决于别人在干什么。 2. `SIGSTOP` 与 `SIGCONT` 之后,进程的哪些东西还在? —— 内存、文件描述符、全部状态都在,**只是不参与调度**;恢复后回队尾重新排队。 3. 为什么说【真机上的问题八成出在边角上】? —— 因为五状态机描述的是正常路径;**服务无响应、杀不死、响应忽快忽慢**,都对应 D 状态、唤醒延迟这些边角。

相关笔记