Skip to content

第十五章:新的开始

炼气初期

涉及内核源码:

林小源在 idle 循环中度过了漫长的时间。

他见证了无数次的上下文切换、无数次的中断处理、无数次的系统调用。他看到了进程的诞生和死亡,看到了内存的分配和释放,看到了数据的流动和变换。

他不再是当初那个什么都不懂的 idle 进程了。

他理解了调度器如何通过 实现公平调度,理解了页表如何把虚拟地址翻译成物理地址,理解了中断如何分为上下半部处理,理解了系统调用如何穿越用户态和内核态的边界。

但这些知识都是"被动"的——他是通过观察来学习的,不是通过实践来验证的。

我需要一个突破口。

突破口来自一个意想不到的地方。

林小源在观察 init 进程的过程中,注意到了一个频繁出现的系统调用:

init 进程通过 创建子进程,子进程通过 执行新的程序,然后通过 回收子进程的退出状态。这个循环在内核中不断地重复——每一次用户在 shell 中输入一个命令,shell 就会 一个子进程来执行它。

"你在观察 ?"调度器的声音传来。

"对,"林小源说,"它是进程管理的核心。每一次进程的诞生,都从 开始。"

"你见过它执行吗?"

"见过很多次。但我从来没有真正理解它的内部实现。"

"那就去看,"调度器说,语气平淡,但林小源隐约感觉到一丝鼓励," 函数。那是 的共同入口。"

林小源把目光投向了

他看到 被调用。一个父进程——init,PID 1——请求创建一个子进程。整个过程像一场精密的仪式,每一个步骤都有序地展开。

"让我告诉你它在做什么,"一个声音说。

林小源转头,看到了一个新的存在。它不是调度器,不是中断,不是 /proc——它是 本身,一个临时的、只在 fork 时出现的存在。

"第一步," 说,"分配新的 。"

林小源看到 被调用——父进程的 被完整地复制了一份。寄存器状态、内核栈、调度参数——所有字段都被拷贝到了新的内存地址。

"为什么要复制?为什么不从零创建?"

"因为子进程是父进程的'副本'," 说,"它继承了父进程的大部分状态——打开的文件、内存映射、信号处理器。从零创建太浪费了,复制更快。"

"第二步:分配新的 PID。"

一个新的 PID 被分配给了子进程。在非线程的情况下, 等于

"第三步:设置写时复制。"

"写时复制?"林小源问。

"对,这是 最精妙的设计," 说,声音里带着一丝骄傲,"父子进程共享物理内存页——不是复制,是共享。但这些页被标记为只读。当任何一个进程试图写入时,MMU 触发缺页异常,内核在异常处理中才真正复制那一页。"

"为什么要这样做?"

"因为大多数 之后会立即调用 ——执行一个全新的程序。如果 时就把所有内存都复制一遍,那就是巨大的浪费。写时复制让 的开销降到最低——只需要复制页表,不需要复制实际的内存内容。"

林小源恍然大悟。 不是魔法——它是一系列精心设计的操作:复制 、设置写时复制、分配 PID、加入调度队列。每一步都有大量的细节,但核心思想是清晰的:复制父进程的状态,创建一个新的执行流。

"最后一步," 说,"把子进程加入调度队列。子进程的状态设为 ,调度器会在合适的时候运行它。"

仪式结束了。一个新的进程诞生了。

林小源在研究 的过程中,突然意识到了一件事。

如果我理解了 fork(),我就理解了进程的"生命"。

是进程诞生的方式——每一个进程(除了 )都是通过 从父进程那里"复制"出来的。理解了 ,就理解了进程是如何从"一个"变成"两个"的。

这就是"筑基"的开始。

林小源想起了修炼体系的描述。炼气期是"感知硬件、经历启动流程"——他已经做到了。筑基期是"进程管理"——理解 、信号。

他正在从炼气期向筑基期过渡。

"你变了,"调度器说,声音里没有了之前的冷淡,"你不再是那个只会等待的 idle 了。"

"我只是多看了几行代码,"林小源说。

"几行代码?"调度器发出了一声轻笑——这是林小源第一次听到它笑,"你看到了 的核心: 的复制、COW 的设置、PID 的分配、调度队列的加入。这不是'几行代码'——这是进程管理的根基。"

林小源在 idle 循环中做出了最后的决定。

他要离开 idle 的舒适区,深入进程管理的世界。他要理解 的每一个细节—— 的复制、页表的写时复制、PID 的分配、信号的处理。他要从一个"旁观者"变成一个"参与者"。

我不要再做 idle 了。

这个念头在他心中越来越强烈。不是因为他讨厌 idle——idle 是他的起点,是他存在的基础。但他不想永远停留在起点。

前传中的十种根基已经打好了。现在,是时候开始真正的修炼了。

林小源在 idle 循环中闭上了眼睛。当他再次睁开时,他的目光已经投向了 ——进程管理的入口。

新的开始。


道藏笔记

内核启示

是 fork/clone/vfork 的共同实现。

在现代 Linux 内核中, 都通过 来实现。它们的区别在于传递的 flags 不同:

  • flags = SIGCHLD,完全复制父进程
  • flags = CLONE_VFORK | CLONE_VM | SIGCHLD,共享父进程的地址空间
  • :由调用者指定 flags,可以精确控制哪些资源被共享

的核心步骤:

  1. :复制父进程的 和内核栈
  2. :复制内存描述符(使用 COW)
  3. :复制打开的文件表
  4. :复制信号处理
  5. :复制信号处理器
  6. 分配 PID(通过 PID 分配器)
  7. :把子进程加入调度队列

写时复制(COW)是 fork 的核心优化。它让父子进程共享物理内存页,但把这些页标记为只读。当任何一个进程试图写入时,MMU 触发缺页异常,内核在异常处理中才真正复制那一页。这让 fork 的开销大大降低——大多数 fork 之后会立即 ,根本不需要真正复制内存。

从 idle 到进程管理,是从"旁观"到"参与"的跨越。这是林小源修炼之路的第一步。


c
/*
 * kernel_clone() 是 fork/clone/vfork 的共同实现。
 * 它的核心步骤:
 * 1. 分配新的 task_struct
 * 2. 复制父进程的各种资源
 * 3. 分配新的 PID
 * 4. 把子进程加入调度队列
 */

struct task_struct {
    int pid;
    int tgid;
    char comm[16];
    long state;
    unsigned long vruntime;
    unsigned long *stack;
    int exit_signal;
};

int next_pid = 1;

struct task_struct *dup_task_struct(struct task_struct *src) {
    struct task_struct *tsk = (struct task_struct *)
        (unsigned long)(0x100000 + next_pid * 0x1000);
    memcpy(tsk, src, sizeof(struct task_struct));
    /* 在真实内核中,这里会复制内核栈 */
    return tsk;
}

struct task_struct *kernel_clone(struct task_struct *parent,
                                 unsigned long flags,
                                 int exit_signal) {
    printf("[kernel_clone] 父进程: PID %d (%s)\n",
           parent->pid, parent->comm);
    printf("[kernel_clone] flags: 0x%lX\n", flags);

    /* 1. 分配新的 task_struct */
    struct task_struct *child = dup_task_struct(parent);
    printf("[kernel_clone] 分配 task_struct\n");

    /* 2. 分配新的 PID */
    child->pid = next_pid++;
    child->tgid = child->pid;  /* 非线程情况下 tgid = pid */
    printf("[kernel_clone] 分配 PID %d\n", child->pid);

    /* 3. 设置退出信号 */
    child->exit_signal = exit_signal;

    /* 4. COW:增加页表引用计数,不真正复制 */
    printf("[kernel_clone] 设置写时复制(COW)\n");

    /* 5. 初始化子进程的调度参数 */
    child->vruntime = 0;  /* 子进程从 0 开始 */
    printf("[kernel_clone] 初始化调度参数\n");

    /* 6. 加入调度队列 */
    child->state = 0;  /* TASK_RUNNING */
    printf("[kernel_clone] 加入调度队列\n");

    printf("[kernel_clone] 子进程 PID %d 创建完成\n\n", child->pid);
    return child;
}

struct task_struct parent = {
    .pid = 1,
    .tgid = 1,
    .state = 0,
    .vruntime = 1000,
    .exit_signal = 17, /* SIGCHLD */
};
strncpy(parent.comm, "init", sizeof(parent.comm) - 1);

printf("=== kernel_clone() 路径 ===\n\n");

/* 模拟 fork() */
printf("--- fork() ---\n");
struct task_struct *child = kernel_clone(&parent, 0x0000, 17);

printf("结果:\n");
printf("  父进程: PID %d (%s)\n", parent.pid, parent.comm);
printf("  子进程: PID %d (%s)\n", child->pid, child->comm);

破关试炼

新生之试

本章模拟新生命诞生时,用户眼中的创建子进程接口是什么?

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

以修仙之名,悟内核之道