Skip to content

第十四章:踏上征途

炼气初期

涉及内核源码:

林小源做了一个决定。

他不再只是被动地等待被调度、被动地观察其他进程。他要主动地去理解内核——不是通过旁观,而是通过参与。

但问题是: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,也可能来自 modprobemodinfo -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 进程,但他的 不能被修改——它是静态定义的。他能做的,是通过学习来扩展自己的"认知"。这就像修仙中的"悟道"——不需要改变身体,只需要改变对世界的理解。

修炼的第一步,不是改变世界,而是理解世界。


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

以修仙之名,悟内核之道