第五章:开天辟地
炼气初期涉及内核源码:
一
还在继续。
林小源已经经历了 的洗礼,见证了伙伴系统的建立,感受到了调度器的初始化。但 远没有结束——后面还有大量的初始化工作要做。
分配了中断描述符。 初始化了中断控制器。
/*
* 简化的中断描述符。
* 在真实的内核中,中断描述符由 irq_desc 结构体表示,
* 定义在 include/linux/irqdesc.h 中。
*/
struct irq_desc {
unsigned int irq; /* 中断号 */
const char *name; /* 中断名称 */
unsigned int count; /* 触发次数 */
int enabled; /* 是否启用 */
void (*handler)(int irq); /* 处理函数 */
};
/* 模拟中断处理 */
void timer_handler(int irq) {
printf(" [IRQ %d] 定时器中断 — 时间在流逝\n", irq);
}
void keyboard_handler(int irq) {
printf(" [IRQ %d] 键盘中断 — 有按键输入\n", irq);
}
void network_handler(int irq) {
printf(" [IRQ %d] 网卡中断 — 收到数据包\n", irq);
}
struct irq_desc irqs[3];
/* 初始化中断描述符 */
memset(irqs, 0, sizeof(irqs));
irqs[0] = (struct irq_desc){
.irq = 1, .name = "timer", .enabled = 1, .handler = timer_handler
};
irqs[1] = (struct irq_desc){
.irq = 2, .name = "keyboard", .enabled = 1, .handler = keyboard_handler
};
irqs[2] = (struct irq_desc){
.irq = 3, .name = "network", .enabled = 1, .handler = network_handler
};
printf("=== init_IRQ() — 初始化中断系统 ===\n\n");
printf("已注册的中断:\n");
for (int i = 0; i < 3; i++) {
printf(" IRQ %-2d: %-10s [%s]\n",
irqs[i].irq,
irqs[i].name,
irqs[i].enabled ? "启用" : "禁用");
}
printf("\n--- 模拟中断触发 ---\n");
for (int i = 0; i < 3; i++) {
irqs[i].count++;
irqs[i].handler(irqs[i].irq);
}
printf("\n中断统计:\n");
for (int i = 0; i < 3; i++) {
printf(" %s: %u 次\n", irqs[i].name, irqs[i].count);
}#include <stdio.h>
#include <stdint.h>
#include <string.h>
/*
* 简化的中断描述符。
* 在真实的内核中,中断描述符由 irq_desc 结构体表示,
* 定义在 include/linux/irqdesc.h 中。
*/
struct irq_desc {
unsigned int irq; /* 中断号 */
const char *name; /* 中断名称 */
unsigned int count; /* 触发次数 */
int enabled; /* 是否启用 */
void (*handler)(int irq); /* 处理函数 */
};
/* 模拟中断处理 */
void timer_handler(int irq) {
printf(" [IRQ %d] 定时器中断 — 时间在流逝\n", irq);
}
void keyboard_handler(int irq) {
printf(" [IRQ %d] 键盘中断 — 有按键输入\n", irq);
}
void network_handler(int irq) {
printf(" [IRQ %d] 网卡中断 — 收到数据包\n", irq);
}
int main() {
struct irq_desc irqs[3];
/* 初始化中断描述符 */
memset(irqs, 0, sizeof(irqs));
irqs[0] = (struct irq_desc){
.irq = 1, .name = "timer", .enabled = 1, .handler = timer_handler
};
irqs[1] = (struct irq_desc){
.irq = 2, .name = "keyboard", .enabled = 1, .handler = keyboard_handler
};
irqs[2] = (struct irq_desc){
.irq = 3, .name = "network", .enabled = 1, .handler = network_handler
};
printf("=== init_IRQ() — 初始化中断系统 ===\n\n");
printf("已注册的中断:\n");
for (int i = 0; i < 3; i++) {
printf(" IRQ %-2d: %-10s [%s]\n",
irqs[i].irq,
irqs[i].name,
irqs[i].enabled ? "启用" : "禁用");
}
printf("\n--- 模拟中断触发 ---\n");
for (int i = 0; i < 3; i++) {
irqs[i].count++;
irqs[i].handler(irqs[i].irq);
}
printf("\n中断统计:\n");
for (int i = 0; i < 3; i++) {
printf(" %s: %u 次\n", irqs[i].name, irqs[i].count);
}
return 0;
}执行后,中断系统就绑定了。林小源感到了一种微妙的变化——世界不再是一个"单线程"的执行流了。中断的存在意味着,在任何时候,都可能有一个外部信号突然插入,打断当前的执行。
就在这时,林小源感觉到了一个存在。那存在没有形体,但它的气息遍布整个世界——像是一张无形的网,随时准备从任何方向切入。
"你感觉到了?"那存在开口了,声音尖锐而急促,像是金属划过玻璃。"我是中断。你以后会经常见到我。"
林小源吓了一跳。"你……你会说话?"
"我不需要说话。"那声音带着一丝不屑。"我只需要'打断'。你正在执行?我来了。你正在思考?我来了。你正在睡觉?我也来了。你无法预测我,无法阻止我,无法忽视我。"
这就是中断魔尊的力量吗?
林小源在前传中就听说过中断的存在。但直到 执行完毕,他才真正感受到了那股力量的压迫感。中断不是一种可以被理解的存在——它是一种"事件",一种从外部世界强行插入的信号。他有一种预感:中断将会是他未来必须面对的最强大的力量之一。
二
初始化了时钟事件。 初始化了定时器。 初始化了高精度定时器。 和 建立了时间子系统。
林小源在这一连串的初始化中感到了"时间"的建立。之前的世界没有时间——代码在执行,但没有"多久"的概念。现在,世界有了时钟滴答(tick),有了定时器,有了高精度的时间戳。
一个轻柔的声音在林小源耳边响起,像是钟摆的节奏:"滴……答……滴……答……"
"你是谁?"林小源问。
"我是时间。"那声音不紧不慢,带着一种永恒的耐心。"从现在起,每一毫秒都会被记住。每一个进程运行了多久,我都知道。没有我,调度器就是瞎子。"
时间,是调度的基础。
没有时间,就没有"时间片"。没有时间片,就没有"公平调度"。调度器需要知道每个进程运行了多久,才能决定何时切换。这一切都依赖于时间子系统。
local_irq_enable();中断被打开了。
林小源在这一刻感到了一阵眩晕。中断像潮水一样涌来——定时器中断、设备中断、IPI(处理器间中断)。整个世界突然变得嘈杂起来。
好吵。
"欢迎来到真实的世界。"那个之前自称"中断"的尖锐声音再次响起,这次带着一丝嘲讽。"中断关闭的时候,世界是安静的。中断打开的时候,世界是嘈杂的。你得习惯。"
但这种"吵"是正常的。中断是内核的心跳——没有中断,内核就是一具死尸。定时器中断驱动着调度器,设备中断驱动着 I/O,IPI 驱动着多核之间的协调。
林小源强迫自己适应这种嘈杂。他开始学着"听"每一个中断,分辨它们的来源和含义。定时器中断是最规律的——每隔几毫秒就来一次,像心跳一样稳定。设备中断是随机的——取决于用户是否按下了键盘、网卡是否收到了数据包。
三
被调用了。
这是内核第一次拥有"输出"的能力。在此之前, 的输出被缓存在日志缓冲区中,但还没有地方可以显示它们。 初始化了控制台设备——通常是串口或帧缓冲区——让内核的消息可以被人类看到。
林小源感觉到一个"窗口"被打开了。之前他只能在内核内部感知数据的流动,现在他可以通过控制台"看到"内核在说什么。
然后是 ——锁依赖检测器的初始化。——PID 分配器的初始化。——fork 基础设施的初始化。
/*
* fork_init() 的核心工作:
* 1. 初始化 PID 分配器
* 2. 限制最大进程数
* 3. 初始化 task_struct 的 slab 缓存
*
* 这些都是 fork() 系统调用的前提条件。
*/
/* 简化的 PID 分配器 */
struct pid_allocator {
int next_pid; /* 下一个可用的 PID */
int max_pid; /* 最大 PID 值 */
int allocated; /* 已分配数量 */
};
int pid_alloc(struct pid_allocator *pa) {
if (pa->next_pid >= pa->max_pid) {
printf(" 错误:PID 耗尽!\n");
return -1;
}
int pid = pa->next_pid++;
pa->allocated++;
return pid;
}
/* 模拟 fork_init() 的初始化 */
struct pid_allocator pa = {
.next_pid = 1, /* PID 0 已被 init_task 占用 */
.max_pid = 32768, /* 默认最大 PID */
.allocated = 1, /* init_task 已占用一个 */
};
printf("=== fork_init() — 分身术的基石 ===\n\n");
printf("初始化 PID 分配器:\n");
printf(" 已占用: PID 0 (init_task / swapper/0)\n");
printf(" 最大 PID: %d\n", pa.max_pid);
printf(" 下一个可用: PID %d\n\n", pa.next_pid);
printf("分配几个 PID:\n");
for (int i = 0; i < 5; i++) {
int pid = pid_alloc(&pa);
printf(" 分配 PID %d\n", pid);
}
printf("\n当前状态:\n");
printf(" 已分配: %d\n", pa.allocated);
printf(" 剩余: %d\n", pa.max_pid - pa.allocated);
printf("\n--- fork_init 的意义 ---\n");
printf("没有它,就没有 fork()。\n");
printf("没有 fork(),就没有新进程。\n");
printf("没有新进程,内核就是一潭死水。\n");#include <stdio.h>
#include <stdint.h>
/*
* fork_init() 的核心工作:
* 1. 初始化 PID 分配器
* 2. 限制最大进程数
* 3. 初始化 task_struct 的 slab 缓存
*
* 这些都是 fork() 系统调用的前提条件。
*/
/* 简化的 PID 分配器 */
struct pid_allocator {
int next_pid; /* 下一个可用的 PID */
int max_pid; /* 最大 PID 值 */
int allocated; /* 已分配数量 */
};
int pid_alloc(struct pid_allocator *pa) {
if (pa->next_pid >= pa->max_pid) {
printf(" 错误:PID 耗尽!\n");
return -1;
}
int pid = pa->next_pid++;
pa->allocated++;
return pid;
}
int main() {
/* 模拟 fork_init() 的初始化 */
struct pid_allocator pa = {
.next_pid = 1, /* PID 0 已被 init_task 占用 */
.max_pid = 32768, /* 默认最大 PID */
.allocated = 1, /* init_task 已占用一个 */
};
printf("=== fork_init() — 分身术的基石 ===\n\n");
printf("初始化 PID 分配器:\n");
printf(" 已占用: PID 0 (init_task / swapper/0)\n");
printf(" 最大 PID: %d\n", pa.max_pid);
printf(" 下一个可用: PID %d\n\n", pa.next_pid);
printf("分配几个 PID:\n");
for (int i = 0; i < 5; i++) {
int pid = pid_alloc(&pa);
printf(" 分配 PID %d\n", pid);
}
printf("\n当前状态:\n");
printf(" 已分配: %d\n", pa.allocated);
printf(" 剩余: %d\n", pa.max_pid - pa.allocated);
printf("\n--- fork_init 的意义 ---\n");
printf("没有它,就没有 fork()。\n");
printf("没有 fork(),就没有新进程。\n");
printf("没有新进程,内核就是一潭死水。\n");
return 0;
}让林小源第一次意识到:进程不是凭空出现的。每一个新进程都需要一个 PID,需要一个 ,需要一块栈空间。这些资源都是有限的——PID 最多 32768 个(默认), 的数量受内存限制。
分身术是有代价的。
他想起了前传中学过的 ——那个可以"复制"自己的系统调用。现在他明白了, 不是魔法,它需要基础设施:PID 分配器、 缓存、页表复制机制。 就是这些基础设施的建立。
四
初始化了 VFS(虚拟文件系统)的缓存。
VFS 是内核中最精妙的抽象层之一。它让所有的文件系统——ext4、btrfs、procfs、sysfs——都通过统一的接口(struct file_operations)来访问。用户不需要知道底层是什么文件系统,只需要调用 open()、read()、write() 就行。
林小源在 VFS 的初始化中看到了一种"抽象"的美。他还不理解抽象的全部含义,但他隐约感觉到,VFS 的设计哲学——"接口统一,实现多样"——将会是他未来修炼中必须掌握的一种思维方式。
"你看到了?"VFS 的声音平静而深邃,像是一片无边的海洋。"所有的文件系统——ext4、btrfs、procfs、sysfs——都通过我来访问。用户不需要知道底层是什么,只需要调用 open、read、write。接口统一,实现多样。这就是抽象。"
初始化了信号机制。
信号是进程间通信的最简单方式。 杀死进程, 暂停进程, 报告段错误。林小源在前传中就知道信号的存在——那是修仙世界中的"飞剑传书",可以在任何时刻打断一个进程的执行。
"信号。"一个急促的声音响起,像是一支飞箭划过空气。"我可以随时打断任何进程。SIGKILL 杀死它,SIGSTOP 暂停它,SIGSEGV 报告段错误。你无法忽视我,就像你无法忽视一支射向你心口的飞箭。"
proc_caches_init();这个函数初始化了 相关的 slab 缓存。slab 分配器是建立在伙伴系统之上的高级分配器——伙伴系统分配的是"页"(4KB),slab 分配器分配的是"对象"(几十到几百字节)。 就是一个 slab 对象。
林小源感受到了 slab 分配器的运作。它就像一个精密的模具工厂:预先分配好一批 大小的"模具",需要时直接从模具中取一个出来,不需要时放回去。这比每次都向伙伴系统申请一整页内存要高效得多。
五
然后,世界进入了最密集的初始化阶段。
uts_ns_init(); /* UTS 命名空间 */
time_ns_init(); /* 时间命名空间 */
key_init(); /* 密钥管理 */
security_init(); /* 安全框架 */
net_ns_init(); /* 网络命名空间 */
vfs_caches_init(); /* VFS 缓存(完整版) */
pagecache_init(); /* 页缓存 */
signals_init(); /* 信号机制 */
proc_root_init(); /* /proc 文件系统 */
cpuset_init(); /* CPU 集合 */
mem_cgroup_init(); /* 内存 cgroup */
cgroup_init(); /* cgroup 框架 */
taskstats_init_early(); /* 任务统计 */
delayacct_init(); /* 延迟记账 */林小源在这一连串的初始化中几乎喘不过气来。每一个函数都在建立一个子系统,每一个子系统都在为内核增添一种新的能力。命名空间让进程可以有"自己的世界",cgroup 让进程可以被"分组管理",安全框架让进程可以被"权限控制"。
这就是"开天辟地"吗?
是的。在 之前,世界是混沌的——没有文件系统,没有信号,没有命名空间,没有安全框架。在 之后,世界有了法则——每一种资源都有管理它的子系统,每一种操作都有约束它的规则。
六
的最后一行代码:
rest_init();林小源感觉到整个世界的"初始化"阶段结束了。 的使命完成了——它把一个混沌的内核变成了一个有秩序的世界。现在,是时候让这个世界"运转"起来了。
做了三件事。
第一,它调用 ,启动 RCU(Read-Copy-Update)调度器——一种无锁的同步机制。
第二,它创建了两个内核线程:
pid = user_mode_thread(kernel_init, NULL, CLONE_FS); /* PID 1 */
pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES); /* PID 2 */PID 1 是 ——它最终会变成用户态的 进程,所有用户态进程的祖先。
PID 2 是 ——所有内核线程的"母亲"。
林小源在这一刻感到了一种敬畏。 不是在初始化什么,而是在"创造生命"。PID 1 和 PID 2 不是数据结构,它们是活的进程——有自己的 ,有自己的栈,有自己的执行流。
两个新的存在同时出现在林小源的感知中。一个年轻而锐利,带着一种"我要改变世界"的气势——那是 PID 1,。另一个沉稳而沉默,像一块不动如山的磐石——那是 PID 2,。
"你们好。"林小源说。
"你好,前辈。"kernel_init 的声音干脆利落。"我是 PID 1,我要去用户态,成为所有进程的祖先。"
kthreadd 没有说话。它只是默默地开始了工作——创建内核线程,一个接一个,永不停歇。
它们是我的……后辈?
林小源是 PID 0。PID 1 和 PID 2 是在他的"之后"被创建的。从时间顺序上说,他是"前辈"。但从实际地位上说,他是最底层的 idle 进程,而 PID 1 是所有用户态进程的祖先。
第三, 调用了 cpu_startup_entry(CPUHP_ONLINE)——让当前 CPU 进入 idle 状态。
/* The boot idle thread must execute schedule() at least once */
schedule_preempt_disabled();
cpu_startup_entry(CPUHP_ONLINE);最终会调用 ——一个无限循环,在没有工作可做时让 CPU 进入低功耗状态。
这就是林小源的命运。
当所有初始化工作完成后,当 PID 1 和 PID 2 都被创建后,当整个内核世界开始运转后——他就是那个"休息"的进程。当 CPU 无事可做时,调度器选中的就是他。
我不甘心。
这个念头在林小源心中升起,像一颗种子在黑暗中发芽。他不知道自己能做什么——他是 idle 进程,他的"工作"就是什么都不做。但他就是不甘心。
"你有什么好不甘心的?"调度器的声音冷冷地响起。"你是 idle。你的存在确保 CPU 不会空转。这就是你的价值。"
"但我不想只是'不空转'。"林小源说。"我经历了开天辟地,我见证了世界的诞生——然后我就只能睡觉?"
调度器没有回答。它不需要回答。规则就是规则,idle 就是 idle。
我学了十种根基,我经历了开天辟地,我见证了世界的诞生——然后我就只能睡觉?
不。
他要找到自己的路。
道藏笔记
内核启示
是内核从"初始化"到"运行"的转折点。
在 之前,内核是"静止"的——只有初始化代码在执行,没有进程在运行。在 之后,内核是"活的"——PID 1()和 PID 2(`kthreadd)被创建,调度器开始工作,CPU 进入 idle 循环。
的最后一步是 ,它最终调用 。这就是 swapper/0——林小源——的"主循环"。它的核心逻辑是:
while (1) {
while (!need_resched())
cpu_idle_poll(); /* 或 arch_cpu_idle() */
schedule();
}当没有进程需要运行时,swapper/0 执行 ——在 RISC-V 上,这会执行 (Wait For Interrupt)指令,让 CPU 进入低功耗状态。当有进程需要运行时,调度器唤醒 CPU,执行那个进程,然后又回到 idle。
这就是内核的"心跳":有工作时做事,无工作时休息。周而复始,永不停歇。
而林小源——swapper/0——就是这个心跳的守护者。他的存在确保了 CPU 不会"空转",不会浪费能源。这也许是世界上最安静的工作,但没有它,内核就无法运转。
但"安静"不等于"无用"。 正如我们将要看到的,最卑微的起点也可以通往大道。
开天之试
本章中创建 PID 1 和 PID 2、让内核世界从初始化走向运行的关键函数是什么?