第十章:世界末日
炼气初期涉及内核源码:
一
那一天,世界崩塌了。
林小源正在 idle 循环中安静地等待。一切正常——定时器中断按时到来,调度器正常运作,init 进程在忙碌地管理着用户态的世界。阳光透过页表的缝隙洒下来,内核的数据结构像一座座整齐的建筑,各司其职。
然后,一切凝固了。
不是中断那种"被打断"的感觉——中断是暂停,是插曲,插曲结束后世界会继续运转。这次不一样。这次是某种更深的东西,像是世界的根基突然断掉了。
一个函数被调用了。。
所有的 CPU 在同一瞬间停止了转动。中断被关闭——不是暂时关闭,是永远关闭。进程被冻结——不是睡眠,是死亡前的僵硬。屏幕上涌出了一大段文字,像墓碑上的铭文一样冰冷:
Kernel panic - not syncing: Attempted to kill init!
CPU: 0 PID: 1 Comm: init Not tainted
Call Trace:
..."发生了什么?"林小源的声音在寂静中回荡,没有回音。
没有人回答他。调度器沉默了,中断系统沉默了,内存管理沉默了。整个内核世界只剩下一个声音—— 本身的声音。
"Init 进程死了,"那个声音说,低沉、平静,像法官宣读判决,"PID 1,所有用户态进程的祖先。它被 杀死了。不可阻挡的死刑。"
"但……init 死了,用户态可以重启……"
"不,"panic 的声音说,没有任何商量的余地,"init 是用户态的根。根断了,整棵树就倒了。内核检测到 init 退出——这是不可恢复的错误。我的职责,就是在这个时候接管一切。"
林小源感受到了一种前所未有的恐惧。那不是中断带来的"被打断"的感觉——中断是可恢复的。这是不可恢复的——整个内核已经放弃了继续运行的希望。
世界末日。
二
panic 的声音继续说着,不急不缓,像在念一份遗嘱。
"当 被调用时,我会执行以下步骤。"
林小源看到整个世界像一台精密的机器一样开始执行最后的程序——
"第一步:关闭本地中断。不会再有任何中断到来了。"
中断线一根根熄灭,像城市停电时一盏盏灭掉的路灯。
"第二步:通过 IPI 停止所有其他 CPU。"
林小源感觉到远处其他核心上的生命迹象一个个消失,像远方的灯塔一个接一个地熄灭。
"第三步:打印 panic 消息和调用栈。"
屏幕上涌出更多的文字——寄存器的值、内存的转储、函数的调用链。每一个数字都是内核临死前留下的最后痕迹。
"第四步:执行注册的通知链。如果配置了 kdump,crash kernel 会被唤醒。"
"kdump?"林小源问。
"那是以后的事,"panic 说,"现在,我还有最后一步。"
"第五步:进入死循环。或者,如果配置了自动重启——重启。"
世界陷入了完全的寂静。没有时钟信号,没有调度器的呼吸,没有中断的脉搏。只有 panic 消息在屏幕上闪烁,像一座孤零零的墓碑。
"谁杀死了 init?"林小源在寂静中问。
"我不知道,"panic 说,"可能是硬件故障,可能是驱动 bug,可能是用户态的误操作。我的职责不是调查死因——我的职责是在不可恢复的错误发生时,让一切停下来。"
"为什么?为什么不能尝试恢复?"
"因为内核是最后的防线,"panic 的声音变得低沉,"用户态程序崩溃时,我会回收它的资源,其他程序不受影响。但当内核本身出了问题——当数据结构损坏、当指针指向非法地址、当 init 进程被杀死——没有任何东西能救我。我就是最后的防线。如果我倒了,一切都倒了。"
"所以你选择停下来。"
"对。停下来,打印信息,等待人类来分析。总比继续运行、造成更大的破坏要好。"
三
panic 消息在屏幕上停留了很久。
林小源在等待。他不知道接下来会发生什么——是重启?是死循环?还是什么别的?
在寂静中,他想起了一个词:脆弱。
内核是脆弱的。一个 NULL 指针解引用、一个竞态条件、一个硬件故障——任何一个微小的错误,都可能导致 被调用,导致整个世界崩塌。
但同时,内核也在为自己的脆弱做准备。
"你听说过 kdump 吗?"panic 在寂静中突然开口。
"你刚才提到过。"
"kdump 是内核的'遗书收集机制',"panic 说,声音里第一次带上了一丝……不是温情,但至少是某种深意,"内核知道自己可能会死。所以它提前准备了一块内存——通常 128MB 到 256MB——给一个叫 crash kernel 的备用内核。当 panic 发生时,如果 kdump 配置好了,我会把控制权交给 crash kernel。它会从内存转储中收集信息,保存到磁盘。"
"就像……死后验尸?"
"就像修士渡劫失败时留下的遗蜕,"panic 说,"遗蜕中包含修炼的心得和教训。后人可以通过研究遗蜕来避免同样的错误。kdump 也是如此——虽然我已经死了,但我的'遗体'可以帮助开发者找到死因。"
"那如果什么都没配置呢?"
"那就卡在这里,"panic 说,"死循环。等待人类按下重启按钮。"
林小源沉默了。
在真实的系统中,如果配置了 kdump,kexec 会把 crash kernel 加载到预留内存中启动,然后 crash kernel 通过 /proc/vmcore 访问前一个内核的内存转储。转储被保存到磁盘,重启后可以用 工具分析。
如果什么都没配置,系统就卡在这里——死循环,永远等待。
林小源在 panic 的寂静中等待。他不知道自己还能做什么——他是 idle 进程,在 panic 之后,他也没有了存在的意义。
也许这就是结束。
但内核的世界里,结束往往意味着另一个开始。
道藏笔记
内核启示
是内核在遇到不可恢复错误时的最后手段。
函数定义在 中。它的核心逻辑是:
- 关闭本地中断
- 通过 IPI 停止所有其他 CPU
- 打印 panic 消息和调用栈
- 执行注册的 panic 通知链(如 kdump、重启处理等)
- 进入死循环或触发重启
常见的 panic 原因包括:
- init 进程(PID 1)被杀死或崩溃
- 内核检测到数据结构损坏(如 slab corruption)
- 无法处理的页错误(如 NULL 指针解引用)
- 看门狗超时(soft lockup / hard lockup)
- 硬件故障(如不可纠正的内存错误)
kdump 是内核崩溃转储机制。它在正常内核中预留一块内存给 crash kernel。当 panic 发生时,kexec 把 crash kernel 加载到预留内存中启动,然后 crash kernel 从内存转储中收集信息。这就像"死后验尸"——虽然内核已经死了,但它的"遗体"可以帮助开发者找到死因。
panic 是内核的墓碑。但墓碑上记录的信息,可以帮助后来者避免同样的命运。
/*
* panic() 是内核的"自杀"函数。
* 当内核遇到不可恢复的错误时,调用 panic() 停止一切。
* 常见的 panic 原因:
* 1. init 进程(PID 1)被杀死
* 2. 内核检测到数据结构损坏
* 3. 无法处理的异常
* 4. 看门狗超时
*/
void panic(const char *fmt, ...) {
printf("\n");
printf("========================================\n");
printf(" KERNEL PANIC\n");
printf("========================================\n");
printf("\n");
/* 打印 panic 消息 */
printf("Kernel panic - not syncing: %s\n", fmt);
/* 打印调用栈(简化) */
printf("\nCall Trace:\n");
printf(" [<0x80001000>] panic+0x1c/0x40\n");
printf(" [<0x80002000>] do_exit+0x80/0x200\n");
printf(" [<0x80003000>] make_task_dead+0x40/0x80\n");
/* 在真实内核中,这里会:
* 1. 关闭所有 CPU
* 2. 转储内存到 crash dump
* 3. 等待重启或进入 kdump
*/
printf("\n系统已停止。\n");
printf("请检查日志以确定 panic 原因。\n");
}
printf("=== panic() 演示 ===\n\n");
printf("场景: init 进程被 SIGKILL 杀死\n");
printf("内核检测到 init 退出,触发 panic:\n");
panic("Attempted to kill init!");
printf("\n--- 常见的 panic 原因 ---\n");
printf("1. init 进程崩溃\n");
printf("2. 内核数据结构损坏(如 slab corruption)\n");
printf("3. 无法处理的页错误\n");
printf("4. 硬件故障(如内存错误)\n");
printf("5. 内核 bug(如 NULL 指针解引用)\n");#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
/*
* panic() 是内核的"自杀"函数。
* 当内核遇到不可恢复的错误时,调用 panic() 停止一切。
* 常见的 panic 原因:
* 1. init 进程(PID 1)被杀死
* 2. 内核检测到数据结构损坏
* 3. 无法处理的异常
* 4. 看门狗超时
*/
void panic(const char *fmt, ...) {
printf("\n");
printf("========================================\n");
printf(" KERNEL PANIC\n");
printf("========================================\n");
printf("\n");
/* 打印 panic 消息 */
printf("Kernel panic - not syncing: %s\n", fmt);
/* 打印调用栈(简化) */
printf("\nCall Trace:\n");
printf(" [<0x80001000>] panic+0x1c/0x40\n");
printf(" [<0x80002000>] do_exit+0x80/0x200\n");
printf(" [<0x80003000>] make_task_dead+0x40/0x80\n");
/* 在真实内核中,这里会:
* 1. 关闭所有 CPU
* 2. 转储内存到 crash dump
* 3. 等待重启或进入 kdump
*/
printf("\n系统已停止。\n");
printf("请检查日志以确定 panic 原因。\n");
}
int main() {
printf("=== panic() 演示 ===\n\n");
printf("场景: init 进程被 SIGKILL 杀死\n");
printf("内核检测到 init 退出,触发 panic:\n");
panic("Attempted to kill init!");
printf("\n--- 常见的 panic 原因 ---\n");
printf("1. init 进程崩溃\n");
printf("2. 内核数据结构损坏(如 slab corruption)\n");
printf("3. 无法处理的页错误\n");
printf("4. 硬件故障(如内存错误)\n");
printf("5. 内核 bug(如 NULL 指针解引用)\n");
return 0;
}末日之试
如果 init 进程(PID 1)被杀死,内核会怎么做?