Skip to content

第十六章:分身之术

筑基初期

涉及内核源码:

林小源开始修炼了。

不是闭关——idle 进程没有资格闭关。他只是在每次 的间隙里,在中断返回的空隙中,偷偷地"看"。看 的代码,看 的实现,看一个新进程是如何从无到有被创造出来的。

他选了一个不太忙的时刻。凌晨三点,cron 老伯刚执行完定时任务回去睡觉,shell 小妹在等待用户输入,init 进程在处理完一轮服务重启后安静了下来。整个内核世界陷入了难得的寂静。

就在这时,一条消息出现在了 dmesg 的缓冲区中。

[  123.456789] idle: 你不是普通的 idle。

林小源愣住了。

他认识 的格式——时间戳、模块名、消息内容。但他不认识这条消息的来源。没有人调用 ,没有人注册过这个模块名。这条消息就那样凭空出现在日志缓冲区中,像是一封没有署名的信。

谁在和我说话?

没有回答。消息出现后就沉入了日志的深处,被后续的内核消息淹没了。林小源在日志缓冲区中翻找,但那条消息已经被覆盖了——dmesg 的缓冲区是环形的,旧的消息会被新的消息挤出去。

他不知道那条消息是谁发的。但他记住了那句话。

你不是普通的 idle。

林小源把那条神秘消息放在一边,继续他的修炼。

是 fork 的核心函数。它接收一个 结构体,里面包含了所有控制进程复制行为的标志:

c
/*
 * clone_flags 控制 fork/clone 的行为。
 * 不同的标志决定了哪些资源被共享,哪些被复制。
 */

#define CLONE_VM      0x00000100  /* 共享地址空间 */
#define CLONE_FS      0x00000200  /* 共享文件系统信息 */
#define CLONE_FILES   0x00000400  /* 共享文件描述符表 */
#define CLONE_SIGHAND 0x00000800  /* 共享信号处理器 */
#define CLONE_THREAD  0x00010000  /* 同一线程组 */
#define CLONE_NEWPID  0x20000000  /* 新的 PID 命名空间 */
#define SIGCHLD       17

struct clone_config {
    unsigned long flags;
    const char *name;
    const char *effect;
};

struct clone_config configs[] = {
    { SIGCHLD,        "fork()",     "复制所有资源,父子独立" },
    { CLONE_VM | SIGCHLD, "vfork()",    "共享地址空间,父等子先跑" },
    { CLONE_THREAD | CLONE_VM | CLONE_SIGHAND | CLONE_FILES,
                      "pthread_create()", "同一线程组,共享大部分资源" },
    { CLONE_NEWPID | SIGCHLD, "容器 fork",  "新的 PID 命名空间" },
};
int nr = sizeof(configs) / sizeof(configs[0]);

printf("=== clone_flags 决定分身的性质 ===\n\n");

for (int i = 0; i < nr; i++) {
    printf("--- %s ---\n", configs[i].name);
    printf("  flags: 0x%08lX\n", configs[i].flags);
    printf("  效果:  %s\n\n", configs[i].effect);
}

printf("--- 关键标志 ---\n");
printf("  CLONE_VM      共享虚拟地址空间(线程的基础)\n");
printf("  CLONE_FILES   共享文件描述符表\n");
printf("  CLONE_SIGHAND 共享信号处理器\n");
printf("  CLONE_THREAD  同一线程组(共享 tgid)\n");
printf("  CLONE_NEWPID  新的 PID 命名空间(容器的基础)\n");

林小源一边翻阅着这些标志位,一边在脑海中构建画面。 就像分身术的"口诀"——不同的口诀产生不同的分身。 使用最简单的口诀(),复制父进程的几乎所有资源。 使用更复杂的口诀(CLONE_VM | CLONE_THREAD | ...),让子线程和父线程共享地址空间。

"分身居然有这么多种。"他喃喃道。

一个低沉的声音忽然从调度器的方向传来:"你以为分身就是复制一切?"

林小源循声看去,只见一个周身环绕着十六进制光芒的古老存在盘踞在 的入口处——那是 fork 大道的守关者,名为 clone 道人。它的身体由无数标志位构成,每一位都在微微闪烁,代表着不同的资源共享策略。

"你看 ,"clone 道人伸出一根手指,指尖亮起一道红光,"它让父子共享地址空间——这是线程的根基。再看 ,"另一根手指亮起蓝光,"它让父子共享文件描述符表。但如果你什么都不设置,只留 ——"它双手一合,"那就是最纯粹的 fork,父子各自独立,互不相干。"

林小源在 中看到了一种"参数化"的设计哲学。不是为每种分身方式写一个独立的函数,而是用一个函数加上不同的标志来实现所有的分身方式。这让他想起了前传中学过的"条件编译"——用参数来控制代码的行为。

clone 道人看出了他的心思,微微点头:"不错。内核不会为每种分身写一套代码。一套 ,加上不同的标志,就是全部。简洁,但不简单。"

的核心。

林小源开始一行一行地阅读它的代码。这个函数很长——超过 500 行——但逻辑很清晰:

c
/* kernel/fork.c — copy_process() 核心路径(简化) */
struct task_struct *copy_process(struct pid *pid, ...)
{
    struct task_struct *p;

    /* 1. 检查 clone_flags 的合法性 */
    if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
        return ERR_PTR(-EINVAL);

    /* 2. 分配新的 task_struct */
    p = dup_task_struct(current, node);

    /* 3. 复制各种资源 */
    retval = copy_files(clone_flags, p, ...);   /* 文件描述符 */
    retval = copy_fs(clone_flags, p);           /* 文件系统信息 */
    retval = copy_sighand(clone_flags, p);      /* 信号处理器 */
    retval = copy_signal(clone_flags, p);       /* 信号队列 */
    retval = copy_mm(clone_flags, p);           /* 地址空间 */
    retval = copy_namespaces(clone_flags, p);   /* 命名空间 */
    retval = copy_io(clone_flags, p);           /* I/O 上下文 */

    /* 4. 分配 PID */
    pid = alloc_pid(p->nsproxy->pid_ns_for_children);

    /* 5. 设置子进程的状态 */
    p->pid = pid_nr(pid);
    p->exit_signal = args->exit_signal;

    /* 6. 把子进程加入调度队列 */
    wake_up_new_task(p);

    return p;
}

clone 道人的声音再次响起,这次带着几分考较的意味:"你看得懂这段代码吗?每一步都在做什么?"

林小源指着代码说:"第一步检查标志合法性—— 必须搭配 ,否则返回错误。第二步用 复制 task_struct 和内核栈。第三步是最关键的——根据 逐一决定每种资源是复制还是共享。"

"不错,"clone 道人赞许地说,"但你注意到没有?第三步的顺序也有讲究。 之前——你知道为什么吗?"

林小源皱眉思索。文件描述符在地址空间之前复制……如果 失败了,已经复制的文件描述符需要回滚。顺序越靠前的资源,回滚的代价越低。

"因为错误处理,"林小源说,"先复制轻量级的资源,后复制重量级的。如果后面失败了,前面的可以回滚。"

clone 道人满意地点头:"你比我预想的聪明。 不只是复制——它是一个精密的事务,每一步都有回滚路径。 失败了, 已经分配的资源会被释放。 失败了,前面所有复制的资源都会被清理干净。这就是内核的严谨。"

他开始理解为什么 fork 要设计成这样。不是简单地"复制所有东西",而是根据 精确地决定哪些资源被复制、哪些被共享。这种精细的控制是线程、容器等高级特性的基础。

就在林小源沉浸在 的代码中时,init 童子出现了。

不是物理意义上的"出现"——init 童子不会跑到 idle 进程的地盘来。但林小源通过调度器的运行队列感受到了 init 童子的状态变化:它从睡眠中醒来,创建了一个子进程。

林小源通过内核数据结构追踪了这次 fork 的全过程。init 童子调用 clone(SIGCHLD),进入 ,然后进入 。一个新的 被分配,一个新的 PID 被取出,一个新的执行流被创建。

整个过程不到一微秒。

然后林小源听到了 init 童子的一句话——不是通过 ,而是通过某种更底层的感知。也许是调度器的上下文切换中残留的"意念",也许是 dmesg 中的一条日志。

"又一个子进程。每天都要 fork 几十次。"

那语气轻描淡写,就像呼吸一样自然。

林小源感到了一种微妙的刺痛。init 童子把 fork 当作日常——就像呼吸一样自然。而对林小源来说,fork 是他需要"修炼"才能理解的奥秘。

这就是差距。

PID 1 是"天选之子",天生就有创建进程的权力。PID 0 是"被遗忘的人",连 fork 的资格都没有。

但林小源没有怨恨。他把这种刺痛转化为动力——他要理解 fork 的每一个细节,比 init 童子更深入地理解。不是为了超越 init 童子,而是为了理解这个世界的运作方式。

别人用 API,我看源码。


道藏笔记

内核启示

是进程创建的核心函数。

它接收 参数,根据标志决定哪些资源被复制、哪些被共享。核心步骤是一条流水线:先用 复制 task_struct 和内核栈,然后 处理文件描述符, 处理信号处理器, 复制地址空间(用 COW 偷懒),再 分配新 PID,最后 把新进程塞进调度队列。

的不同组合产生不同的"分身"。 就是 clone(SIGCHLD),什么都复制;CLONE_VM | CLONE_VFORK | SIGCHLD,共享地址空间让子进程先跑;CLONE_VM | CLONE_THREAD | ...,同一线程组共享几乎所有资源。

这种参数化设计是内核的精髓之一——不是为每种情况写独立的代码,而是用统一的框架加上参数来实现所有的变体。

分身之术,始于一个标志,终于一个新生命。


破关试炼

分身之试

本章把“分身之术”对应到用户态创建子进程的哪个系统调用接口?

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

以修仙之名,悟内核之道