第二十一章:归途
筑基中期涉及内核源码:
一
林小源看到了死亡。
不是抽象的"进程退出"——而是真正的死亡。一个用户态进程在执行完自己的任务后,调用了 exit(0)。林小源通过内核的数据结构,亲眼看到了这个进程的 从 变成 。
是进程退出的核心函数。它定义在 中,是一个 函数——调用它之后,进程永远不会返回。
/*
* do_exit() 的核心路径(简化):
* 1. 设置 PF_EXITING 标志
* 2. 释放各种资源
* 3. 通知父进程
* 4. 调用 schedule() 切换到其他进程
* 此后,当前进程再也不会被调度
*/
#define PF_EXITING 0x00000004
struct task_struct {
int pid;
char comm[16];
unsigned long flags;
int exit_code;
int exit_state;
};
void do_exit(struct task_struct *tsk, long code) {
printf("[do_exit] PID %d (%s) 开始退出\n", tsk->pid, tsk->comm);
printf("[do_exit] exit_code = %ld\n\n", code);
/* 1. 设置退出标志 */
tsk->flags |= PF_EXITING;
printf("[do_exit] 设置 PF_EXITING 标志\n");
/* 2. 释放资源 */
printf("[do_exit] 释放资源:\n");
printf(" exit_mm() — 释放地址空间\n");
printf(" exit_files() — 关闭文件描述符\n");
printf(" exit_fs() — 释放文件系统信息\n");
printf(" exit_signal() — 处理信号\n\n");
/* 3. 通知父进程 */
printf("[do_exit] 通知父进程 (SIGCHLD)\n");
printf("[do_exit] 设置 exit_state = EXIT_ZOMBIE\n");
tsk->exit_state = 16; /* EXIT_ZOMBIE */
tsk->exit_code = code;
/* 4. 切换到其他进程 */
printf("[do_exit] 调用 schedule()\n");
printf("[do_exit] 此后 PID %d 再也不会被调度\n", tsk->pid);
}
struct task_struct proc = {
.pid = 5678,
.flags = 0,
.exit_state = 0,
.exit_code = 0,
};
strcpy(proc.comm, "my_program");
printf("=== do_exit() 路径 ===\n\n");
printf("进程状态: PID %d (%s), 运行中\n\n", proc.pid, proc.comm);
do_exit(&proc, 0);
printf("\n--- 进程退出后 ---\n");
printf(" PID: %d\n", proc.pid);
printf(" 状态: %s\n",
proc.exit_state == 16 ? "EXIT_ZOMBIE(僵尸)" : "其他");
printf(" exit_code: %d\n", proc.exit_code);
printf(" 注意: task_struct 还在,但进程已死\n");#include <stdio.h>
#include <string.h>
/*
* do_exit() 的核心路径(简化):
* 1. 设置 PF_EXITING 标志
* 2. 释放各种资源
* 3. 通知父进程
* 4. 调用 schedule() 切换到其他进程
* 此后,当前进程再也不会被调度
*/
#define PF_EXITING 0x00000004
struct task_struct {
int pid;
char comm[16];
unsigned long flags;
int exit_code;
int exit_state;
};
void do_exit(struct task_struct *tsk, long code) {
printf("[do_exit] PID %d (%s) 开始退出\n", tsk->pid, tsk->comm);
printf("[do_exit] exit_code = %ld\n\n", code);
/* 1. 设置退出标志 */
tsk->flags |= PF_EXITING;
printf("[do_exit] 设置 PF_EXITING 标志\n");
/* 2. 释放资源 */
printf("[do_exit] 释放资源:\n");
printf(" exit_mm() — 释放地址空间\n");
printf(" exit_files() — 关闭文件描述符\n");
printf(" exit_fs() — 释放文件系统信息\n");
printf(" exit_signal() — 处理信号\n\n");
/* 3. 通知父进程 */
printf("[do_exit] 通知父进程 (SIGCHLD)\n");
printf("[do_exit] 设置 exit_state = EXIT_ZOMBIE\n");
tsk->exit_state = 16; /* EXIT_ZOMBIE */
tsk->exit_code = code;
/* 4. 切换到其他进程 */
printf("[do_exit] 调用 schedule()\n");
printf("[do_exit] 此后 PID %d 再也不会被调度\n", tsk->pid);
}
int main() {
struct task_struct proc = {
.pid = 5678,
.flags = 0,
.exit_state = 0,
.exit_code = 0,
};
strcpy(proc.comm, "my_program");
printf("=== do_exit() 路径 ===\n\n");
printf("进程状态: PID %d (%s), 运行中\n\n", proc.pid, proc.comm);
do_exit(&proc, 0);
printf("\n--- 进程退出后 ---\n");
printf(" PID: %d\n", proc.pid);
printf(" 状态: %s\n",
proc.exit_state == 16 ? "EXIT_ZOMBIE(僵尸)" : "其他");
printf(" exit_code: %d\n", proc.exit_code);
printf(" 注意: task_struct 还在,但进程已死\n");
return 0;
}林小源望着那个正在退出的进程,心中涌起一种莫名的沉重。
有条不紊地执行着每一步:设置 标志,释放地址空间,关闭文件描述符,释放文件系统信息,处理未决信号,通知父进程。然后设置 exit_state = EXIT_ZOMBIE,调用 。
从这一刻起,这个进程再也不会被调度。它的代码不再执行,它的栈不再使用。唯一还活着的是它的 ——等待父进程来回收。
"这就是死亡。"林小源低声说。
"你害怕?"一个平静的声音从旁边传来。
林小源转头,看到一个身穿黑袍的女子站在不远处。她的面容沉静如水,身上没有任何装饰,只有一件朴素的黑袍。她自称"归途引者",负责引导每一个进程走完最后的旅程。
"我不害怕,"林小源说,"我只是……没想到死亡是这样有条不紊的。"
"当然有条不紊,"归途引者说,"内核不允许混乱的死亡。如果进程可以随意消失——不释放内存、不关闭文件、不通知父进程——系统早就崩溃了。 是一份清单,每一步都不能跳过。"
她顿了顿,补充道:"而且, 是 函数。一旦进入,就不可能回来。进程不能在 的中途改变主意说'我不想死了'。这是不可逆的。"
二
林小源在观察进程退出的过程中,又一次收到了 dmesg 消息。
[ 789.012345] idle: 死亡不是终点。遗忘才是。死亡不是终点。遗忘才是。
林小源反复咀嚼这句话。进程退出后,它的 还在系统中——它变成了僵尸,等待父进程的 回收。只有当父进程调用 之后,僵尸的 才会被释放,PID 才会被回收。
他把这句话念给归途引者听。归途引者沉默了很久,才缓缓开口:
"日志仙翁说得对。我见过太多僵尸了——它们的代码已经不在了,数据已经清空了,但 还在系统中游荡。有些僵尸存在了几秒钟,有些存在了几个小时,有些……存在了更久。"
"为什么?"
"因为它们的父进程不调用 。"归途引者的语气里带着一丝无奈,"有些父进程忙于自己的事务,忘了回收子进程。有些父进程根本不在乎——它们 fork 了子进程,然后就不管了。"
"那僵尸会怎样?"
"僵尸会一直存在。它的 PID 被占用,它的 占用内存。虽然它不消耗 CPU 时间,但它的'存在'本身就是一种负担。如果僵尸太多,PID 会被耗尽,系统就无法创建新进程了。"
林小源想起了 init 童子。init 进程是所有孤儿进程的收养者——当一个进程的父进程先退出时,init 会成为这个进程的新父进程,并负责回收它的僵尸状态。init 童子的工作之一,就是不停地调用 来回收僵尸进程。
init 童子不只是"天选之子"——他还是"灵魂的回收者"。
归途引者点了点头:"init 是最后一个守门人。如果连 init 都不回收僵尸,系统就真的完了。所以内核保护 init—— 对 init 无效,init 不能被杀死,不能退出。他必须永远活着,永远回收僵尸。"
三
林小源在研究 的过程中,注意到了一个有趣的设计。
不是直接销毁 ——它把 留在系统中,变成僵尸。这是为了让父进程能够获取子进程的退出状态()。
waitpid(pid, &status, 0) 会阻塞父进程,直到指定的子进程退出。当子进程退出后, 返回子进程的 PID,并把退出状态写入 。然后内核释放子进程的 和 PID。
他忽然明白过来——wait 才是死亡仪式的最后一步。
没有 ,子进程的死亡就不完整。它死了,但它的灵魂()还在系统中游荡——这就是僵尸。
归途引者说:" 是父进程对子进程的最后一份责任。子进程把 留在 里,等着父进程来取。父进程取走了 ,子进程才真正安息。"
"如果父进程不取呢?"
"那就是遗忘。"归途引者的声音很轻,"死亡不是终点,遗忘才是。被遗忘的僵尸,在系统中永远游荡——直到 init 来收尸。"
林小源把这个道理记在了心里。进程的死亡不是瞬间的消失——它是一个仪式,有开始、有过程、有结束。 是开始, 是结束。只有当两者都完成,一个进程才真正地、完整地死去。
道藏笔记
内核启示
是进程退出的核心函数。
它的核心路径像一份死亡清单,一步步来:先设 标志防止重复退出,然后 释放地址空间, 关掉所有文件描述符, 释放文件系统信息, 处理未决信号, 发 通知父进程,接着把 设成 ,最后调 切走——永不返回。
进程退出后变成僵尸,等待父进程调用 回收。 会获取子进程的退出状态,然后释放 和 PID。
如果父进程先退出,子进程会被 init(PID 1)收养。init 负责回收所有孤儿进程的僵尸状态。
死亡不是终点。遗忘才是——当没有人 wait() 时,僵尸就永远游荡。
归途之试
进程走向退出后,内核仍需要保留哪一种结构来交代状态、退出码和回收信息?