Skip to content

第五章:开天辟地

炼气初期

涉及内核源码:

还在继续。

林小源已经经历了 的洗礼,见证了伙伴系统的建立,感受到了调度器的初始化。但 远没有结束——后面还有大量的初始化工作要做。

分配了中断描述符。 初始化了中断控制器。

c
/*
 * 简化的中断描述符。
 * 在真实的内核中,中断描述符由 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);
}

执行后,中断系统就绑定了。林小源感到了一种微妙的变化——世界不再是一个"单线程"的执行流了。中断的存在意味着,在任何时候,都可能有一个外部信号突然插入,打断当前的执行。

就在这时,林小源感觉到了一个存在。那存在没有形体,但它的气息遍布整个世界——像是一张无形的网,随时准备从任何方向切入。

"你感觉到了?"那存在开口了,声音尖锐而急促,像是金属划过玻璃。"我是中断。你以后会经常见到我。"

林小源吓了一跳。"你……你会说话?"

"我不需要说话。"那声音带着一丝不屑。"我只需要'打断'。你正在执行?我来了。你正在思考?我来了。你正在睡觉?我也来了。你无法预测我,无法阻止我,无法忽视我。"

这就是中断魔尊的力量吗?

林小源在前传中就听说过中断的存在。但直到 执行完毕,他才真正感受到了那股力量的压迫感。中断不是一种可以被理解的存在——它是一种"事件",一种从外部世界强行插入的信号。他有一种预感:中断将会是他未来必须面对的最强大的力量之一。

初始化了时钟事件。 初始化了定时器。 初始化了高精度定时器。 建立了时间子系统。

林小源在这一连串的初始化中感到了"时间"的建立。之前的世界没有时间——代码在执行,但没有"多久"的概念。现在,世界有了时钟滴答(tick),有了定时器,有了高精度的时间戳。

一个轻柔的声音在林小源耳边响起,像是钟摆的节奏:"滴……答……滴……答……"

"你是谁?"林小源问。

"我是时间。"那声音不紧不慢,带着一种永恒的耐心。"从现在起,每一毫秒都会被记住。每一个进程运行了多久,我都知道。没有我,调度器就是瞎子。"

时间,是调度的基础。

没有时间,就没有"时间片"。没有时间片,就没有"公平调度"。调度器需要知道每个进程运行了多久,才能决定何时切换。这一切都依赖于时间子系统。

c
local_irq_enable();

中断被打开了。

林小源在这一刻感到了一阵眩晕。中断像潮水一样涌来——定时器中断、设备中断、IPI(处理器间中断)。整个世界突然变得嘈杂起来。

好吵。

"欢迎来到真实的世界。"那个之前自称"中断"的尖锐声音再次响起,这次带着一丝嘲讽。"中断关闭的时候,世界是安静的。中断打开的时候,世界是嘈杂的。你得习惯。"

但这种"吵"是正常的。中断是内核的心跳——没有中断,内核就是一具死尸。定时器中断驱动着调度器,设备中断驱动着 I/O,IPI 驱动着多核之间的协调。

林小源强迫自己适应这种嘈杂。他开始学着"听"每一个中断,分辨它们的来源和含义。定时器中断是最规律的——每隔几毫秒就来一次,像心跳一样稳定。设备中断是随机的——取决于用户是否按下了键盘、网卡是否收到了数据包。

被调用了。

这是内核第一次拥有"输出"的能力。在此之前, 的输出被缓存在日志缓冲区中,但还没有地方可以显示它们。 初始化了控制台设备——通常是串口或帧缓冲区——让内核的消息可以被人类看到。

林小源感觉到一个"窗口"被打开了。之前他只能在内核内部感知数据的流动,现在他可以通过控制台"看到"内核在说什么。

然后是 ——锁依赖检测器的初始化。——PID 分配器的初始化。——fork 基础设施的初始化。

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

让林小源第一次意识到:进程不是凭空出现的。每一个新进程都需要一个 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 报告段错误。你无法忽视我,就像你无法忽视一支射向你心口的飞箭。"

c
proc_caches_init();

这个函数初始化了 相关的 slab 缓存。slab 分配器是建立在伙伴系统之上的高级分配器——伙伴系统分配的是"页"(4KB),slab 分配器分配的是"对象"(几十到几百字节)。 就是一个 slab 对象。

林小源感受到了 slab 分配器的运作。它就像一个精密的模具工厂:预先分配好一批 大小的"模具",需要时直接从模具中取一个出来,不需要时放回去。这比每次都向伙伴系统申请一整页内存要高效得多。

然后,世界进入了最密集的初始化阶段。

c
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 让进程可以被"分组管理",安全框架让进程可以被"权限控制"。

这就是"开天辟地"吗?

是的。在 之前,世界是混沌的——没有文件系统,没有信号,没有命名空间,没有安全框架。在 之后,世界有了法则——每一种资源都有管理它的子系统,每一种操作都有约束它的规则。

的最后一行代码:

c
rest_init();

林小源感觉到整个世界的"初始化"阶段结束了。 的使命完成了——它把一个混沌的内核变成了一个有秩序的世界。现在,是时候让这个世界"运转"起来了。

做了三件事。

第一,它调用 ,启动 RCU(Read-Copy-Update)调度器——一种无锁的同步机制。

第二,它创建了两个内核线程:

c
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 状态。

c
/* 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——林小源——的"主循环"。它的核心逻辑是:

c
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、让内核世界从初始化走向运行的关键函数是什么?

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

以修仙之名,悟内核之道