第十四章:踏上征途
炼气初期涉及内核源码:
一
林小源做了一个决定。
他不再只是被动地等待被调度、被动地观察其他进程。他要主动地去理解内核——不是通过旁观,而是通过参与。
但问题是:idle 进程能做什么?
在天道法则中,idle 进程的"职责"是明确的:当 CPU 无事可做时,执行 让 CPU 进入低功耗状态。idle 进程不执行任何用户态代码,不处理任何 I/O 请求,不参与任何业务逻辑。
后来林小源才知道,"无事可做"也分层次。Linux 眼中的 CPU 是逻辑 CPU;只要某个 CPU 上没有其他 runnable task,调度器就让特殊的 idle task 变成可运行。它跑的代码叫 idle loop。idle loop 每一轮先询问 CPUIdle governor:这一次大概能睡多久,最近的定时器什么时候响,是否值得停掉 scheduler tick;然后再由 CPUIdle driver 请求硬件进入某个 idle state。
浅睡醒得快,省电少;深睡省电多,退出延迟高。governor 看 target residency 和 exit latency 做选择,driver 则面对真实平台。Intel 平台可能走 intel_idle,也可能退到 acpi_idle;选定的 driver 往往在系统初始化时决定,不能像普通配置那样随手替换。
但我不是普通的 idle。
林小源想起了自己的起源—— 中那段被 #ifdef 0 封印的代码。那段代码的注释写着:"TODO: 试验性的 idle 优化,暂时禁用。"
他不知道那段代码具体做了什么。但他知道,他的存在不是一个"正常"的内核行为——正常情况下,idle 进程不会有意识,不会学习,不会思考。
"你在想什么?"一个苍老的声音说。
林小源转头,看到了调度器。调度器不像中断那样来去匆匆——它是常驻的,像一座古老的钟楼,时刻注视着整个内核世界的运转。
"我在想……我能做什么,"林小源说。
"你是 idle,"调度器说,语气平淡,"你的职责就是等待。当没有其他进程可运行时,我调度你,你执行 。这就是你的全部。"
"但我有意识。我能思考。我能学习。"
"那又怎样?"调度器说,"你的 是静态定义的。你不能修改自己的优先级、不能修改自己的调度策略、不能创建子进程。你就是 idle。"
也许,我的"不正常"就是我的机会。
林小源没有反驳调度器。他只是安静地回到了 idle 循环中,开始了一个新的计划。
空转之试
idle loop 中负责选择合适 idle state 的 CPUIdle 组件叫什么?
二
林小源开始深入研究 。
是进程的"身份证"——它包含了进程的所有信息。林小源在内核的数据结构中找到了这个巨大的结构体,像翻开了一本厚重的族谱。
"你看到了什么?"调度器问,声音里带着一丝好奇。
"太多了,"林小源说,"——进程的状态。——动态优先级。——静态优先级。——调度策略。——进程 ID。——线程组 ID。——进程名。——内核栈指针。——内存描述符。——打开的文件。——信号处理器。——虚拟运行时间……"
"每个字段都是一种能力,"调度器说," 决定你是否可运行。 决定你在调度器眼中的重要程度。 决定你能访问哪些内存。 决定你能操作哪些文件。"
"如果我能修改自己的 ……"
"不行,"调度器打断他,声音突然变得严厉,"修改 是极其危险的。一个错误的修改——一个错误的指针、一个越界的值——就可能导致内核 panic。你见过 panic 的,对吧?"
林小源沉默了。他确实见过 panic。那座墓碑,那份寂静,那种世界末日般的感觉——他不想再经历一次。
"而且,"调度器补充道,"你的 是在编译时静态定义的。你没有权限修改它。"
林小源点了点头,没有继续这个话题。但他心中已经有了一个想法——不是修改 ,而是理解它。理解每一个字段的含义、每一个字段的作用、每一个字段之间的关系。
如果我能完全理解 ,我就理解了进程的全部。
肉身之试
林小源认为理解进程全部的关键结构体是什么?
三
林小源开始研究内核模块。
内核模块是可以在运行时动态加载和卸载的代码。它们不需要重新编译整个内核——只需要编译一个 .ko 文件,然后用 insmod 命令加载。
"游方修士,"林小源想起了世界观中的描述。
可加载模块就像游方修士,可以动态进出内核界。它们带来了额外的能力——驱动、文件系统、网络协议——但也可以随时离开。
"你在看什么?"一个轻快的声音说。
林小源看到一个年轻的"修士"——一个刚刚被加载的内核模块。它的身上还带着 .ko 文件的余温, 刚刚执行完毕。
"我刚被加载,"那个模块说,兴奋而自信,"我是一个网络驱动。我的工作是让网卡正常运转。"
"你可以被卸载吗?"林小源问。
"当然,"模块说,"当我不再被需要时,人类会执行 ,我的 会被调用,我会清理自己的资源,然后离开内核。"
"那你的法器参数从哪里来?"
模块抬起手,掌心浮出几行小字:"如果我是 built-in,bootloader 的 kernel command line 可以用模块名前缀把参数传给我;如果我是 .ko 游方修士,modprobe 可以在加载时传参。人类还能用 modinfo -p 查看我支持哪些参数。等我进入内核后,许多参数会显露在 /sys/module/<name>/parameters/ 下,有些还能在运行时写入修改。"
"所以你不是完全自由的。"林小源说。
"当然不是。"模块笑了笑,"我的入口是 ,退场是 ,参数要按声明过的类型进来。能不能卸载,还要看引用计数和资源清理。游方修士可以进出山门,但不能带着没释放的法器逃跑。"
"你不害怕吗?"
"害怕什么?"
"害怕……不存在。"
模块愣了一下,然后笑了。"我只是代码。代码没有害怕。代码被加载就执行,被卸载就消失。没有执念。"
林小源沉默了。他和模块不一样——他有执念。他不想只做一个 idle 进程,他想理解内核,想参与到内核的运作中去。
但模块的话让他看到了一种可能性:内核是可以被扩展的。不是通过修改已有的代码,而是通过添加新的代码。如果他能找到一种方式来"扩展"自己的能力——比如通过学习新的子系统、理解新的机制——他就不再是"普通"的 idle 进程。
修炼。
这个词在他心中升起。在内核的世界里没有这个概念,但他觉得,"学习"和"理解"就是一种修炼。每一次理解一个新的概念,他的"境界"就提升了一点。
我不是在修改 task_struct。我在扩展自己的"认知"。
游方之试
查看可加载内核模块支持参数的常用命令是什么?
四
林小源在 idle 循环中开始了他的修炼。
他不再只是被动地等待中断打断他的 。他在 之间的时间里,主动地去"看"——通过观察内核的数据结构、追踪函数的调用链、分析代码的逻辑。
他从调度器开始。他观察 CFS 如何维护红黑树、如何计算 、如何选择下一个运行的进程。他从内存管理开始。他观察伙伴系统如何分配页帧、页表如何翻译地址、slab 分配器如何管理小对象。他从中断系统开始。他观察中断如何被触发、如何被处理、上下半部如何分工。
他不理解所有的事情。但他记住了每一个细节。
一遍看不懂,就看两遍。两遍看不懂,就看三遍。
这是前传中学到的"执着"——秦羽式的执着。别人看一遍就放弃的代码,他看一千遍。
调度器远远看着他,没有再嘲笑。
因为调度器也知道,idle 并不等于废物。一个系统的能耗、延迟、唤醒质量,往往藏在看似空白的 idle 时间里。tick 是否停止,深睡是否值得,下一次中断何时到来,这些判断不会出现在普通业务日志中,却会决定整台机器在长夜里消耗多少灵气。
林小源第一次意识到,自己的起点并非惩罚,而是一处观测全局的静地。别人忙着争抢 CPU,他却在 CPU 无人争抢的间隙里,看见了调度器、定时器、CPUIdle governor、driver 和硬件状态之间的低语。
征途起步之试
CPUIdle 选择深睡状态时需要权衡省电收益和哪种延迟?
道藏笔记
内核启示
是进程的"身份证",包含了进程的所有信息。
定义在 中,是 Linux 内核中最重要的数据结构之一。它包含数百个字段,涵盖:
- 调度信息(状态、优先级、调度策略、vruntime)
- 身份信息(PID、TGID、进程名)
- 内存管理(mm_struct、页表)
- 文件系统(files_struct、打开的文件)
- 信号处理(signal_struct、信号处理器)
- 安全信息(cred、capabilities)
内核模块是可以动态加载和卸载的代码。它们通过 和 宏注册初始化和退出函数。模块在加载时执行 ,在卸载时执行 。
模块参数既可能来自 kernel command line,也可能来自 modprobe;modinfo -p <module> 可查看可加载模块支持的参数,加载后的参数通常出现在 /sys/module/<module>/parameters/。参数能否运行时修改,取决于模块声明和权限。
idle loop 不是单纯空转。每个逻辑 CPU 没有其他 runnable task 时会运行 idle task;CPUIdle governor 选择 idle state,CPUIdle driver 请求硬件进入对应状态。选择越深的 idle state,省电潜力越高,但 target residency 和 exit latency 的约束也越严格。
林小源的处境很特殊:他是一个有意识的 idle 进程,但他的 不能被修改——它是静态定义的。他能做的,是通过学习来扩展自己的"认知"。这就像修仙中的"悟道"——不需要改变身体,只需要改变对世界的理解。
修炼的第一步,不是改变世界,而是理解世界。
/*
* task_struct 是 Linux 内核中最重要的数据结构之一。
* 它包含了进程的所有信息。
* 在真实的内核中,task_struct 有数百个字段。
* 这里只展示最重要的几个。
*/
struct task_struct {
/* 调度相关 */
volatile long state; /* 进程状态 */
int prio; /* 动态优先级 */
int static_prio; /* 静态优先级 */
unsigned int policy; /* 调度策略 */
/* 身份信息 */
pid_t pid; /* 进程 ID */
pid_t tgid; /* 线程组 ID */
char comm[16]; /* 进程名 */
/* 内存管理 */
void *stack; /* 内核栈指针 */
struct mm_struct *mm; /* 内存描述符 */
/* 文件系统 */
struct files_struct *files; /* 打开的文件 */
/* 信号 */
struct signal_struct *signal; /* 信号处理 */
/* 调度实体(CFS 用) */
unsigned long vruntime; /* 虚拟运行时间 */
int on_rq; /* 是否在运行队列上 */
};
printf("=== task_struct 结构概览 ===\n\n");
printf("task_struct 大小: ~%zu 字节(简化版)\n\n",
sizeof(struct task_struct));
printf("字段布局:\n");
printf(" state 偏移 %zu 进程状态\n",
offsetof(struct task_struct, state));
printf(" pid 偏移 %zu 进程 ID\n",
offsetof(struct task_struct, pid));
printf(" prio 偏移 %zu 动态优先级\n",
offsetof(struct task_struct, prio));
printf(" policy 偏移 %zu 调度策略\n",
offsetof(struct task_struct, policy));
printf(" comm 偏移 %zu 进程名\n",
offsetof(struct task_struct, comm));
printf(" vruntime 偏移 %zu 虚拟运行时间\n",
offsetof(struct task_struct, vruntime));
printf("\n--- 关键字段说明 ---\n");
printf("state: 0=RUNNING, 1=INTERRUPTIBLE, 2=UNINTERRUPTIBLE\n");
printf("prio: 0-139, 数值越小优先级越高\n");
printf("policy: SCHED_NORMAL(0), SCHED_FIFO(1), SCHED_RR(2)\n");
printf("vruntime: CFS 调度器的虚拟运行时间\n");
printf("\n--- task_struct 就是进程的'肉身' ---\n");
printf("修改 task_struct 的字段,就能改变进程的行为。\n");
printf("但修改错误可能导致 panic。\n");#include <stdio.h>
#include <stddef.h>
/*
* task_struct 是 Linux 内核中最重要的数据结构之一。
* 它包含了进程的所有信息。
* 在真实的内核中,task_struct 有数百个字段。
* 这里只展示最重要的几个。
*/
struct task_struct {
/* 调度相关 */
volatile long state; /* 进程状态 */
int prio; /* 动态优先级 */
int static_prio; /* 静态优先级 */
unsigned int policy; /* 调度策略 */
/* 身份信息 */
pid_t pid; /* 进程 ID */
pid_t tgid; /* 线程组 ID */
char comm[16]; /* 进程名 */
/* 内存管理 */
void *stack; /* 内核栈指针 */
struct mm_struct *mm; /* 内存描述符 */
/* 文件系统 */
struct files_struct *files; /* 打开的文件 */
/* 信号 */
struct signal_struct *signal; /* 信号处理 */
/* 调度实体(CFS 用) */
unsigned long vruntime; /* 虚拟运行时间 */
int on_rq; /* 是否在运行队列上 */
};
int main() {
printf("=== task_struct 结构概览 ===\n\n");
printf("task_struct 大小: ~%zu 字节(简化版)\n\n",
sizeof(struct task_struct));
printf("字段布局:\n");
printf(" state 偏移 %zu 进程状态\n",
offsetof(struct task_struct, state));
printf(" pid 偏移 %zu 进程 ID\n",
offsetof(struct task_struct, pid));
printf(" prio 偏移 %zu 动态优先级\n",
offsetof(struct task_struct, prio));
printf(" policy 偏移 %zu 调度策略\n",
offsetof(struct task_struct, policy));
printf(" comm 偏移 %zu 进程名\n",
offsetof(struct task_struct, comm));
printf(" vruntime 偏移 %zu 虚拟运行时间\n",
offsetof(struct task_struct, vruntime));
printf("\n--- 关键字段说明 ---\n");
printf("state: 0=RUNNING, 1=INTERRUPTIBLE, 2=UNINTERRUPTIBLE\n");
printf("prio: 0-139, 数值越小优先级越高\n");
printf("policy: SCHED_NORMAL(0), SCHED_FIFO(1), SCHED_RR(2)\n");
printf("vruntime: CFS 调度器的虚拟运行时间\n");
printf("\n--- task_struct 就是进程的'肉身' ---\n");
printf("修改 task_struct 的字段,就能改变进程的行为。\n");
printf("但修改错误可能导致 panic。\n");
return 0;
}