审校基线:本页引用的内核符号、行号与源码链接以 Linux 7.3-rc4(torvalds/linux master,2026-09-26 实测)为基准;审校当日 master 的确切 commit 未记录,后续版本可能已有漂移。
先有鸡还是先有蛋?内核的作弊解是:0 号进程是静态定义的,不是由谁创建的。rest_init 的两行 kernel_thread 按代码顺序决定了 PID 1 和 2 的归属。
2026-09-23 · Booting Up, from Bootloader to the First Process

第 2 关 · 系统启动:从 Bootloader 到第一个进程

定位

M01 回答了"进程是什么、怎么创建"。这一关回答"第一个进程从哪来"——先有鸡还是先有蛋的问题。内核的作弊解是:0 号进程是静态定义的(init_task),不是由谁创建的。从它出发,rest_init() 的两行 kernel_thread 按代码顺序生出 PID 1(kernel_init)和 PID 2(kthreadd)。

这一关的三个看点:init 的两段式出生(内核线程 → execve("/init"),机制复用而非特殊通道)、kthreadd 作为"中转站"的存在理由(调用者可能不能睡眠)、以及 1 号进程"杀不死、退不得"的两条内核守线。M01 的僵尸/孤儿机制在这里第一次有了真正的用武之地。

核心机制深讲

2.1 0 号:静态定义的作弊解

内核启动的最后阶段,start_kernel() 跑完所有初始化(内存、调度器、中断……),调用 rest_init()。此时已经有一个"进程"在跑了——0 号进程 init_task,它是静态定义在内核镜像里的(init/init_task.c),不是由任何创建路径生出来的。

这就是先有鸡先有蛋的作弊解:调度器需要"当前进程"才能工作,而第一个进程没法被创建——那就直接静态定义一个。0 号进程随后变成每个 CPU 的 idle 进程(cpu_idle),调度器无事可做时就切回它。它从不"运行"用户代码,是调度的零点。

2.2 rest_init:两行代码决定 PID 1 和 2

rest_init() 的核心就两行:

```c
kernel_thread(kernel_init, NULL, CLONE_FS); /* PID 1 */
kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); /* PID 2 */
```

kernel_thread() 是内核态创建内核线程的调用(和用户态的 kthread_create 是两码事——后者是 kthreadd 的中转机制,见 2.4)。PID 的分配顺序就是代码顺序:先创建的拿 1,后创建的拿 2。PID 1 是 kernel_init,PID 2 是 kthreadd——"1 号"和"2 号"的身份不是约定的,是这两行的先后写死的。

注意 kernel_thread 直接创建,不经过 kthreadd 中转——因为此时 kthreadd 自己还没出生,中转站不存在。

2.3 init 的两段式出生:内核线程 → execve("/init")

kernel_init 出生时是内核线程。它的工作分两段:

1. 内核态准备:kernel_init_freeable()——挂载 rootfs、打开 /dev/console 作为 0/1/2 号 fd、准备用户态环境。
2. execve("/init"):把自己替换成用户态程序。M01 的"替换而非新建"在这里第一次实战:kernel_init 没有死,它变成了 /init——tgid 还是 1,但 mm 从 NULL 换成了用户地址空间。

两段式的设计理由是机制复用:内核不需要为"第一个用户进程"发明特殊通道——execve 的替换语义本来就能把"当前执行流"换成另一个程序。内核线程 → 用户进程的跃迁,只是 execve 的一次普通调用。特殊的是调用者(PID 1 的内核线程),不是机制。

2.4 kthreadd:为什么需要一个中转站

PID 2 的 kthreadd 是所有内核线程的父亲。但"创建内核线程"为什么需要中转?直接调 copy_process 不行吗?

因为调用者可能处于不能睡眠的上下文(中断上下文、持锁路径)。copy_process 要分配 task_struct、内核栈——可能睡眠。而 kthread_create() 的做法是:把"创建请求"挂到 kthreadd 的工作队列,让 kthreadd(一个永远可睡眠的普通内核线程上下文)代劳。调用者只管排队,不用自己进可睡眠路径。

这就是中转站的存在理由:把"不能睡眠的请求者"和"需要睡眠的创建动作"解耦。用户态没有这个问题(系统调用上下文总能睡眠),所以 clone() 不需要中转。

2.5 1 号杀不死、退不得:两条守线

杀不死:kill -9 1 无效。机制在 sig_task_ignored()(kernel/signal.c,已对照主线源码核实):

```c
if (is_global_init(t) && sig_kernel_only(sig))
return 1; /* 忽略 */
```

SIGKILL/SIGSTOP 是 sig_kernel_only 的信号——只能由内核发、用户态发了也被忽略。注意这不是"1 号屏蔽了信号",是内核在投递路径上直接判"全局 init + 内核专有信号 → 忽略"。精准打击,不留配置口。

退不得:/init 如果直接 return 0,内核 panic("Attempted to kill init!"),而不是关机。关机是另一条路:reboot(2) 系统调用(LINUX_REBOOT_CMD_POWER_OFF / RESTART),由内核直接断电/重启——init 的退出在语义上只可能是"意外死亡",内核的选择是 panic 而不是"顺便关机"。

容器是例外:容器里的 init 退出只停容器、不 panic——因为 do_exit 检查的是 is_global_init()(全局 init),容器 init 不是全局 init,走正常退出路径。这是 pid namespace 的语义,不是特例补丁。

2.6 孤儿移交:init 才是收养者

M01 留了个尾巴:父先死,孤儿由谁收养?答案是 init(PID 1),不是 kthreadd。

机制:forget_original_parent() → find_new_reaper()。查找顺序是三级 fallback:先找最近的 child_subreaper 祖先(显式标记的"代理收养者"),找不到就交给该 pid namespace 的 child_reaper——初始命名空间里就是 PID 1。

两个常见误解一次澄清:

前提审查:如果 init 被拿掉了,孤儿交给谁?——问题本身不成立。find_new_reaper() 的三级 fallback 终点就是 init 本人,没有合法的 fallback 返回;内核用两处 panic 守线:启动时 "No working init found"(没找到可用的 init 就别往下走了),运行时 "Attempted to kill init!"(1 号死了整个系统没有继续的意义)。"无 init"的假设下,reparenting 没有有效终点——出题时假设题须先自查前提。

为什么这样设计

为什么 0 号进程要静态定义,而不是也走创建路径?

因为创建路径本身需要"当前进程"存在——copy_process 要从 current 继承一堆东西。第一个进程没有"当前",只能凭空定义。静态 init_task 是打破循环依赖的最小作弊:定义一个,后面的一切(包括 rest_init 的两行 kernel_thread)就都能走正常路径了。

为什么 init 的出生要分两段(内核线程 → execve),而不是直接在用户态启动?

因为内核态的准备工作(挂载 rootfs、开 console fd)必须在有"进程上下文"的前提下做,而用户态程序又需要这些准备好才能跑。两段式让"准备"和"变身"各归其位——且变身用的 execve 是现成机制,零新代码。这是内核设计里反复出现的模式:用通用机制的组合代替专用通道。

为什么 kthreadd 的子线程退出不用 wait()?

因为 wait() 是"父取子退出值"的同步契约(M01 Q9),而内核线程的退出值没人关心。SIG_IGN 自动回收是更便宜的选择:连僵尸这个中间态都省了。机制选型的标准永远是"谁需要这个信息"——没人需要,就别建状态。

与已学模块的关联

自测

出题原则:10 题中记忆性题目不超过 3 道,其余为应用/推理/权衡类——场景预测、设计 trade-off 比较、从代码路径找问题、解释"为什么这样设计而不是那样"。难度递进,最后两道需要想一会儿。

Q1. 0 号进程是由谁创建的?为什么它必须是静态定义的?

不是由谁创建的——init_task 是静态定义在内核镜像里的。必须是静态的,因为创建路径(copy_process)本身需要"当前进程"(current)存在才能继承东西;第一个进程没有"当前",只能凭空定义。这是打破循环依赖的最小作弊:定义一个 0 号,后面的一切就都能走正常路径了。0 号随后变成每 CPU 的 idle 进程。

Q2. rest_init() 里两行 kernel_thread 的顺序决定了什么?kernel_thread 和 kthread_create 是同一个东西吗?

顺序决定了 PID 1 和 2 的归属:先创建的 kernel_init 拿 PID 1,后创建的 kthreadd 拿 PID 2——"1 号/2 号"的身份是这两行的先后写死的。kernel_thread 和 kthread_create 是两码事:前者是 rest_init 用的直接创建调用(此时 kthreadd 还没出生,没有中转站可用);后者是经 kthreadd 中转的创建机制(调用者可能不能睡眠)。名字像,路径完全不同。

Q3. init 的"两段式出生"是哪两段?第二段用了什么已有机制,说明了什么设计思想?

第一段:kernel_init 作为内核线程做准备(kernel_init_freeable:挂载 rootfs、开 /dev/console 做 0/1/2 fd);第二段:execve("/init") 把自己替换成用户态程序。第二段用的是 M01 的"替换"机制——说明内核用通用机制的组合代替专用通道:"第一个用户进程"不需要特殊出生通道,一次普通的 execve 就够了。特殊的是调用者(PID 1 的内核线程),不是机制。

Q4. kthreadd(PID 2)的职责是什么?为什么创建内核线程需要经它中转?

kthreadd 是所有内核线程的父亲、kthread_create() 的中转站。需要中转是因为调用者可能处于不能睡眠的上下文(中断、持锁路径),而 copy_process 要分配 task_struct 和内核栈、可能睡眠。kthread_create 把创建请求排给 kthreadd(永远可睡眠的上下文)代劳——把"不能睡眠的请求者"和"需要睡眠的创建动作"解耦。

Q5. 容器里的 init 和全局 init 退出时行为有什么不同?do_exit 检查的是什么?

全局 init 退出 → 内核 panic("Attempted to kill init!");容器 init 退出 → 只停容器,不 panic。因为 do_exit 检查的是 is_global_init()——容器 init 不是全局 init,走正常退出路径。这是 pid namespace 的语义:每个 namespace 有自己的 child_reaper,全局 init 死了整个系统无意义、panic,容器 init 死了只影响本容器。

Q6. kthreadd 的子线程退出后,kthreadd 会调用 wait() 回收它吗?实际机制是什么?

不会调 wait()。kthreadd 启动时调 ignore_signals() 把 SIGCHLD 设为 SIG_IGN——子线程退出时内核自动回收,连僵尸中间态都不产生。wait() 是"父取子退出值"的同步契约(M01 Q9),内核线程的退出值没人关心——机制选型的标准是"谁需要这个信息",没人需要就别建状态。

Q7. 孤儿进程(父先死)会被谁收养?kthreadd 会收养孤儿吗?

被 init(PID 1) 收养——机制是 forget_original_parent() → find_new_reaper(),三级 fallback 的终点就是 init。kthreadd 从不收养孤儿:它是"直接子线程"的父亲(kthread_create 中转),不是孤儿的收养者。"收养"是用户态进程树管理的概念,kthreadd 的孩子们是内核线程,不走这条路。

Q8. kill -9 1 为什么杀不死 init?点名具体的函数和判断条件。

内核在信号投递路径上直接拦截:sig_task_ignored()(kernel/signal.c),条件是 is_global_init(t) && sig_kernel_only(sig)——目标是全局 init 且信号是内核专有信号(SIGKILL/SIGSTOP 属于此类)→ 直接返回"忽略"。这不是"1 号屏蔽了信号",是投递路径上的精准硬判,不留配置口。

Q9. /init 直接 return 0 会发生什么?正常关机走的是哪条路径?

/init 直接 return 0 → 内核 panic(不是关机)。init 的退出在语义上只可能是"意外死亡",内核的选择是 panic 而不是"顺便关机"。正常关机走 reboot(2) 系统调用(LINUX_REBOOT_CMD_POWER_OFF / RESTART)——由内核直接断电/重启,init 根本不需要"退出"来关机。

Q10. 设计题:假设有人把 init 拿掉了(PID 1 不存在),此时一个进程变成孤儿,find_new_reaper() 会发生什么?这个假设本身成立吗?

假设本身不成立。find_new_reaper() 的三级 fallback(最近的 child_subreaper 祖先 → 该 pid namespace 的 child_reaper)终点就是 init 本人——没有合法的 fallback 返回,reparenting 没有有效终点。内核用两处 panic 守线:启动时 "No working init found"(找不到可用的 init 就别往下走),运行时 "Attempted to kill init!"(1 号死了系统没有继续的意义)。出题时假设题须先自查前提——"无 init 时孤儿交给谁"是个伪问题,因为"无 init 的系统"在内核看来根本不该存在。

延伸阅读

Bootloader → 解压内核 → start_kernel()
  ├─ 0 号进程(idle):静态定义,不是被"创建"的
  └─ rest_init()
       ├─ kernel_thread(kernel_init, …) → 先创建 → PID 1
       └─ kernel_thread(kthreadd, …)    → 后创建 → PID 2
  └─ kernel_init():两段式出生
       ├─ kernel_init_freeable():先做完内核侧准备
       └─ execve("/init"):换上用户态第一个程序

跟着这条链走:静态的 0 号 → rest_init 两行 kernel_thread(顺序即 PID)→
kernel_init 两段式出生 → execve("/init")。记住作弊点:0 号不是被创建的,
PID 1/2 的身份是两行代码的先后写死的。

父进程死亡 → 子进程成孤儿 → find_new_reaper() 三级 fallback
  ├─ 同线程组其他线程
  ├─ …(逐级降级)
  └─ 终点:init(PID 1)——孤儿的终点永远是 init
kthreadd 从不收养孤儿(它只负责"生"内核线程)
"无 init"假设下问题本身不成立:两处 panic 是内核的守线

孤儿的终点是 init(PID 1),不是 kthreadd —— kthreadd 从不收养孤儿。
三级 fallback 的终点就是 init 本人;"无 init"假设下问题本身不成立,
两处 panic 是内核的守线。对应 Q7 和 Q10。

在页面里每题写下你的答案(关键词即可),卡住的题点一下"卡住了"标记,
点"生成作答汇总"后复制、贴到聊天里——对错由我来判定,我逐题批改并据此
调整下一关的深浅;准备好了说"下一关"。

作答汇总

点下面按钮生成汇总文本,复制后贴到「内核二十关」侧聊里,对错由我判定。