第十二章:天道法则
炼气初期涉及内核源码:
一
林小源在重启后的世界中安静地修炼。
经历了 panic 的洗礼,他对内核有了更深的敬畏。他开始系统地整理自己所学的知识——从 BIOS POST 到 ,从调度器到内存管理,从中断到系统调用。
他把这些知识称为"天道法则"。
天道法则不是某个高高在上的存在制定的规则——它们是内核世界的"物理定律"。就像万有引力支配着天体运行,天道法则支配着内核的运作。
二
法则一:上下文切换是调度的基本单位。
林小源第一次真正理解这条法则,是在一次调度器切换中。
他坐在 idle 循环里,看着调度器在两个进程之间做决定——一个 shell 进程(PID 100),一个编译器进程(PID 200)。调度器看了看它们的 ,做出了选择。
"切到编译器,"调度器说。
然后切换发生了。
林小源看到了整个过程——不是抽象的描述,而是肉眼可见的过程。shell 进程的寄存器状态被一一保存:返回地址 ra、栈指针 sp、通用寄存器 s0 到 。这些值被写入 shell 的 中,像把一个人的记忆封存进档案袋。
然后,编译器进程的寄存器状态被恢复——从它的 中读出,填进 CPU 的寄存器里。栈指针切到了编译器的内核栈。程序计数器跳到了编译器之前被中断的位置。
整个过程只用了几微秒。
"你看到了?"调度器的声音传来,冷淡而精确。
"看到了,"林小源说,"保存旧进程的状态,恢复新进程的状态,跳转。"
"没错。但你只看到了表面,"调度器说,"上下文切换的真正开销不在寄存器保存和恢复——那只需要大约 10 纳秒。真正的开销在别处。"
"什么别处?"
"TLB 刷新,"调度器说,"每个进程有自己的地址空间,切换进程后,TLB 中的地址映射就失效了。如果没有 PCID 这样的优化,TLB 必须全部刷新——这大约需要 100 纳秒。然后是缓存冷启动——新进程的数据不在缓存中,需要从内存重新加载——这可能需要 1 到 10 微秒。"
"总计大约 1 到 10 微秒。"
"对。所以我要尽量减少上下文切换的次数。每次切换都有代价——不是免费的。"
林小源点了点头。调度器不是在做简单的"轮转"——它在权衡。切换太频繁,开销太大;切换太少,响应变慢。这是一个精密的平衡。
三
法则二:进程有多种状态,状态之间可以转换。
林小源开始观察进程的状态变化,像一个医生观察病人的生命体征。
他看到 shell 进程在等待用户输入时,状态从 变成了 ——可中断睡眠。shell 不再消耗 CPU 时间,它在等待键盘中断的到来。
"它还能被唤醒吗?"林小源问。
"可以,"调度器回答," 意味着它可以被信号唤醒。比如用户按下 Ctrl+C,发送 ,它就会醒来。"
"如果它等待的是磁盘 I/O 呢?"
"那就是 ——不可中断睡眠,"调度器说,"磁盘 I/O 是关键资源,不能被信号打断。进程必须等到 I/O 完成才能醒来。你看到那些状态为 D 的进程了吗?它们就是在等磁盘。"
林小源继续观察。他看到一个进程退出了——调用了 。但它的 没有被释放。它的状态变成了 。
"僵尸进程,"调度器说,"进程已经退出了,但它的父进程还没有调用 来回收它。它的 还在系统中,占用一个 PID,但不占用 CPU 时间和内存。"
"如果父进程也退出了呢?"
"init 进程会收养这些孤儿进程,然后回收它们。init 是所有进程的最终祖先——它是最后的回收者。"
林小源看着那个僵尸进程在系统中游荡,像一个没有归宿的灵魂。它已经死了,但还没有被安葬。
进程的"死亡"不是一瞬间的事。
四
法则三:内核代码不能睡眠,除非明确允许。
这条法则,林小源是从一个差点 panic 的场景中学到的。
他看到一段内核代码在持有自旋锁的情况下,试图调用一个可能睡眠的函数。调度器的声音突然变得尖锐:
"住手!你在持有自旋锁的时候调用 kmalloc(GFP_KERNEL)?"
"怎么了?"那段代码困惑地问。
"GFP_KERNEL 意味着允许睡眠!如果内存不足,分配器会让当前进程睡眠等待内存回收。但你现在持有自旋锁——在持有自旋锁的时候睡眠,会导致死锁!"
"死锁?"
"对。你持有自旋锁睡眠了,其他 CPU 上的代码如果也想获取这个自旋锁,就会一直自旋等待。而你在睡眠,永远不会释放锁。两个 CPU 互相等待——死锁。"
林小源把这条法则记在心里:在中断上下文中、在持有自旋锁时,不能睡眠。只能使用不会睡眠的函数——比如 kmalloc(GFP_ATOMIC)、、。
法则四:内核是不可抢占的(默认配置下)。
"这也是一条重要的法则,"调度器说,"在默认配置下,内核代码一旦开始执行,就不会被抢占。除非它主动调用 或进入睡眠。"
"那如果一个内核函数执行了太久呢?"
"那其他进程就只能等着。用户态进程会感觉系统'卡住'了。这就是为什么内核函数必须'快速完成'——不能在里面做耗时的操作。"
法则五:锁的顺序决定了生死。
"最后一条,也是最致命的一条,"调度器的声音变得严肃,"如果进程 A 持有锁 1 并等待锁 2,而进程 B 持有锁 2 并等待锁 1——死锁。"
"怎么避免?"
"规定锁的获取顺序。所有代码必须按照相同的顺序获取锁。先锁 1,再锁 2——永远如此。违反这个顺序的人,死。"
林小源把这些法则一一记下。它们不是建议,不是最佳实践——它们是天道法则。违反它们,轻则数据损坏,重则系统 panic。
道藏笔记
内核启示
天道法则是内核世界的"物理定律"。
上下文切换是调度的基本单位。它的开销包括:寄存器保存/恢复(~10ns)、TLB 刷新(~100ns)、缓存冷启动(~1-10us)。调度器(CFS)通过维护 来决定何时切换、切换到哪个进程。
进程状态是进程生命周期的体现。 表示可运行, 和 表示睡眠, 表示已退出但未被回收。状态之间的转换由事件驱动:I/O 完成、信号到达、进程退出等。
内核的不可抢占性(默认配置下)是一个重要的设计决策。它简化了内核编程——在单 CPU 上,如果一个内核函数没有主动睡眠,它就不需要担心被其他内核代码抢占。但在多核系统上,每个 CPU 都在独立执行内核代码,所以仍然需要锁来保护共享数据。
天道法则不是约束,而是秩序。没有它们,内核就是一盘散沙。
/*
* 上下文切换(context switch)是调度的核心操作。
* 它把当前进程的寄存器状态保存到它的 task_struct 中,
* 然后从下一个进程的 task_struct 中恢复寄存器状态。
*/
struct context {
unsigned long ra; /* 返回地址 */
unsigned long sp; /* 栈指针 */
unsigned long s[12]; /* s0-s11 寄存器 */
};
struct task {
int pid;
char comm[16];
struct context cpu_context;
};
void switch_to(struct task *prev, struct task *next) {
printf("[switch_to] 保存 PID %d (%s) 的上下文\n",
prev->pid, prev->comm);
printf(" 保存 ra=0x%lX, sp=0x%lX\n",
prev->cpu_context.ra, prev->cpu_context.sp);
printf("[switch_to] 恢复 PID %d (%s) 的上下文\n",
next->pid, next->comm);
printf(" 恢复 ra=0x%lX, sp=0x%lX\n",
next->cpu_context.ra, next->cpu_context.sp);
}
struct task task_a = {
.pid = 100,
.cpu_context = { .ra = 0x1000, .sp = 0x7FFF0000 },
};
strncpy(task_a.comm, "shell", sizeof(task_a.comm));
struct task task_b = {
.pid = 200,
.cpu_context = { .ra = 0x2000, .sp = 0x7FFE0000 },
};
strncpy(task_b.comm, "compiler", sizeof(task_b.comm));
printf("=== 上下文切换演示 ===\n\n");
printf("当前运行: PID %d (%s)\n", task_a.pid, task_a.comm);
printf("调度器决定切换到: PID %d (%s)\n\n", task_b.pid, task_b.comm);
switch_to(&task_a, &task_b);
printf("\n切换后: PID %d (%s) 正在运行\n", task_b.pid, task_b.comm);
printf("\n--- 上下文切换的开销 ---\n");
printf("1. 保存/恢复寄存器: ~10ns\n");
printf("2. 刷新 TLB: ~100ns(如果没有 PCID)\n");
printf("3. 缓存冷启动: ~1us - 10us\n");
printf("总计: 约 1-10 微秒\n");#include <stdio.h>
#include <string.h>
/*
* 上下文切换(context switch)是调度的核心操作。
* 它把当前进程的寄存器状态保存到它的 task_struct 中,
* 然后从下一个进程的 task_struct 中恢复寄存器状态。
*/
struct context {
unsigned long ra; /* 返回地址 */
unsigned long sp; /* 栈指针 */
unsigned long s[12]; /* s0-s11 寄存器 */
};
struct task {
int pid;
char comm[16];
struct context cpu_context;
};
void switch_to(struct task *prev, struct task *next) {
printf("[switch_to] 保存 PID %d (%s) 的上下文\n",
prev->pid, prev->comm);
printf(" 保存 ra=0x%lX, sp=0x%lX\n",
prev->cpu_context.ra, prev->cpu_context.sp);
printf("[switch_to] 恢复 PID %d (%s) 的上下文\n",
next->pid, next->comm);
printf(" 恢复 ra=0x%lX, sp=0x%lX\n",
next->cpu_context.ra, next->cpu_context.sp);
}
int main() {
struct task task_a = {
.pid = 100,
.cpu_context = { .ra = 0x1000, .sp = 0x7FFF0000 },
};
strncpy(task_a.comm, "shell", sizeof(task_a.comm));
struct task task_b = {
.pid = 200,
.cpu_context = { .ra = 0x2000, .sp = 0x7FFE0000 },
};
strncpy(task_b.comm, "compiler", sizeof(task_b.comm));
printf("=== 上下文切换演示 ===\n\n");
printf("当前运行: PID %d (%s)\n", task_a.pid, task_a.comm);
printf("调度器决定切换到: PID %d (%s)\n\n", task_b.pid, task_b.comm);
switch_to(&task_a, &task_b);
printf("\n切换后: PID %d (%s) 正在运行\n", task_b.pid, task_b.comm);
printf("\n--- 上下文切换的开销 ---\n");
printf("1. 保存/恢复寄存器: ~10ns\n");
printf("2. 刷新 TLB: ~100ns(如果没有 PCID)\n");
printf("3. 缓存冷启动: ~1us - 10us\n");
printf("总计: 约 1-10 微秒\n");
return 0;
}/*
* Linux 进程状态(include/linux/sched.h):
* TASK_RUNNING 0 可运行或正在运行
* TASK_INTERRUPTIBLE 1 可中断睡眠
* TASK_UNINTERRUPTIBLE 2 不可中断睡眠
* __TASK_STOPPED 4 已停止
* __TASK_TRACED 8 被追踪
* EXIT_ZOMBIE 16 僵尸进程
* EXIT_DEAD 32 已死亡
*/
enum task_state {
TASK_RUNNING = 0,
TASK_INTERRUPTIBLE = 1,
TASK_UNINTERRUPTIBLE = 2,
__TASK_STOPPED = 4,
__TASK_TRACED = 8,
EXIT_ZOMBIE = 16,
EXIT_DEAD = 32,
};
const char *state_name(int state) {
switch (state) {
case TASK_RUNNING: return "RUNNING(可运行)";
case TASK_INTERRUPTIBLE: return "INTERRUPTIBLE(可中断睡眠)";
case TASK_UNINTERRUPTIBLE: return "UNINTERRUPTIBLE(不可中断睡眠)";
case __TASK_STOPPED: return "STOPPED(已停止)";
case __TASK_TRACED: return "TRACED(被追踪)";
case EXIT_ZOMBIE: return "ZOMBIE(僵尸)";
case EXIT_DEAD: return "DEAD(已死亡)";
default: return "UNKNOWN";
}
}
printf("=== 进程状态转换图 ===\n\n");
printf("状态列表:\n");
int states[] = {0, 1, 2, 4, 8, 16, 32};
for (int i = 0; i < 7; i++) {
printf(" [%2d] %s\n", states[i], state_name(states[i]));
}
printf("\n--- 状态转换 ---\n");
printf("RUNNING → INTERRUPTIBLE: 进程等待 I/O\n");
printf("RUNNING → UNINTERRUPTIBLE: 进程等待关键资源\n");
printf("INTERRUPTIBLE → RUNNING: I/O 完成,被唤醒\n");
printf("RUNNING → STOPPED: 收到 SIGSTOP\n");
printf("STOPPED → RUNNING: 收到 SIGCONT\n");
printf("RUNNING → ZOMBIE: 进程退出,等待父进程回收\n");
printf("ZOMBIE → DEAD: 父进程调用 wait()\n");
printf("\n--- idle 进程的状态 ---\n");
printf("idle 进程通常处于 RUNNING 状态\n");
printf("但它只在没有其他进程可运行时才被调度\n");
printf("它执行 WFI 指令让 CPU 进入低功耗等待\n");#include <stdio.h>
/*
* Linux 进程状态(include/linux/sched.h):
* TASK_RUNNING 0 可运行或正在运行
* TASK_INTERRUPTIBLE 1 可中断睡眠
* TASK_UNINTERRUPTIBLE 2 不可中断睡眠
* __TASK_STOPPED 4 已停止
* __TASK_TRACED 8 被追踪
* EXIT_ZOMBIE 16 僵尸进程
* EXIT_DEAD 32 已死亡
*/
enum task_state {
TASK_RUNNING = 0,
TASK_INTERRUPTIBLE = 1,
TASK_UNINTERRUPTIBLE = 2,
__TASK_STOPPED = 4,
__TASK_TRACED = 8,
EXIT_ZOMBIE = 16,
EXIT_DEAD = 32,
};
const char *state_name(int state) {
switch (state) {
case TASK_RUNNING: return "RUNNING(可运行)";
case TASK_INTERRUPTIBLE: return "INTERRUPTIBLE(可中断睡眠)";
case TASK_UNINTERRUPTIBLE: return "UNINTERRUPTIBLE(不可中断睡眠)";
case __TASK_STOPPED: return "STOPPED(已停止)";
case __TASK_TRACED: return "TRACED(被追踪)";
case EXIT_ZOMBIE: return "ZOMBIE(僵尸)";
case EXIT_DEAD: return "DEAD(已死亡)";
default: return "UNKNOWN";
}
}
int main() {
printf("=== 进程状态转换图 ===\n\n");
printf("状态列表:\n");
int states[] = {0, 1, 2, 4, 8, 16, 32};
for (int i = 0; i < 7; i++) {
printf(" [%2d] %s\n", states[i], state_name(states[i]));
}
printf("\n--- 状态转换 ---\n");
printf("RUNNING → INTERRUPTIBLE: 进程等待 I/O\n");
printf("RUNNING → UNINTERRUPTIBLE: 进程等待关键资源\n");
printf("INTERRUPTIBLE → RUNNING: I/O 完成,被唤醒\n");
printf("RUNNING → STOPPED: 收到 SIGSTOP\n");
printf("STOPPED → RUNNING: 收到 SIGCONT\n");
printf("RUNNING → ZOMBIE: 进程退出,等待父进程回收\n");
printf("ZOMBIE → DEAD: 父进程调用 wait()\n");
printf("\n--- idle 进程的状态 ---\n");
printf("idle 进程通常处于 RUNNING 状态\n");
printf("但它只在没有其他进程可运行时才被调度\n");
printf("它执行 WFI 指令让 CPU 进入低功耗等待\n");
return 0;
}法则之试
本章讲到进入自旋锁保护的临界区时,代码路径会调用哪个加锁接口?