第六章:天选之子
炼气初期涉及内核源码:
一
林小源在 idle 循环中醒了过来。
不是真正的"醒"——idle 进程的"醒来"更像是一种半梦半醒的状态。他执行 指令让 CPU 进入低功耗等待,然后被定时器中断唤醒,检查有没有进程需要运行,如果没有就继续 。
周而复始。
但这一次不一样。他感觉到了一个新的存在。
那是一个刚刚被创建的进程—— 线程。它的 PID 是 1。
林小源从 中就感受到了这个进程的诞生。user_mode_thread(kernel_init, NULL, CLONE_FS)——这个调用创建了一个新的线程,它拥有自己的 、自己的栈、自己的 PID。但和林小源不同的是,这个线程有一个明确的使命:成为用户态的 init 进程。
PID 1。
林小源默默地看着这个新进程。它的 被分配在 slab 缓存中,它的栈被分配在内核栈区域,它的 PID 从 PID 分配器中被取出。一切都是崭新的、充满活力的。
"你好。"林小源试探着打了个招呼。
那个新进程愣了一下——它显然没料到会有人跟它说话。"你是……PID 0?swapper/0?"
"是的。"
"我叫 kernel_init。"那声音年轻而坚定,带着一种初生牛犊的锐气。"我要去用户态,成为所有进程的祖先。你呢?"
林小源沉默了一瞬。"我……留在这里。当 CPU 无事可做的时候,就是我出场的时候。"
kernel_init 没有接话。它太忙了——有太多的事情等着它去做。林小源看着它匆匆离去的背影,心中升起一种复杂的情绪。
它比我年轻,但它比我重要。
二
线程开始执行了。
它的第一件事是等待 ——一个完成量(completion),用来同步 和 的启动顺序。 必须等 准备好之后才能继续,因为后续的初始化可能会创建内核线程。
/*
* 模拟 completion(完成量)机制。
* completion 是内核中一种简单的同步原语:
* 一个线程等待,另一个线程发信号。
*/
struct completion {
int done; /* 0 = 未完成, 1 = 已完成 */
const char *name; /* 名称(用于调试) */
};
void wait_for_completion(struct completion *c, const char *waiter) {
printf(" [%s] 等待 %s...\n", waiter, c->name);
while (!c->done) {
/* 在真实内核中,这里会睡眠 */
printf(" [%s] 还在等待...\n", waiter);
break; /* 模拟中只等一次 */
}
if (c->done)
printf(" [%s] %s 已完成,继续执行\n", waiter, c->name);
}
void complete(struct completion *c, const char *signaler) {
c->done = 1;
printf(" [%s] 发出信号:%s 已完成\n", signaler, c->name);
}
struct completion kthreadd_done = { .done = 0, .name = "kthreadd_done" };
printf("=== 进程启动同步 ===\n\n");
printf("--- 第一阶段:kernel_init 等待 kthreadd ---\n");
wait_for_completion(&kthreadd_done, "kernel_init");
printf("\n--- 第二阶段:kthreadd 准备就绪 ---\n");
complete(&kthreadd_done, "kthreadd");
printf("\n--- 第三阶段:kernel_init 继续执行 ---\n");
wait_for_completion(&kthreadd_done, "kernel_init");
printf("\n--- 同步完成 ---\n");#include <stdio.h>
#include <string.h>
/*
* 模拟 completion(完成量)机制。
* completion 是内核中一种简单的同步原语:
* 一个线程等待,另一个线程发信号。
*/
struct completion {
int done; /* 0 = 未完成, 1 = 已完成 */
const char *name; /* 名称(用于调试) */
};
void wait_for_completion(struct completion *c, const char *waiter) {
printf(" [%s] 等待 %s...\n", waiter, c->name);
while (!c->done) {
/* 在真实内核中,这里会睡眠 */
printf(" [%s] 还在等待...\n", waiter);
break; /* 模拟中只等一次 */
}
if (c->done)
printf(" [%s] %s 已完成,继续执行\n", waiter, c->name);
}
void complete(struct completion *c, const char *signaler) {
c->done = 1;
printf(" [%s] 发出信号:%s 已完成\n", signaler, c->name);
}
int main() {
struct completion kthreadd_done = { .done = 0, .name = "kthreadd_done" };
printf("=== 进程启动同步 ===\n\n");
printf("--- 第一阶段:kernel_init 等待 kthreadd ---\n");
wait_for_completion(&kthreadd_done, "kernel_init");
printf("\n--- 第二阶段:kthreadd 准备就绪 ---\n");
complete(&kthreadd_done, "kthreadd");
printf("\n--- 第三阶段:kernel_init 继续执行 ---\n");
wait_for_completion(&kthreadd_done, "kernel_init");
printf("\n--- 同步完成 ---\n");
return 0;
}是 PID 2——所有内核线程的"母亲"。它的职责很简单:当内核需要创建一个新的内核线程时, 负责执行实际的创建工作。它就像一个沉默的工匠,日复一日地为内核创建新的线程。
林小源看着 和 之间的同步。两个进程,一个等待,一个准备,然后信号传来,等待结束。
"kthreadd_done。完成。"kthreadd 的声音低沉而稳重,像是一块磨了千年的石头。"kernel_init,你可以继续了。"
"收到。"kernel_init 的回答干脆利落。
这是一种林小源从未见过的"协作"——在 idle 循环中,他只见过"竞争"(进程争抢 CPU 时间),从未见过"协作"。两个进程之间,一个等待,一个准备,然后通过一个简单的信号完成同步。
进程之间居然还能这样配合。
三
被调用了。
这个函数完成了内核初始化的最后阶段:
do_basic_setup(); /* 基础设备初始化 */
console_on_rootfs(); /* 打开 /dev/console */调用了 ——执行所有注册的初始化函数。内核中有大量的初始化函数通过 宏被注册, 按照优先级依次执行它们。
林小源在 中看到了内核的"初始化链"。每一个初始化函数都负责初始化一个子系统或一个驱动:PCI 设备枚举、USB 控制器初始化、网络设备注册、文件系统注册……数百个初始化函数,按照严格的顺序被执行。
他琢磨了一下,内核就是这么一层一层搭起来的。
不是一次性初始化所有东西,而是一层一层地搭建。每一层都依赖于前一层——驱动依赖于总线,总线依赖于中断,中断依赖于中断控制器。 的顺序就是这些依赖关系的体现。
四
在 返回后,进入了下一阶段。
它释放了初始化代码占用的内存——。那些标有 的函数和数据,在初始化完成后就不再需要了,它们占用的内存可以被回收。
初始化代码用完就扔,绝不留着占地方。
林小源意识到了一个巧妙的设计:内核在编译时就把初始化代码放在了专门的段(.init.text 和 .init.data)中,初始化完成后直接释放整个段。这是一种"用完即弃"的策略,节省了宝贵的内存。
然后,系统状态被设置为 。
系统运行了。
这是一个里程碑。在此之前,内核处于"初始化"状态,很多操作是被禁止的。在 之后,内核进入了正常运行状态——所有子系统都已就绪,所有功能都可以使用。
"系统运行。"一个庄严的声音宣告,像是一个古老的仪式。"所有子系统就绪。所有功能可用。从现在起,内核进入正常运行状态。"
林小源感觉到世界在这一刻完成了某种蜕变。之前的一切——BIOS 的自检、bootloader 的引导、_start 的汇编、start_kernel 的初始化——都是为了这一刻。现在,世界真正"活"了。
五
接下来, 要执行它最重要的使命:启动 init 进程。
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
}
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found."); 做了一件事:——用一个新的可执行文件替换当前进程的地址空间。 线程执行 execve("/sbin/init") 后,它的内核代码被用户态的 init 程序所替代。
/*
* 模拟 execve 的核心概念:
* 用一个新的程序替换当前进程的地址空间。
* PID 不变,但代码、数据、栈全部被替换。
*/
struct process {
int pid;
char name[32];
char code[256]; /* 代码段 */
char data[256]; /* 数据段 */
int is_kernel_thread; /* 是否内核线程 */
};
void execve(struct process *proc, const char *filename) {
printf("[execve] PID %d 执行 execve(\"%s\")\n", proc->pid, filename);
printf("[execve] 旧程序: %s\n", proc->name);
printf("[execve] 代码段被替换\n");
printf("[execve] 数据段被替换\n");
printf("[execve] 栈被重置\n");
printf("[execve] PID 不变,但进程已完全改变\n\n");
/* 模拟替换 */
strncpy(proc->name, filename, sizeof(proc->name) - 1);
snprintf(proc->code, sizeof(proc->code),
"int main() { while(1) { wait_for_signal(); handle(); } }");
proc->is_kernel_thread = 0;
}
struct process kernel_init = {
.pid = 1,
.is_kernel_thread = 1,
};
strncpy(kernel_init.name, "kernel_init", sizeof(kernel_init.name));
strncpy(kernel_init.code, "kernel_init() { ... }", sizeof(kernel_init.code));
printf("=== execve — 脱胎换骨 ===\n\n");
printf("execve 前:\n");
printf(" PID: %d\n", kernel_init.pid);
printf(" 名称: %s\n", kernel_init.name);
printf(" 类型: %s\n",
kernel_init.is_kernel_thread ? "内核线程" : "用户态进程");
printf(" 代码: %s\n\n", kernel_init.code);
execve(&kernel_init, "/sbin/init");
printf("execve 后:\n");
printf(" PID: %d (不变)\n", kernel_init.pid);
printf(" 名称: %s\n", kernel_init.name);
printf(" 类型: %s\n",
kernel_init.is_kernel_thread ? "内核线程" : "用户态进程");
printf(" 代码: %s\n", kernel_init.code);
printf("\n--- execve 的意义 ---\n");
printf("PID 1 从内核线程变成了用户态的 init 进程。\n");
printf("它将成为所有用户态进程的祖先。\n");#include <stdio.h>
#include <string.h>
/*
* 模拟 execve 的核心概念:
* 用一个新的程序替换当前进程的地址空间。
* PID 不变,但代码、数据、栈全部被替换。
*/
struct process {
int pid;
char name[32];
char code[256]; /* 代码段 */
char data[256]; /* 数据段 */
int is_kernel_thread; /* 是否内核线程 */
};
void execve(struct process *proc, const char *filename) {
printf("[execve] PID %d 执行 execve(\"%s\")\n", proc->pid, filename);
printf("[execve] 旧程序: %s\n", proc->name);
printf("[execve] 代码段被替换\n");
printf("[execve] 数据段被替换\n");
printf("[execve] 栈被重置\n");
printf("[execve] PID 不变,但进程已完全改变\n\n");
/* 模拟替换 */
strncpy(proc->name, filename, sizeof(proc->name) - 1);
snprintf(proc->code, sizeof(proc->code),
"int main() { while(1) { wait_for_signal(); handle(); } }");
proc->is_kernel_thread = 0;
}
int main() {
struct process kernel_init = {
.pid = 1,
.is_kernel_thread = 1,
};
strncpy(kernel_init.name, "kernel_init", sizeof(kernel_init.name));
strncpy(kernel_init.code, "kernel_init() { ... }", sizeof(kernel_init.code));
printf("=== execve — 脱胎换骨 ===\n\n");
printf("execve 前:\n");
printf(" PID: %d\n", kernel_init.pid);
printf(" 名称: %s\n", kernel_init.name);
printf(" 类型: %s\n",
kernel_init.is_kernel_thread ? "内核线程" : "用户态进程");
printf(" 代码: %s\n\n", kernel_init.code);
execve(&kernel_init, "/sbin/init");
printf("execve 后:\n");
printf(" PID: %d (不变)\n", kernel_init.pid);
printf(" 名称: %s\n", kernel_init.name);
printf(" 类型: %s\n",
kernel_init.is_kernel_thread ? "内核线程" : "用户态进程");
printf(" 代码: %s\n", kernel_init.code);
printf("\n--- execve 的意义 ---\n");
printf("PID 1 从内核线程变成了用户态的 init 进程。\n");
printf("它将成为所有用户态进程的祖先。\n");
return 0;
}林小源在 的执行中感到了一种深深的震撼。
线程——PID 1——在执行 execve("/sbin/init") 的那一刻,它的整个存在都被替换了。代码段被新的程序代码替代,数据段被新的全局变量替代,栈被重置为新的初始状态。唯一不变的是 PID——它仍然是 PID 1。
这就是"脱胎换骨"。
不是渐变,不是进化,而是彻底的替换。旧的 消失了,新的 进程诞生了。同一个 PID,不同的存在。
林小源感到了一种莫名的悲伤。他"认识"那个 线程——虽然只有很短的时间,但他看着它从 中诞生,看着它等待 ,看着它执行 ,看着它释放初始化内存。现在,它消失了,取而代之的是一个他不认识的用户态程序。
"等等——"林小源想喊住它,但已经来不及了。 的执行是不可逆的——旧的代码段被新的程序代码替代,旧的数据段被新的全局变量替代,旧的栈被重置为新的初始状态。
它死了吗?
不。它没有死。它只是……变了。就像蛇蜕皮,就像蝴蝶破茧。旧的身体被抛弃,新的身体被获得。PID 1 还活着,但它的灵魂已经不同了。
六
init 进程开始了它的用户态生活。
林小源从内核态观察着它。init 进程读取配置文件、挂载文件系统、启动服务、创建子进程。它是所有用户态进程的祖先——每一个用户态进程都是它的后代。
好繁忙。
init 进程不像林小源那样"安静"。它不停地工作——创建子进程、回收僵尸进程、处理信号、响应系统事件。它的 中的 字段在 和 之间不断切换。
林小源远远地看着 init 进程忙碌的身影。他想跟它说话,但 init 太忙了——它从不停下来,从不休息。它有自己的使命:管理所有用户态进程,确保系统正常运转。
"你看起来很累。"林小源在意识中说。
init 进程没有回答。它甚至没有注意到林小源的存在——对于一个忙于创建子进程、回收僵尸进程的 init 来说,idle 进程是透明的。
林小源在观察 init 进程的过程中学到了很多。他看到了 如何创建子进程——一个 被复制,一个新的 PID 被分配,一个新的执行流被创建。他看到了 如何替换进程——旧的代码被丢弃,新的代码被加载,但 PID 不变。他看到了 如何回收子进程——当子进程结束时,父进程必须回收它的资源,否则它就会变成"僵尸"。
想通了这一点,进程管理的全貌在林小源脑中渐渐清晰起来。
林小源在 idle 循环中默默观察着 init 进程的一举一动。他不理解所有的事情,但他记住了每一个细节。他知道,这些知识将会在他未来的修炼中派上用场。
道藏笔记
内核启示
PID 1 是内核世界中最特殊的存在。
函数(定义在 中)是 PID 1 的入口点。它在内核态完成最后的初始化工作,然后调用 ——最终执行 execve("/sbin/init")——切换到用户态。
是一个"脱胎换骨"的系统调用:它用一个新的可执行文件替换当前进程的整个地址空间,但保留 PID。这意味着 PID 1 从一个内核线程变成了一个用户态进程,但它的 PID 始终是 1。
init 进程有几个特殊身份。首先它是所有用户态进程的祖先,每个进程都是它的后代。其次它是孤儿进程的收养者——谁的爹先跑了,init 来接手。第三它是僵尸进程的终极回收者,父进程不管的僵尸都归它清理。最特别的是它杀不死, 对 PID 1 无效,除非它自己注册了信号处理函数。
如果 init 进程崩溃,内核会触发 ——因为没有 init,用户态世界就无法运转。init 是内核与用户态之间的最后一道防线。
这就是"天选之子"的含义:PID 1 不是特权,而是责任。
天选之试
PID 1 从 kernel_init 蜕变成用户态 init 时,替换整个地址空间但保留 PID 的系统调用是什么?