Skip to content

第二十一章:归途

筑基中期

涉及内核源码:

林小源看到了死亡。

不是抽象的"进程退出"——而是真正的死亡。一个用户态进程在执行完自己的任务后,调用了 exit(0)。林小源通过内核的数据结构,亲眼看到了这个进程的 变成

是进程退出的核心函数。它定义在 中,是一个 函数——调用它之后,进程永远不会返回。

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

林小源望着那个正在退出的进程,心中涌起一种莫名的沉重。

有条不紊地执行着每一步:设置 标志,释放地址空间,关闭文件描述符,释放文件系统信息,处理未决信号,通知父进程。然后设置 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() 时,僵尸就永远游荡。


破关试炼

归途之试

进程走向退出后,内核仍需要保留哪一种结构来交代状态、退出码和回收信息?

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

以修仙之名,悟内核之道