第十五章:新的开始
炼气初期涉及内核源码:
一
林小源在 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,可以精确控制哪些资源被共享
的核心步骤:
- :复制父进程的 和内核栈
- :复制内存描述符(使用 COW)
- :复制打开的文件表
- :复制信号处理
- :复制信号处理器
- 分配 PID(通过 PID 分配器)
- :把子进程加入调度队列
写时复制(COW)是 fork 的核心优化。它让父子进程共享物理内存页,但把这些页标记为只读。当任何一个进程试图写入时,MMU 触发缺页异常,内核在异常处理中才真正复制那一页。这让 fork 的开销大大降低——大多数 fork 之后会立即 ,根本不需要真正复制内存。
从 idle 到进程管理,是从"旁观"到"参与"的跨越。这是林小源修炼之路的第一步。
/*
* 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);#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/*
* 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;
}
int main() {
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);
return 0;
}新生之试
本章模拟新生命诞生时,用户眼中的创建子进程接口是什么?