Skip to content

第四十九章:睡眠与唤醒

结丹后期

涉及内核源码:

林小源在调度竞技场中,看到了一片安静的湖泊。

湖泊的水面上漂浮着无数个光球,每个光球都是一个正在睡眠的进程。它们静静地浮在水面上,随着波纹轻轻起伏。有的光球发出柔和的蓝光——那是 TASK_INTERRUPTIBLE,可中断睡眠;有的光球发出深沉的紫光——那是 TASK_UNINTERRUPTIBLE,不可中断睡眠。

"大多数时间,进程都在睡眠。"一个温柔的声音从湖边传来。

林小源转头看去,一个身穿白色长裙的女子坐在湖边的石头上,双手轻轻拨弄着湖水。她的长裙上绣着状态转换的图谱——从 TASK_RUNNING 到 TASK_INTERRUPTIBLE,再到 TASK_UNINTERRUPTIBLE。

"我是睡眠与唤醒,"她说,"进程生命周期中最安静的部分。"

林小源在她身边坐下:"前辈,进程为什么要睡眠?"

"因为等待,"她指了指湖面上的光球,"等待 I/O 完成、等待信号、等待锁、等待用户输入。CPU 是宝贵的资源,不能浪费在空转上。当进程不需要 CPU 时,它应该睡眠,把 CPU 让给其他需要的进程。"

她抬手从湖中捞起一个光球,光球在她掌心缓缓旋转:"睡眠的流程是这样的——进程检查条件是否满足,不满足就设置状态为 TASK_INTERRUPTIBLE,然后调用 schedule() 让出 CPU。schedule() 把它从运行队列中移除,它就进入了这片湖泊。"

林小源看着湖面上的蓝光和紫光,问道:"TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 有什么区别?"

白衣女子指着蓝光的光球:"TASK_INTERRUPTIBLE 是可中断睡眠。进程在等待事件时,可以被信号唤醒。比如一个进程在等待用户输入,如果用户按了 Ctrl+C,SIGINT 信号会把它唤醒。大多数睡眠都是这种状态——ps 中显示为 'S'。"

她又指向紫光的光球:"TASK_UNINTERRUPTIBLE 是不可中断睡眠。进程在等待关键的 I/O 操作——比如磁盘读写、页面调入——不能被信号唤醒。即使你发 SIGKILL,它也不会醒来。ps 中显示为 'D'。"

林小源吃了一惊:"连 kill -9 都杀不死?"

"杀不死,"白衣女子说,"因为它正在等待硬件操作完成。如果这时候强行杀死进程,可能导致数据不一致、文件系统损坏。所以内核设计了不可中断睡眠——宁可让用户等一等,也不能破坏数据完整性。"

林小源想起了以前遇到的一个情况:一个进程变成了 D 状态,kill 怎么都杀不掉。当时他以为是 bug,现在才明白,那是内核在保护数据。

"那进程怎么醒来呢?"

白衣女子微笑着抬起手,一道光芒从她掌心射出,落在一个蓝光光球上。光球瞬间变成金色——TASK_RUNNING。

"这就是 try_to_wake_up(),"她说,"它做三件事:设置进程状态为 TASK_RUNNING,把进程加入运行队列,如果目标 CPU 需要就触发调度。进程从湖泊中升起,重新回到竞技场上。"

林小源看着那个被唤醒的光球飞向远方,问道:"唤醒的时候,进程会被放到哪个 CPU 上?"

白衣女子站起身来,指着远方的竞技场:"try_to_wake_up() 会尝试把进程放到最合适的 CPU 上。什么是'最合适'?有三个考虑:第一,之前运行它的 CPU——因为它的数据可能还在那个 CPU 的缓存中;第二,空闲的 CPU——避免等待;第三,NUMA 节点的本地 CPU——避免远程内存访问。"

林小源看着远方的竞技场,看到了光球飞向了它之前运行的那个 CPU。那个 CPU 的缓存中还残留着它的数据,不需要重新加载。

"唤醒也需要选择,"林小源说,"不是随便放一个 CPU 就行。"

白衣女子点头:"选择错误的 CPU 会导致缓存失效、NUMA 远程访问,增加延迟。try_to_wake_up() 的逻辑很复杂,但目标很简单——让醒来的进程尽快开始执行。"

林小源看着湖泊中漂浮的光球,心中感慨。调度器不只是选择进程——它还要管理进程的状态转换。从运行到睡眠,从睡眠到运行,每一个转换都需要精心设计。


c
/*
 * 进程的状态:
 * TASK_RUNNING        — 可运行(在运行队列中或正在运行)
 * TASK_INTERRUPTIBLE  — 可中断睡眠(等待事件,可被信号唤醒)
 * TASK_UNINTERRUPTIBLE — 不可中断睡眠(等待事件,不可被信号唤醒)
 * __TASK_STOPPED      — 停止(收到 SIGSTOP)
 * EXIT_ZOMBIE         — 僵尸
 *
 * 睡眠的典型流程:
 *   1. 进程检查条件是否满足
 *   2. 条件不满足 → 设置状态为 TASK_INTERRUPTIBLE
 *   3. 调用 schedule() 让出 CPU
 *   4. 进程从运行队列中移除
 *   5. 等待事件发生
 *   6. 事件发生 → 唤醒进程
 *   7. 进程被放回运行队列
 *   8. 进程继续执行
 *
 * 唤醒的函数:try_to_wake_up()
 *   1. 设置进程状态为 TASK_RUNNING
 *   2. 把进程加入运行队列
 *   3. 如果目标 CPU 需要,触发调度
 */

#define TASK_RUNNING           0
#define TASK_INTERRUPTIBLE     1
#define TASK_UNINTERRUPTIBLE   2

struct task_struct {
    int pid;
    char comm[16];
    int state;
};

void sleep(struct task_struct *tsk) {
    tsk->state = TASK_INTERRUPTIBLE;
    printf("[PID %d] 进入睡眠状态\n", tsk->pid);
}

void wake_up(struct task_struct *tsk) {
    tsk->state = TASK_RUNNING;
    printf("[PID %d] 被唤醒\n", tsk->pid);
}

printf("=== 睡眠与唤醒 ===\n\n");

struct task_struct proc = { 100, "my_proc", TASK_RUNNING };

printf("初始状态: PID %d, state=%d (RUNNING)\n\n",
       proc.pid, proc.state);

/* 进程睡眠 */
sleep(&proc);
printf("当前状态: PID %d, state=%d (INTERRUPTIBLE)\n\n",
       proc.pid, proc.state);

/* 进程被唤醒 */
wake_up(&proc);
printf("当前状态: PID %d, state=%d (RUNNING)\n\n",
       proc.pid, proc.state);

printf("--- 睡眠状态的区别 ---\n");
printf("TASK_INTERRUPTIBLE:\n");
printf("  - 可以被信号唤醒\n");
printf("  - 等待 I/O、等待用户输入\n");
printf("  - ps 中显示为 'S'\n\n");
printf("TASK_UNINTERRUPTIBLE:\n");
printf("  - 不能被信号唤醒\n");
printf("  - 等待磁盘 I/O、等待锁\n");
printf("  - ps 中显示为 'D'\n");
printf("  - 不可杀死(kill -9 无效)\n\n");

printf("--- 唤醒的过程 ---\n");
printf("try_to_wake_up():\n");
printf("  1. 设置进程状态为 TASK_RUNNING\n");
printf("  2. 把进程加入运行队列\n");
printf("  3. 如果目标 CPU 需要,触发调度\n");
printf("  4. 进程被调度执行\n");

道藏笔记

内核启示

睡眠和唤醒是进程状态转换的核心机制。

进程的状态:

  • — 可运行(在运行队列中)
  • — 可中断睡眠
  • — 不可中断睡眠
  • — 停止(收到
  • — 僵尸

睡眠的流程:

  1. 设置状态为
  2. 调用 让出 CPU
  3. 进程从运行队列中移除
  4. 等待事件发生
  5. 事件发生 → 唤醒进程
  6. 进程被放回运行队列

唤醒的流程():

  1. 设置进程状态为
  2. 把进程加入运行队列
  3. 选择最合适的 CPU
  4. 如果需要,触发调度

睡眠是进程的"日常"——大部分时间都在等待。


破关试炼

睡眠之试

沉睡任务被事件唤醒后,需要重新变成哪一种可运行状态才能参与调度?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道