第四十九章:睡眠与唤醒
结丹后期涉及内核源码:
一
林小源在调度竞技场中,看到了一片安静的湖泊。
湖泊的水面上漂浮着无数个光球,每个光球都是一个正在睡眠的进程。它们静静地浮在水面上,随着波纹轻轻起伏。有的光球发出柔和的蓝光——那是 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() 的逻辑很复杂,但目标很简单——让醒来的进程尽快开始执行。"
林小源看着湖泊中漂浮的光球,心中感慨。调度器不只是选择进程——它还要管理进程的状态转换。从运行到睡眠,从睡眠到运行,每一个转换都需要精心设计。
/*
* 进程的状态:
* 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");#include <stdio.h>
/*
* 进程的状态:
* 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);
}
int main() {
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");
return 0;
}道藏笔记
内核启示
睡眠和唤醒是进程状态转换的核心机制。
进程的状态:
- — 可运行(在运行队列中)
- — 可中断睡眠
- — 不可中断睡眠
- — 停止(收到 )
- — 僵尸
睡眠的流程:
- 设置状态为
- 调用 让出 CPU
- 进程从运行队列中移除
- 等待事件发生
- 事件发生 → 唤醒进程
- 进程被放回运行队列
唤醒的流程():
- 设置进程状态为
- 把进程加入运行队列
- 选择最合适的 CPU
- 如果需要,触发调度
睡眠是进程的"日常"——大部分时间都在等待。
睡眠之试
沉睡任务被事件唤醒后,需要重新变成哪一种可运行状态才能参与调度?