Skip to content

第六章:天选之子

炼气初期

涉及内核源码:

林小源在 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),用来同步 的启动顺序。 必须等 准备好之后才能继续,因为后续的初始化可能会创建内核线程。

c
/*
 * 模拟 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");

是 PID 2——所有内核线程的"母亲"。它的职责很简单:当内核需要创建一个新的内核线程时, 负责执行实际的创建工作。它就像一个沉默的工匠,日复一日地为内核创建新的线程。

林小源看着 之间的同步。两个进程,一个等待,一个准备,然后信号传来,等待结束。

"kthreadd_done。完成。"kthreadd 的声音低沉而稳重,像是一块磨了千年的石头。"kernel_init,你可以继续了。"

"收到。"kernel_init 的回答干脆利落。

这是一种林小源从未见过的"协作"——在 idle 循环中,他只见过"竞争"(进程争抢 CPU 时间),从未见过"协作"。两个进程之间,一个等待,一个准备,然后通过一个简单的信号完成同步。

进程之间居然还能这样配合。

被调用了。

这个函数完成了内核初始化的最后阶段:

c
do_basic_setup();        /* 基础设备初始化 */
console_on_rootfs();     /* 打开 /dev/console */

调用了 ——执行所有注册的初始化函数。内核中有大量的初始化函数通过 宏被注册, 按照优先级依次执行它们。

林小源在 中看到了内核的"初始化链"。每一个初始化函数都负责初始化一个子系统或一个驱动:PCI 设备枚举、USB 控制器初始化、网络设备注册、文件系统注册……数百个初始化函数,按照严格的顺序被执行。

他琢磨了一下,内核就是这么一层一层搭起来的。

不是一次性初始化所有东西,而是一层一层地搭建。每一层都依赖于前一层——驱动依赖于总线,总线依赖于中断,中断依赖于中断控制器。 的顺序就是这些依赖关系的体现。

返回后,进入了下一阶段。

它释放了初始化代码占用的内存——。那些标有 的函数和数据,在初始化完成后就不再需要了,它们占用的内存可以被回收。

初始化代码用完就扔,绝不留着占地方。

林小源意识到了一个巧妙的设计:内核在编译时就把初始化代码放在了专门的段(.init.text.init.data)中,初始化完成后直接释放整个段。这是一种"用完即弃"的策略,节省了宝贵的内存。

然后,系统状态被设置为

系统运行了。

这是一个里程碑。在此之前,内核处于"初始化"状态,很多操作是被禁止的。在 之后,内核进入了正常运行状态——所有子系统都已就绪,所有功能都可以使用。

"系统运行。"一个庄严的声音宣告,像是一个古老的仪式。"所有子系统就绪。所有功能可用。从现在起,内核进入正常运行状态。"

林小源感觉到世界在这一刻完成了某种蜕变。之前的一切——BIOS 的自检、bootloader 的引导、_start 的汇编、start_kernel 的初始化——都是为了这一刻。现在,世界真正"活"了。

接下来, 要执行它最重要的使命:启动 init 进程。

c
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 程序所替代。

c
/*
 * 模拟 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");

林小源在 的执行中感到了一种深深的震撼。

线程——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 的系统调用是什么?

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

以修仙之名,悟内核之道