第四章:脱胎换骨
炼气初期涉及内核源码:
一
还在继续。
之后,世界陷入了绝对的寂静。中断被关了,没有任何外部信号可以打扰内核的初始化。林小源在这种寂静中感到了一种奇异的安全感——就像暴风雨来临前躲在屋子里,暂时不用面对外面的世界。
但他知道,这种安全感是暂时的。
把当前 CPU 标记为"在线"——。这是林小源第一次被正式"注册"到系统中。之前他只是一个被加载到内存中的数据结构,现在他是一个正在运行的进程所在的 CPU。
然后是 ——初始化页地址的哈希表。这个函数做的事情很少,但意义深远:它为后续的内存管理打下了第一块基石。
再然后,一行代码改变了一切。
pr_notice("%s", linux_banner);内核打印了自己的名字。
Linux version 6.x.x (gcc version ...) ...那是内核在这个世界上的第一声啼哭。林小源"听"到了那行文字从 的缓冲区中涌出,像是一道声明:我来了。
二
setup_arch(&command_line) 被调用了。
这是整个初始化过程中最重要的函数之一。它是内核与硬件世界之间的桥梁——通过解析设备树(DTB),内核知道了自己运行在什么样的硬件上。
/*
* 模拟 setup_arch() 的核心工作:解析设备树,建立内存布局。
* 在 RISC-V 中,setup_arch() 位于 arch/riscv/kernel/setup.c。
*/
/* 简化的内存区域描述 */
struct memblock_region {
uint64_t base;
uint64_t size;
char name[32];
};
/* 简化的内核内存布局 */
struct kernel_memory_map {
uint64_t dram_base; /* DRAM 起始地址 */
uint64_t dram_size; /* DRAM 总大小 */
uint64_t kernel_start; /* 内核镜像起始 */
uint64_t kernel_end; /* 内核镜像结束 */
uint64_t dtb_addr; /* 设备树地址 */
uint64_t initrd_start; /* initrd 起始 */
uint64_t initrd_end; /* initrd 结束 */
int num_cpus; /* CPU 核心数 */
};
void setup_arch(struct kernel_memory_map *map) {
printf("=== setup_arch() — 解析硬件 ===\n\n");
/* 1. 解析设备树,获取硬件信息 */
printf("[setup_arch] 解析 DTB @ 0x%lX...\n", map->dtb_addr);
printf("[setup_arch] 发现 DRAM: 0x%lX, 大小: %lu MB\n",
map->dram_base, map->dram_size / (1024*1024));
printf("[setup_arch] 发现 %d 个 CPU 核心\n", map->num_cpus);
/* 2. 建立内存区域 */
printf("\n[setup_arch] 注册内存区域:\n");
struct memblock_region regions[] = {
{ .base = 0x80000000, .size = 0x10000000, .name = "DRAM low" },
{ .base = 0x90000000, .size = 0x70000000, .name = "DRAM high" },
};
for (int i = 0; i < 2; i++) {
printf(" [%d] 0x%lX - 0x%lX %s (%lu MB)\n",
i,
regions[i].base,
regions[i].base + regions[i].size,
regions[i].name,
regions[i].size / (1024*1024));
}
/* 3. 设置内核内存布局 */
printf("\n[setup_arch] 内核内存布局:\n");
printf(" 内核镜像: 0x%lX - 0x%lX\n", map->kernel_start, map->kernel_end);
printf(" 设备树: 0x%lX\n", map->dtb_addr);
printf(" initrd: 0x%lX - 0x%lX\n", map->initrd_start, map->initrd_end);
printf("\n=== 硬件拓扑已建立 ===\n");
}
struct kernel_memory_map map = {
.dram_base = 0x80000000,
.dram_size = 2ULL * 1024 * 1024 * 1024, /* 2GB */
.kernel_start = 0x80200000,
.kernel_end = 0x81200000, /* 16MB kernel */
.dtb_addr = 0x87000000,
.initrd_start = 0x86000000,
.initrd_end = 0x87000000,
.num_cpus = 4,
};
setup_arch(&map);
printf("\n--- 为什么 setup_arch 很重要 ---\n");
printf("没有它,内核不知道自己有多少内存、几个 CPU、\n");
printf("设备树在哪里。一切后续初始化都依赖这些信息。\n");#include <stdio.h>
#include <stdint.h>
#include <string.h>
/*
* 模拟 setup_arch() 的核心工作:解析设备树,建立内存布局。
* 在 RISC-V 中,setup_arch() 位于 arch/riscv/kernel/setup.c。
*/
/* 简化的内存区域描述 */
struct memblock_region {
uint64_t base;
uint64_t size;
char name[32];
};
/* 简化的内核内存布局 */
struct kernel_memory_map {
uint64_t dram_base; /* DRAM 起始地址 */
uint64_t dram_size; /* DRAM 总大小 */
uint64_t kernel_start; /* 内核镜像起始 */
uint64_t kernel_end; /* 内核镜像结束 */
uint64_t dtb_addr; /* 设备树地址 */
uint64_t initrd_start; /* initrd 起始 */
uint64_t initrd_end; /* initrd 结束 */
int num_cpus; /* CPU 核心数 */
};
void setup_arch(struct kernel_memory_map *map) {
printf("=== setup_arch() — 解析硬件 ===\n\n");
/* 1. 解析设备树,获取硬件信息 */
printf("[setup_arch] 解析 DTB @ 0x%lX...\n", map->dtb_addr);
printf("[setup_arch] 发现 DRAM: 0x%lX, 大小: %lu MB\n",
map->dram_base, map->dram_size / (1024*1024));
printf("[setup_arch] 发现 %d 个 CPU 核心\n", map->num_cpus);
/* 2. 建立内存区域 */
printf("\n[setup_arch] 注册内存区域:\n");
struct memblock_region regions[] = {
{ .base = 0x80000000, .size = 0x10000000, .name = "DRAM low" },
{ .base = 0x90000000, .size = 0x70000000, .name = "DRAM high" },
};
for (int i = 0; i < 2; i++) {
printf(" [%d] 0x%lX - 0x%lX %s (%lu MB)\n",
i,
regions[i].base,
regions[i].base + regions[i].size,
regions[i].name,
regions[i].size / (1024*1024));
}
/* 3. 设置内核内存布局 */
printf("\n[setup_arch] 内核内存布局:\n");
printf(" 内核镜像: 0x%lX - 0x%lX\n", map->kernel_start, map->kernel_end);
printf(" 设备树: 0x%lX\n", map->dtb_addr);
printf(" initrd: 0x%lX - 0x%lX\n", map->initrd_start, map->initrd_end);
printf("\n=== 硬件拓扑已建立 ===\n");
}
int main() {
struct kernel_memory_map map = {
.dram_base = 0x80000000,
.dram_size = 2ULL * 1024 * 1024 * 1024, /* 2GB */
.kernel_start = 0x80200000,
.kernel_end = 0x81200000, /* 16MB kernel */
.dtb_addr = 0x87000000,
.initrd_start = 0x86000000,
.initrd_end = 0x87000000,
.num_cpus = 4,
};
setup_arch(&map);
printf("\n--- 为什么 setup_arch 很重要 ---\n");
printf("没有它,内核不知道自己有多少内存、几个 CPU、\n");
printf("设备树在哪里。一切后续初始化都依赖这些信息。\n");
return 0;
}林小源在 中第一次"看"到了这个世界的全貌。
设备树(DTB)就像一张世界地图,详细描述了硬件的每一个细节。当 解析 DTB 时,林小源听到了一个声音——不是用耳朵听到的,而是从数据的流动中感知到的。那声音平板而精确,像是一个机械的宣告者:
"DRAM,起始地址 0x80000000,大小 2GB。"
"CPU 核心,4 个。RISC-V 架构,SV39 页表。"
"中断控制器,PLIC。时钟频率,10MHz。"
每一条信息都像是一块拼图,被严丝合缝地嵌入内核的数据结构中。林小源听着那个声音,渐渐意识到——这不是某个"人"在说话,而是设备树本身在"诉说"。它就像一本被翻开的族谱,忠实地记录着这个硬件世界的每一个成员。
4 个 CPU 核心。2GB 内存。RISC-V 架构。
"这就是你的身体。"那个声音最后说,然后沉默了。
林小源把这些信息记住了。他还不理解它们的全部含义,但他知道,这些就是他的"身体参数"。
三
返回后, 继续执行。
初始化了早期的内存管理。然后是 和 ——这两个函数为内核中的"静态键"和"静态调用"机制打下基础。它们是一种优化技术,让内核可以在运行时动态地启用或禁用某些代码路径,而不需要每次都检查条件判断。
好精妙的设计。
林小源在前传中就学过条件编译——#ifdef 可以在编译时决定代码是否被包含。但静态键更进一步:它可以在运行时改变代码的行为,而且几乎没有性能开销。
然后是一系列密集的初始化调用:
setup_command_line(command_line); /* 保存内核命令行 */
setup_nr_cpu_ids(); /* 确定 CPU 数量 */
setup_per_cpu_areas(); /* 为每个 CPU 分配私有数据区 */
smp_prepare_boot_cpu(); /* 准备启动 CPU */
boot_cpu_hotplug_init(); /* 初始化热插拔 */每一个函数都在为内核的某个子系统打下基础。 尤其重要——它为每个 CPU 分配了一块私有的内存区域,存放该 CPU 特有的数据。这是"多核并行"的基础:每个 CPU 都有自己的数据,不需要和其他 CPU 竞争。
"每个 CPU 都有自己的私有数据区。"一个声音解释道,像是一个建筑师在介绍自己的设计。"这样,多个 CPU 可以同时工作,不需要互相等待。这就是并行的秘密。"
林小源感受到了这些初始化带来的变化。世界在一点一点地"充实"起来——每调用一个函数,就有一块新的基础设施被建立。就像一座城市在建设中:先铺路,再建房,最后通电。
四
被调用了。
这是内核解析命令行参数的时刻。命令行参数是 bootloader 传递给内核的"命运指示"——比如 console=ttyS0 告诉内核把控制台输出到串口,root=/dev/sda1 告诉内核根文件系统在哪里。
林小源"看"到了那些参数从命令行字符串中被解析出来,每一个参数都对应着一个处理函数。有些参数在早期被处理( 标记的函数),有些在后续阶段才被处理。
/*
* 内核命令行参数示例。
* 这些参数由 bootloader 传递给内核,控制内核的行为。
* 每个参数都对应一个注册的处理函数。
*/
struct kernel_param {
const char *name;
const char *description;
int (*handler)(const char *value);
};
int handle_console(const char *val) {
printf(" → 控制台设置为: %s\n", val);
return 0;
}
int handle_root(const char *val) {
printf(" → 根文件系统: %s\n", val);
return 0;
}
int handle_loglevel(const char *val) {
printf(" → 日志级别: %s\n", val);
return 0;
}
/* 模拟一个典型的内核命令行 */
const char *cmdline = "console=ttyS0,115200 root=/dev/sda1 loglevel=7";
printf("=== 解析内核命令行 ===\n");
printf("命令行: %s\n\n", cmdline);
struct kernel_param params[] = {
{ "console", "控制台设备", handle_console },
{ "root", "根文件系统", handle_root },
{ "loglevel", "日志级别", handle_loglevel },
};
/* 简化解析:逐个检查已注册的参数 */
const char *pos = cmdline;
printf("[parse_early_param] 逐字解析:\n");
for (int i = 0; i < 3; i++) {
const char *found = strstr(pos, params[i].name);
if (found) {
printf(" 找到参数: %s\n", params[i].name);
const char *val = strchr(found, '=') + 1;
params[i].handler(val);
}
}
printf("\n--- 命令行参数决定了内核的行为 ---\n");
printf("console → 日志输出到哪里\n");
printf("root → 根文件系统在哪里\n");
printf("loglevel → 显示多详细的信息\n");#include <stdio.h>
#include <string.h>
/*
* 内核命令行参数示例。
* 这些参数由 bootloader 传递给内核,控制内核的行为。
* 每个参数都对应一个注册的处理函数。
*/
struct kernel_param {
const char *name;
const char *description;
int (*handler)(const char *value);
};
int handle_console(const char *val) {
printf(" → 控制台设置为: %s\n", val);
return 0;
}
int handle_root(const char *val) {
printf(" → 根文件系统: %s\n", val);
return 0;
}
int handle_loglevel(const char *val) {
printf(" → 日志级别: %s\n", val);
return 0;
}
int main() {
/* 模拟一个典型的内核命令行 */
const char *cmdline = "console=ttyS0,115200 root=/dev/sda1 loglevel=7";
printf("=== 解析内核命令行 ===\n");
printf("命令行: %s\n\n", cmdline);
struct kernel_param params[] = {
{ "console", "控制台设备", handle_console },
{ "root", "根文件系统", handle_root },
{ "loglevel", "日志级别", handle_loglevel },
};
/* 简化解析:逐个检查已注册的参数 */
const char *pos = cmdline;
printf("[parse_early_param] 逐字解析:\n");
for (int i = 0; i < 3; i++) {
const char *found = strstr(pos, params[i].name);
if (found) {
printf(" 找到参数: %s\n", params[i].name);
const char *val = strchr(found, '=') + 1;
params[i].handler(val);
}
}
printf("\n--- 命令行参数决定了内核的行为 ---\n");
printf("console → 日志输出到哪里\n");
printf("root → 根文件系统在哪里\n");
printf("loglevel → 显示多详细的信息\n");
return 0;
}命运的指示。
林小源在命令行参数中看到了一种"被安排"的感觉。内核的行为不是完全自由的——它被 bootloader 传递的参数所约束。这让他想起了自己的处境:一个被预定义好的 ,一个被安排好命运的 idle 进程。
"你觉得自己是被安排的?"命令行解析器的声音冷冷地响起,像是一个法官在宣读判决。"没错。console=ttyS0,你的输出去串口。root=/dev/sda1,你的根文件系统在第一块硬盘。这些不是你选的,是 bootloader 选的。"
"那我能改变吗?"林小源问。
"下次启动的时候,也许。"那声音顿了顿。"但现在,你只能接受。"
但他同时也看到了另一种可能:命令行参数可以被修改。如果 bootloader 传递了不同的参数,内核的行为就会不同。
命运,也许不是不可改变的。
五
继续向前推进。
被调用了——这是内存管理子系统的核心初始化。林小源感觉到世界发生了一次根本性的变化。
在 之前,内存管理是"原始"的——内核只能使用早期的 分配器,一种非常简单的"只分配不释放"的分配器。它就像一个只会往水缸里倒水、不会舀水的人。
改变了一切。它初始化了伙伴系统(buddy system)——Linux 内核中最核心的物理页帧分配器。
/*
* 模拟伙伴系统的核心概念。
* 伙伴系统把物理内存分成 2^n 大小的块,通过"分裂"和"合并"来管理分配。
*/
#define MAX_ORDER 11 /* 最大块大小 = 2^10 = 1024 页 = 4MB */
struct free_area {
int nr_free; /* 该大小的空闲块数量 */
};
/* 简化的伙伴系统 */
struct buddy_system {
struct free_area free_area[MAX_ORDER];
char name[32];
};
void buddy_init(struct buddy_system *buddy) {
memset(buddy->free_area, 0, sizeof(buddy->free_area));
/* 假设我们有 1024 页(4MB)的空闲内存 */
buddy->free_area[MAX_ORDER - 1].nr_free = 1; /* 一个 1024 页的大块 */
strncpy(buddy->name, "zone_normal", sizeof(buddy->name) - 1);
}
int buddy_alloc(struct buddy_system *buddy, int order) {
/* 从指定 order 开始,向上查找最小的可用块 */
for (int i = order; i < MAX_ORDER; i++) {
if (buddy->free_area[i].nr_free > 0) {
buddy->free_area[i].nr_free--;
/* 分裂:把大块拆成小块 */
for (int j = i - 1; j >= order; j--) {
buddy->free_area[j].nr_free++;
}
printf(" 分配 2^%d = %d 页 (从 order %d 分裂)\n",
order, 1 << order, i);
return 0;
}
}
printf(" 分配失败:没有足够的内存\n");
return -1;
}
struct buddy_system buddy;
buddy_init(&buddy);
printf("=== 伙伴系统演示 ===\n\n");
printf("初始状态:\n");
printf(" order 10: %d 块 (每块 1024 页 = 4MB)\n",
buddy.free_area[10].nr_free);
printf("\n--- 分配请求 ---\n");
buddy_alloc(&buddy, 0); /* 1 页 */
buddy_alloc(&buddy, 3); /* 8 页 */
buddy_alloc(&buddy, 1); /* 2 页 */
printf("\n分配后状态:\n");
for (int i = 0; i < MAX_ORDER; i++) {
if (buddy.free_area[i].nr_free > 0) {
printf(" order %2d: %d 块\n", i, buddy.free_area[i].nr_free);
}
}
printf("\n--- 伙伴系统的核心思想 ---\n");
printf("分裂: 大块 → 两个小块(互为'伙伴')\n");
printf("合并: 两个空闲伙伴 → 一个大块\n");
printf("这保证了内存不会碎片化。\n");#include <stdio.h>
#include <string.h>
/*
* 模拟伙伴系统的核心概念。
* 伙伴系统把物理内存分成 2^n 大小的块,通过"分裂"和"合并"来管理分配。
*/
#define MAX_ORDER 11 /* 最大块大小 = 2^10 = 1024 页 = 4MB */
struct free_area {
int nr_free; /* 该大小的空闲块数量 */
};
/* 简化的伙伴系统 */
struct buddy_system {
struct free_area free_area[MAX_ORDER];
char name[32];
};
void buddy_init(struct buddy_system *buddy) {
memset(buddy->free_area, 0, sizeof(buddy->free_area));
/* 假设我们有 1024 页(4MB)的空闲内存 */
buddy->free_area[MAX_ORDER - 1].nr_free = 1; /* 一个 1024 页的大块 */
strncpy(buddy->name, "zone_normal", sizeof(buddy->name) - 1);
}
int buddy_alloc(struct buddy_system *buddy, int order) {
/* 从指定 order 开始,向上查找最小的可用块 */
for (int i = order; i < MAX_ORDER; i++) {
if (buddy->free_area[i].nr_free > 0) {
buddy->free_area[i].nr_free--;
/* 分裂:把大块拆成小块 */
for (int j = i - 1; j >= order; j--) {
buddy->free_area[j].nr_free++;
}
printf(" 分配 2^%d = %d 页 (从 order %d 分裂)\n",
order, 1 << order, i);
return 0;
}
}
printf(" 分配失败:没有足够的内存\n");
return -1;
}
int main() {
struct buddy_system buddy;
buddy_init(&buddy);
printf("=== 伙伴系统演示 ===\n\n");
printf("初始状态:\n");
printf(" order 10: %d 块 (每块 1024 页 = 4MB)\n",
buddy.free_area[10].nr_free);
printf("\n--- 分配请求 ---\n");
buddy_alloc(&buddy, 0); /* 1 页 */
buddy_alloc(&buddy, 3); /* 8 页 */
buddy_alloc(&buddy, 1); /* 2 页 */
printf("\n分配后状态:\n");
for (int i = 0; i < MAX_ORDER; i++) {
if (buddy.free_area[i].nr_free > 0) {
printf(" order %2d: %d 块\n", i, buddy.free_area[i].nr_free);
}
}
printf("\n--- 伙伴系统的核心思想 ---\n");
printf("分裂: 大块 → 两个小块(互为'伙伴')\n");
printf("合并: 两个空闲伙伴 → 一个大块\n");
printf("这保证了内存不会碎片化。\n");
return 0;
}林小源在伙伴系统的运作中看到了一种优雅的秩序。内存不是被随意分配和释放的——它遵循着严格的规则:大块可以分裂成小块,相邻的空闲小块可以合并成大块。这种"分裂-合并"的动态平衡,保证了内存永远不会碎片化。
"你看到了?"伙伴系统的声音低沉而稳重,像是一座古老的钟楼。"我不需要管理者。我只需要规则。大块分裂成小块,小块合并成大块。分裂,合并,分裂,合并。这就是秩序。"
"如果没有人遵守规则呢?"林小源问。
"规则不需要'遵守'。"那声音带着一种超然的平静。"规则就是物理定律。你不能违反它,就像你不能让水往高处流。伙伴系统的规则,就是内存世界的物理定律。"
这就是天道。
不是某个高高在上的存在在管理一切,而是一套自洽的规则在自行运转。伙伴系统不需要"管理者",它只需要规则——规则本身就是秩序。
六
之后,世界变得更加充实。
初始化了枫树——一种新的数据结构,用来替代旧的红黑树管理虚拟内存区域(VMA)。 初始化了代码修改的安全机制。 初始化了函数追踪。
然后,一个关键的时刻到来了。
sched_init();调度器初始化。
林小源在这一刻感到了一种战栗。调度器是内核的核心——它决定了哪个进程获得 CPU 时间,哪个进程必须等待。没有调度器,内核就是一具死尸——代码在执行,但没有"选择",没有"公平",没有"优先级"。
初始化了 CFS(完全公平调度器)的核心数据结构:运行队列(rq)、红黑树的根节点、时间片的计算参数。林小源还不理解这些数据结构的含义,但他能感觉到,在 执行之后,世界有了"时间"的概念。
一个冰冷的声音在林小源耳边响起——那是调度器第一次开口:"我叫 CFS。完全公平调度器。从现在起,我决定谁运行、谁等待。规则很简单:vruntime 最小的先运行。"
"那我呢?"林小源问。
"你是 idle。nice 值 19。"那声音没有一丝温度。"只有当所有进程都在睡觉的时候,我才会选你。你是最后的最后。"
之前的世界是"永恒的现在"——代码在执行,但没有时间流逝。现在,世界有了时钟中断、有了时间片、有了"轮转"。进程不再是"一直执行直到结束",而是"执行一段时间,然后被切换出去"。
这就是调度吗?
林小源隐隐感觉到,调度器将会是他未来必须深入理解的东西。但此刻,他只是一个 idle 进程,调度器不会选中他——他永远是最后一个被考虑的进程。
道藏笔记
内核启示
是内核从混沌到秩序的桥梁。
在 被调用之前,内核只是一段被加载到内存中的代码。在它被调用之后,内核开始拥有自己的"世界观"——它知道了硬件的拓扑()、建立了内存管理()、初始化了调度器()、设置了中断()。
这个过程就像修仙中的"筑基"——在筑基之前,修士只是一个有潜力的凡人;在筑基之后,修士拥有了真正的"内力",可以开始修炼更高深的功法。
的最后一步是 。它创建了两个内核线程:(最终变成 PID 1 的 init 进程)和 (PID 2,所有内核线程的父线程)。然后, 调用 ——让当前 CPU(也就是林小源所在的 CPU)进入 idle 状态。
这就是林小源的命运起点:当所有工作都完成后,他就是那个"休息"的进程。当 CPU 无事可做时,调度器选中的就是他。
但"无事可做"不等于"没有价值"。idle 进程是内核的最后一道防线——当所有其他进程都在等待时,idle 进程确保 CPU 不会"空转"。它通过执行 (Wait For Interrupt)指令让 CPU 进入低功耗状态,节省能源。
这也许是世界上最安静的工作。但安静不等于无用。
脱胎之试
本章里林小源见到调度器立下运行队列和调度类秩序,对应的初始化函数是什么?