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

同样是等 1 秒:sleep 占 CPU 0.000 秒,忙等占满一个核。睡眠的本质是【主动让出 CPU 并挂到等待队列】——这就是为什么所有等待都该用事件通知,而不是循环查时间。

📝 本节和前面几节的关系

4.17 区分了两种时间,这一节讲【等】的时候内核到底做了什么——为什么睡眠几乎不花钱,忙等却烧掉一个核。

一、sleep 到底做了什么

第一步,把一个定时器挂上:告诉内核【多少纳秒之后叫我】。 第二步,把自己挂到等待队列,状态改成【可中断睡眠(S)】。 第三步,让出 CPU——schedule(),调度器挑别人跑。 第四步,定时器到点,内核把它从等待队列挪到就绪队列。然后它排队等 CPU。 整个过程它一次也没占 CPU——这就是为什么 1 秒的 sleep 只花 0.000 秒 CPU 时间。

二、忙等为什么这么贵

忙等是【一直占着 CPU 问时间到了没】:while time() < end: pass。 它不让出 CPU,所以这一个核被它一个人占满。在单核机器上,别人全被拖慢。 实测:睡 1 秒占 CPU 0.000 秒;忙等 1 秒占 CPU 1.00 秒。墙上时间一样,代价差三个数量级。 所以【轮询】在很多场景里是性能杀手——尤其是在线程池、事件循环里写 while 等标志位。

三、该用什么代替忙等

一、条件变量/事件:让别人在条件满足时通知你。这是最正统的做法。 二、阻塞 IO:read 会一直等到有数据,期间不占 CPU。 三、select/poll/epoll:等【多个 fd 里任意一个有动静】。这是所有高性能服务器的基础。 四、定时器/延迟队列:要等一段时间就用定时器,不要循环查表。 四条合起来的原则:等的时候必须把 CPU 交出去。

四、睡眠也有代价

唤醒有延迟:定时器到点不等于立刻跑,要排队(4.9 讲的唤醒延迟)。 睡太短没意义:sleep(0.0001) 的实际精度远达不到——受时钟粒度与调度延迟限制,通常只能到毫秒级。 睡太久会误事:服务里 sleep(60) 做重试,意味着最坏要等一分钟。 所以正确的做法是【事件驱动 + 超时】:既不用忙等,也不会因为睡太久而错过。

五、动手 Demo:进程睡眠——睡着的时候不占 CPU

同样的【等 1 秒】,两种等法:sleep 一百次 与 忙等一圈。看占 CPU 那一列。

# ① 睡 1 秒
t0, c0 = time.perf_counter(), time.process_time()
for _ in range(100):
    time.sleep(0.01)
t1, c1 = time.perf_counter(), time.process_time()

# ② 忙等 1 秒
t2, c2 = time.perf_counter(), time.process_time()
end = time.perf_counter() + 1.0
while time.perf_counter() < end:
    pass
t3, c3 = time.perf_counter(), time.process_time()

实跑输出:

--------------------------------------------------------------------
进程睡眠——睡着的时候不占 CPU
--------------------------------------------------------------------
① 同样是【等 1 秒】,两种等法的代价

  等法             墙上时钟     占 CPU       等的时候在干嘛
  ------------------------------------------------------------
  sleep 100 次     1.11 秒      0.016 秒     睡着,不占 CPU
  忙等 1 秒        1.00 秒      0.94 秒      空转,占满一个核

      **两者墙上时钟一样,占的 CPU 差了三个数量级。**

  **sleep 的本质是【主动让出 CPU 并挂到等待队列】**:
  内核把它标成【可中断睡眠】,等定时器到点再叫醒。**这期间调度器根本不会挑它。**
  **忙等则是【一直占着 CPU 问时间到了没】**——这一个核白烧了。

  **这就是为什么所有【等待】都该用事件通知,而不是循环检查时间。**

② 睡眠进程在 ps 里长什么样

  状态     含义             典型场景
  -------------------------------------------
  S        可中断睡眠       等 IO、等信号
  D        不可中断睡眠     等硬件、NFS 卡住
  R        就绪或运行       在跑或者在排队

  **一份 `ps aux` 里绝大多数进程是 S**——它们没在跑,只是挂在那儿等着。
  **所以【进程数很多】不等于【机器很忙】。**

墙上时钟差不多(都约 1 秒),占的 CPU 差三个数量级。这就是【让出】与【占着】的区别。 sleep 的四个步骤:挂定时器 → 挂等待队列 → 让出 CPU → 到点回就绪队列。全程不占 CPU。 忙等在最坏情况下能把整个系统拖慢——单核机器上它占满就是别人没得跑。 所以看到 while 里查时间、查标志位的代码,基本就是性能 bug 的候选。

📋 在真 Linux 上验证:time sleep 1 与 time sh -c 'while :; do :; done'(后者按 Ctrl+C 停)——前者 CPU 近 0,后者吃满一个核。

30 秒实验

  1. time sleep 2——看 real 是 2 秒,user+sys 是 0.00 秒。
  2. sleep 100 & 然后 ps -o stat,cmd -p $!——看它是 S 状态。
  3. cat /proc/<sleep 的 pid>/wchan——它会告诉你这个进程在等什么内核函数。

常见误区

  1. 【sleep 会占 CPU】。它恰恰相反,它把 CPU 让出去了。占 CPU 的是忙等。
  2. 【sleep(0.001) 就是精确 1 毫秒】。受时钟粒度与调度延迟限制,实际精度通常只到毫秒级,还可能更差。
  3. 【重试就用 sleep 固定几秒】。更好的是【指数退避 + 抖动 + 超时】。固定 sleep 会让一批客户端同时重试,把服务再打崩一次。
自测三问(点开对答案) 1. sleep 的四个步骤是什么?为什么它不占 CPU? —— 挂定时器、挂等待队列、让出 CPU、到点回就绪队列。**中间一直挂在等待队列里,调度器不会挑它。** 2. 忙等为什么这么贵? —— 它不让出 CPU,**一直占着一个核问时间到了没**。单核机器上别人全被拖慢。 3. 等待的正确做法有哪几种? —— 条件变量/事件、阻塞 IO、`select`/`poll`/`epoll`、定时器。**共同点:等的时候把 CPU 交出去。**

相关笔记