M01 回答了"进程是什么、怎么创建"。这一关回答"第一个进程从哪来"——先有鸡还是先有蛋的问题。内核的作弊解是:0 号进程是静态定义的(init_task),不是由谁创建的。从它出发,rest_init() 的两行 kernel_thread 按代码顺序生出 PID 1(kernel_init)和 PID 2(kthreadd)。
这一关的三个看点:init 的两段式出生(内核线程 → execve("/init"),机制复用而非特殊通道)、kthreadd 作为"中转站"的存在理由(调用者可能不能睡眠)、以及 1 号进程"杀不死、退不得"的两条内核守线。M01 的僵尸/孤儿机制在这里第一次有了真正的用武之地。
内核启动的最后阶段,start_kernel() 跑完所有初始化(内存、调度器、中断……),调用 rest_init()。此时已经有一个"进程"在跑了——0 号进程 init_task,它是静态定义在内核镜像里的(init/init_task.c),不是由任何创建路径生出来的。
这就是先有鸡先有蛋的作弊解:调度器需要"当前进程"才能工作,而第一个进程没法被创建——那就直接静态定义一个。0 号进程随后变成每个 CPU 的 idle 进程(cpu_idle),调度器无事可做时就切回它。它从不"运行"用户代码,是调度的零点。
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 自己还没出生,中转站不存在。
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 的内核线程),不是机制。
PID 2 的 kthreadd 是所有内核线程的父亲。但"创建内核线程"为什么需要中转?直接调 copy_process 不行吗?
因为调用者可能处于不能睡眠的上下文(中断上下文、持锁路径)。copy_process 要分配 task_struct、内核栈——可能睡眠。而 kthread_create() 的做法是:把"创建请求"挂到 kthreadd 的工作队列,让 kthreadd(一个永远可睡眠的普通内核线程上下文)代劳。调用者只管排队,不用自己进可睡眠路径。
这就是中转站的存在理由:把"不能睡眠的请求者"和"需要睡眠的创建动作"解耦。用户态没有这个问题(系统调用上下文总能睡眠),所以 clone() 不需要中转。
杀不死: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 的语义,不是特例补丁。
M01 留了个尾巴:父先死,孤儿由谁收养?答案是 init(PID 1),不是 kthreadd。
机制:forget_original_parent() → find_new_reaper()。查找顺序是三级 fallback:先找最近的 child_subreaper 祖先(显式标记的"代理收养者"),找不到就交给该 pid namespace 的 child_reaper——初始命名空间里就是 PID 1。
两个常见误解一次澄清:
kthreadd 从不收养孤儿。它是"直接子线程"的父亲(kthread_create 的中转),不是"孤儿"的收养者。"收养"是用户态进程树管理的概念,kthreadd 的孩子们都是内核线程,不走这条路。kthreadd 的子线程退出不靠 wait() 回收。kthreadd 启动时调了 ignore_signals() 把 SIGCHLD 设成 SIG_IGN——子线程退出时内核自动回收,不产生僵尸。M01 Q9 的"僵尸等父 wait"是用户态父子的契约,不可套用到 kthreadd 身上。前提审查:如果 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 自动回收是更便宜的选择:连僵尸这个中间态都省了。机制选型的标准永远是"谁需要这个信息"——没人需要,就别建状态。
task_struct——"内核里只有 task_struct"在这一关第一次看到全貌:idle 进程、init、kthreadd,无一例外。kernel_init → /init 是"替换而非新建"的实战——tgid 还是 1,mm 从 NULL 换成用户地址空间。execve("/init") 扔掉旧 mm、建新 mm——"换地址空间就是换一棵页表树 + 换 CR3",M04 会展开。出题原则:10 题中记忆性题目不超过 3 道,其余为应用/推理/权衡类——场景预测、设计 trade-off 比较、从代码路径找问题、解释"为什么这样设计而不是那样"。难度递进,最后两道需要想一会儿。
不是由谁创建的——init_task 是静态定义在内核镜像里的。必须是静态的,因为创建路径(copy_process)本身需要"当前进程"(current)存在才能继承东西;第一个进程没有"当前",只能凭空定义。这是打破循环依赖的最小作弊:定义一个 0 号,后面的一切就都能走正常路径了。0 号随后变成每 CPU 的 idle 进程。
顺序决定了 PID 1 和 2 的归属:先创建的 kernel_init 拿 PID 1,后创建的 kthreadd 拿 PID 2——"1 号/2 号"的身份是这两行的先后写死的。kernel_thread 和 kthread_create 是两码事:前者是 rest_init 用的直接创建调用(此时 kthreadd 还没出生,没有中转站可用);后者是经 kthreadd 中转的创建机制(调用者可能不能睡眠)。名字像,路径完全不同。
第一段:kernel_init 作为内核线程做准备(kernel_init_freeable:挂载 rootfs、开 /dev/console 做 0/1/2 fd);第二段:execve("/init") 把自己替换成用户态程序。第二段用的是 M01 的"替换"机制——说明内核用通用机制的组合代替专用通道:"第一个用户进程"不需要特殊出生通道,一次普通的 execve 就够了。特殊的是调用者(PID 1 的内核线程),不是机制。
kthreadd 是所有内核线程的父亲、kthread_create() 的中转站。需要中转是因为调用者可能处于不能睡眠的上下文(中断、持锁路径),而 copy_process 要分配 task_struct 和内核栈、可能睡眠。kthread_create 把创建请求排给 kthreadd(永远可睡眠的上下文)代劳——把"不能睡眠的请求者"和"需要睡眠的创建动作"解耦。
全局 init 退出 → 内核 panic("Attempted to kill init!");容器 init 退出 → 只停容器,不 panic。因为 do_exit 检查的是 is_global_init()——容器 init 不是全局 init,走正常退出路径。这是 pid namespace 的语义:每个 namespace 有自己的 child_reaper,全局 init 死了整个系统无意义、panic,容器 init 死了只影响本容器。
不会调 wait()。kthreadd 启动时调 ignore_signals() 把 SIGCHLD 设为 SIG_IGN——子线程退出时内核自动回收,连僵尸中间态都不产生。wait() 是"父取子退出值"的同步契约(M01 Q9),内核线程的退出值没人关心——机制选型的标准是"谁需要这个信息",没人需要就别建状态。
被 init(PID 1) 收养——机制是 forget_original_parent() → find_new_reaper(),三级 fallback 的终点就是 init。kthreadd 从不收养孤儿:它是"直接子线程"的父亲(kthread_create 中转),不是孤儿的收养者。"收养"是用户态进程树管理的概念,kthreadd 的孩子们是内核线程,不走这条路。
内核在信号投递路径上直接拦截:sig_task_ignored()(kernel/signal.c),条件是 is_global_init(t) && sig_kernel_only(sig)——目标是全局 init 且信号是内核专有信号(SIGKILL/SIGSTOP 属于此类)→ 直接返回"忽略"。这不是"1 号屏蔽了信号",是投递路径上的精准硬判,不留配置口。
/init 直接 return 0 → 内核 panic(不是关机)。init 的退出在语义上只可能是"意外死亡",内核的选择是 panic 而不是"顺便关机"。正常关机走 reboot(2) 系统调用(LINUX_REBOOT_CMD_POWER_OFF / RESTART)——由内核直接断电/重启,init 根本不需要"退出"来关机。
假设本身不成立。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 的系统"在内核看来根本不该存在。
start_kernel、rest_init,启动的本体kthreadd、kthread_create 中转机制sig_task_ignored,1 号杀不死的本体forget_original_parent、find_new_reaper,孤儿移交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。
在页面里每题写下你的答案(关键词即可),卡住的题点一下"卡住了"标记,
点"生成作答汇总"后复制、贴到聊天里——对错由我来判定,我逐题批改并据此
调整下一关的深浅;准备好了说"下一关"。
点下面按钮生成汇总文本,复制后贴到「内核二十关」侧聊里,对错由我判定。