各位同仁,各位技术爱好者,大家下午好!
今天我们齐聚一堂,探讨一个在操作系统领域,尤其是Linux内核中,既令人心生敬畏又极度关键的话题:内核恐慌(Kernel Panics)。当我们的系统运行在一片祥和之中,突然屏幕上闪过一串刺眼的错误信息,然后戛然而止,这就是内核恐慌。它意味着操作系统的心脏——内核,遇到了一个它无法自行恢复的致命错误。
但系统并非就此沉寂,无迹可寻。在生命的最后一刻,内核会挣扎着留下它的“遗言”,也就是我们常说的内存转储(kdump)。这就像飞机上的黑匣子,记录下失事前的所有细节,为我们事后分析事故原因提供宝贵线索。作为一名编程专家,我将带领大家深入剖析内核恐慌的本质、内核如何“安全”自尽,以及它留下的“遗言”——kdump的收集与分析机制。
1. 何为内核恐慌?——系统核心的致命失序
首先,让我们明确内核恐慌的定义。内核恐慌(Kernel Panic)是操作系统内核检测到内部一致性错误或无法恢复的系统错误时所采取的一种安全措施。简单来说,就是内核发现自己处于一个它认为无法继续安全运行的状态,为了避免数据损坏、安全漏洞或其他不可预测的后果,它选择停止一切操作,让系统彻底挂起或重启。
内核恐慌与我们常见的应用程序崩溃(如段错误Segmentation Fault)有着本质区别。应用程序崩溃通常只影响单个程序,而内核恐慌则意味着整个操作系统的核心组件失效,所有依赖于内核的服务和进程都将停止。
1.1 内核恐慌的诱因
内核恐慌并非无缘无故发生,它背后通常隐藏着更深层次的问题:
- 硬件故障:
- 内存损坏: 内存条的物理缺陷可能导致内核读写到错误的数据或指令。
- CPU错误: 处理器自身的错误,如微码bug或内部寄存器损坏。
- I/O设备问题: 磁盘控制器、网卡等设备的严重故障,导致内核无法正确访问硬件资源。
- 软件缺陷:
- NULL指针解引用: 这是最常见的错误之一。内核代码试图访问一个NULL地址,导致页面错误。
- Use-After-Free: 内存被释放后又被错误地使用,可能导致数据覆盖或访问非法内存。
- Double-Free: 内存被释放两次,可能破坏内存分配器的数据结构。
- 竞态条件(Race Condition): 多个执行路径并发访问共享资源,缺乏适当同步,导致数据损坏。
- 死锁(Deadlock): 多个进程或线程互相等待对方释放资源,导致系统停滞。
- 不正确的驱动程序: 第三方驱动程序代码质量问题,可能直接在内核空间中引入bug。
- 栈溢出: 内核栈空间有限,递归调用过深或局部变量过大可能导致栈溢出。
- 中断处理错误: 中断处理函数中的错误可能导致系统不稳定。
- 资源耗尽:
- 虽然Linux有OOM Killer机制处理内存耗尽,但在某些极端情况下,如果内存分配器本身出现问题,或者在关键路径上无法分配内存,也可能导致内核恐慌。
1.2 "Oops" vs. "Panic"
在Linux内核中,我们经常会看到“Kernel Oops”和“Kernel Panic”这两个术语。它们都表示内核遇到了问题,但严重程度不同:
- Kernel Oops: 意味着内核检测到一个错误,但它认为这个错误是可恢复的,或者至少不会立即导致整个系统崩溃。内核会打印一条Oops消息到控制台和日志,然后尝试继续运行受影响的进程,或者终止该进程。如果Oops发生在关键路径上,或者发生了多次,它最终可能升级为Kernel Panic。
- Kernel Panic: 表示内核检测到一个它认为无法恢复的致命错误。此时,内核不再尝试继续运行,而是选择停止所有操作,打印Panic消息,并根据配置进入死循环、重启或触发kdump。
2. 内核的“自杀”之路:panic() 函数的深层剖析
当内核决定“放弃治疗”时,它会通过一个核心函数——panic() 来执行其“自杀”流程。这个函数是所有致命错误处理的终点。
2.1 触发路径:从错误到恐慌
内核中的错误通常通过各种宏和函数来检测和报告,例如BUG()、BUG_ON()、WARN()、WARN_ON()等。这些宏在检测到不一致性时,最终会根据错误的严重性决定是否调用panic()。
例如,一个典型的NULL指针解引用会导致一个CPU异常(如x86上的通用保护错误General Protection Fault或页面错误Page Fault)。CPU会将控制权交给内核的异常处理程序(如do_page_fault或do_general_protection)。这些处理程序会分析错误上下文,如果发现错误发生在内核态且无法恢复(例如,试图访问一个非法的内核地址),它们就会调用die()函数,而die()在无法挽回的情况下,最终会调用panic()。
让我们看一个简化的例子,说明一个BUG_ON如何触发panic:
// 假设这是一个简化的内核模块代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/sched.h> // for current
#include <linux/panic.h> // for panic
static int __init my_panic_init(void)
{
printk(KERN_INFO "Loading my_panic module...n");
// 示例1: 故意触发一个NULL指针解引用
// int *ptr = NULL;
// *ptr = 10; // 这将导致一个页面错误,最终可能导致panic
// 示例2: 使用BUG_ON宏直接触发panic
// BUG_ON(1); // 传入一个总是为真的条件,直接触发BUG,进而可能panic
// 示例3: 模拟一个更复杂的错误,最终调用panic
// 假设我们有一个关键数据结构,但它被意外地设置为NULL
void *critical_data_structure = NULL;
if (critical_data_structure == NULL) {
printk(KERN_EMERG "Critical data structure is NULL! This is an unrecoverable error.n");
// 在实际内核中,通常不会直接调用panic,
// 而是通过BUG()或更高级的错误处理机制
// 这里为了演示,我们直接调用panic
// 注意:在实际模块中直接调用panic是不推荐的,除非你真的想崩溃系统
panic("My module detected a critical internal error: critical_data_structure is NULL");
}
return 0;
}
static void __exit my_panic_exit(void)
{
printk(KERN_INFO "Unloading my_panic module.n");
}
module_init(my_panic_init);
module_exit(my_panic_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A module to demonstrate kernel panic.");
2.2 panic() 函数的职责
panic() 函数是内核恐慌的核心,它在被调用后,会执行一系列关键的清理和信息记录工作,然后停止系统。以下是panic()函数(在kernel/panic.c中定义)通常会执行的步骤,我们对其进行概念性简化:
-
打印恐慌信息:
panic()的第一个任务是将恐慌消息(通常是错误字符串和当前的CPU寄存器状态、调用栈信息等)打印到控制台和内核日志缓冲区。这是我们获得初步诊断信息的最重要来源。// 简化后的panic()函数片段 void panic(const char *fmt, ...) { // 1. 禁用中断,防止进一步的并发问题和数据损坏 // local_irq_disable(); // preemption_disable(); // 2. 打印恐慌消息 // vprintk(fmt, args); // printk(KERN_EMERG "Kernel panic - not syncing: %sn", message); // dump_stack(); // 打印调用栈 // ... 其他步骤 ... } -
禁用中断和抢占: 为了防止在恐慌处理过程中发生新的中断或上下文切换,导致更复杂的问题或数据损坏,
panic()会立即禁用所有中断并关闭抢占。 -
同步文件系统(尽力而为): 内核会尝试将所有脏数据(dirty data)从内存缓存同步到磁盘。但这通常是一个尽力而为的操作,因为内核可能已经处于严重损坏状态,文件系统同步可能无法成功。
-
通知注册的Panic Notifiers: 内核允许其他子系统注册回调函数(Panic Notifiers)。当恐慌发生时,这些函数会被调用,允许它们执行一些紧急清理工作,例如:
- 某些驱动程序可能需要关闭硬件。
- 安全模块可能需要记录关键事件。
// 注册和调用panic notifier的简化概念 struct notifier_block my_panic_notifier = { .notifier_call = my_notifier_function, .priority = 10, }; // 在模块初始化时注册 // atomic_notifier_chain_register(&panic_notifier_list, &my_panic_notifier); // 在panic()中被调用 // atomic_notifier_call_chain(&panic_notifier_list, 0, message); -
触发 Kdump /
crash_kexec(): 这是核心步骤。如果系统配置了 kdump(我们稍后会详细讨论),panic()函数会调用crash_kexec()。这个函数的作用是准备并启动一个预先加载的、专门用于捕获内存转储的第二个内核(dump kernel)。这是收集“遗言”的关键。// panic() 函数中调用 crash_kexec() 的简化 if (kexec_crash_loaded()) { crash_kexec(NULL); // 切换到dump kernel } -
最终行为: 如果 kdump 没有配置或失败,或者
panic_timeout设置为0,内核会进入一个无限循环(for (;;) ;),使系统彻底挂起。如果panic_timeout设置为一个正数,内核会在等待一段时间后强制重启系统。// panic() 函数的最终部分 // if (panic_timeout > 0) { // mdelay(panic_timeout * 1000); // 等待panic_timeout秒 // machine_restart(NULL); // 重启系统 // } else { // // 无限循环,挂起系统 // for (;;) // cpu_relax(); // }
panic() 函数的流程总结:
| 步骤序号 | 动作描述 | 目的 |
|---|---|---|
| 1 | 禁用中断和抢占 | 防止新的错误、竞态条件以及在恐慌处理期间的数据损坏 |
| 2 | 打印恐慌消息和调用栈 | 向用户提供初步的错误信息,记录导致恐慌的调用路径 |
| 3 | 尽力同步文件系统(可选) | 尝试将内存中的脏数据写入磁盘,减少数据丢失 |
| 4 | 调用注册的Panic Notifiers | 允许其他内核子系统执行紧急清理或记录操作 |
| 5 | 触发 crash_kexec() (如果配置了 kdump) |
切换到 dump kernel,以便捕获崩溃内核的内存状态(核心步骤) |
| 6 | 最终行为(无限循环或重启) | 如果 kdump 失败或未配置,系统挂起或按 panic_timeout 配置重启 |
3. “遗言”的收集:Kdump 机制的精妙设计
当内核面临绝境时,它最希望的就是能留下完整的“遗言”,以便我们事后能查明死因。这个“遗言”就是内存转储(memory dump),而收集它的机制,就是 Kdump。Kdump 是 Linux 内核的一个强大功能,它允许在系统崩溃时捕获当前内核的完整内存映像。
3.1 Kdump 是什么?为什么需要它?
Kdump 利用了 kexec 系统调用,在主内核崩溃时,能够“热启动”到一个预先加载的、独立的、极简的“dump kernel”(转储内核)。这个 dump kernel 的任务就是从崩溃的主内核的内存空间中,将内存数据复制出来并保存成文件,通常命名为 vmcore。
为什么需要 Kdump?
- 保留崩溃现场: 没有 Kdump,系统崩溃后通常直接重启。所有关于崩溃现场的关键信息(内存状态、寄存器值、调用栈等)都会随之消失。
- 事后分析:
vmcore文件可以配合crash工具进行事后分析(post-mortem debugging),帮助开发者和系统管理员精确诊断内核崩溃的原因。 - 避免数据丢失: 相比于在崩溃时直接重启,Kdump 允许我们保存关键数据,尽管它本身并不能阻止数据丢失,但它提供了解决问题的基础。
3.2 Kdump 的工作原理
Kdump 的工作原理可以分为几个阶段:
-
内存预留:
- 在系统启动时(引导加载程序阶段),通过
crashkernel=引导参数,为dump kernel预留一块独立的、连续的内存区域。这块内存不会被主内核使用,即使主内核崩溃,这块内存也是“干净”的。 - 例如:
crashkernel=256M@16M表示从物理地址16MB开始预留256MB内存。
- 在系统启动时(引导加载程序阶段),通过
-
加载
dump kernel:- 主内核启动后,
kdump服务(通常是systemd单元kdump.service)会使用kexec -p命令,将dump kernel及其initramfs(包含makedumpfile等工具)加载到预留的内存区域中。 kexec -p是一个特殊的kexec模式,它不是立即执行新内核,而是将新内核加载到内存中,并设置好入口点和寄存器,等待被触发。
- 主内核启动后,
-
触发
Kdump:- 当主内核检测到致命错误并调用
panic()时,如果kdump已配置且加载成功,panic()会调用crash_kexec()。 crash_kexec()会利用kexec系统调用,将 CPU 的控制权从崩溃的主内核直接转移到预加载的dump kernel。这一过程跳过了常规的 BIOS/UEFI 初始化,因此速度非常快,并且不会清除 CPU 的寄存器状态,最大程度地保留了崩溃现场。
- 当主内核检测到致命错误并调用
-
dump kernel启动与收集:dump kernel启动后,它是一个独立的、极简的 Linux 内核。它的initramfs中包含了必要的工具,如makedumpfile。dump kernel会将崩溃主内核的整个内存(或其重要部分,通过makedumpfile过滤)从原始内存地址空间复制到指定的文件系统路径(通常是/var/crash)。makedumpfile是一个非常重要的工具,它可以过滤掉不必要的内存页(如全零页、用户空间页),对数据进行压缩,从而大大减小vmcore文件的大小。
-
系统重启:
dump kernel完成内存转储后,它会执行一次常规的重启操作,将系统引导回正常的主内核,从而恢复服务。
Kdump 流程图概览:
+-------------------+ +-------------------+ +-------------------+
| 引导加载程序 |----->| 主内核启动 |----->| kdump 服务启动 |
| (GRUB, UEFI) | | (加载 crashkernel) | | (kexec -p 加载 dump kernel)|
+-------------------+ +-------------------+ +-------------------+
| |
| |
v v
+-------------------+ +-------------------+
| 系统正常运行 | | dump kernel 处于待命状态 |
| | | (在 crashkernel 区域) |
+-------------------+ +-------------------+
|
| 内核检测到致命错误 (e.g., NULL指针解引用)
v
+-------------------+
| 调用 panic() |
| (禁用中断,打印信息) |
+-------------------+
|
| 调用 crash_kexec() (如果 kdump 已配置)
v
+-------------------+
| kexec 系统调用 |------------------------------------>| dump kernel 接管控制权 |
| (CPU寄存器切换到 dump kernel 入口点) | (跳过BIOS/UEFI) |
+-------------------+ +-------------------+
|
| dump kernel 启动
v
+-------------------+
| dump kernel 运行 |
| (initramfs 包含 makedumpfile) |
+-------------------+
|
| 读取主内核内存,过滤,压缩
v
+-------------------+
| 保存 vmcore 文件 |
| (到 /var/crash/...) |
+-------------------+
|
| dump kernel 重启系统
v
+-------------------+
| 系统重新引导 |
| (回到主内核) |
+-------------------+
3.3 Kdump 的关键组件
| 组件名称 | 描述 | 作用 |
|---|---|---|
crashkernel= |
GRUB/引导加载程序参数,用于预留内存 | 为 dump kernel 提供独立的内存空间,避免崩溃内核的数据污染 |
kexec |
Linux 系统调用和用户空间工具 | 实现“热启动”新内核,避免完整的硬件初始化,保留崩溃现场 |
dump kernel |
专门编译的极简内核,运行在预留内存中 | 负责读取崩溃主内核的内存,执行转储操作 |
initramfs |
dump kernel 的初始内存文件系统 | 包含 makedumpfile 等工具和必要的驱动,以便 dump kernel 能够正常工作并写入文件 |
makedumpfile |
用户空间工具,用于收集、过滤和压缩 vmcore 文件 |
大幅减少 vmcore 文件大小,提高转储效率 |
kdump.conf |
kdump 服务的配置文件 | 配置转储文件的保存路径、收集方式、过滤级别等 |
crash |
离线调试工具 | 用于加载 vmlinux 和 vmcore 文件,进行事后分析 |
4. 配置与测试 Kdump:让系统拥有“黑匣子”
为了让我们的系统在崩溃时能够留下“遗言”,我们需要正确配置 Kdump。
4.1 引导加载程序配置 (GRUB)
首先,编辑 GRUB 配置文件,通常是 /etc/default/grub。找到 GRUB_CMDLINE_LINUX 或 GRUB_CMDLINE_LINUX_DEFAULT 行,添加 crashkernel= 参数。
# /etc/default/grub 示例
GRUB_CMDLINE_LINUX_DEFAULT="rhgb quiet crashkernel=auto"
# 或者明确指定大小和起始地址
# GRUB_CMDLINE_LINUX_DEFAULT="rhgb quiet crashkernel=256M@16M"
crashkernel=auto:这是一个智能选项,它会尝试根据系统总内存大小自动选择合适的预留内存大小和位置。对于大多数系统来说,这是推荐的选项。crashkernel=X@Y:手动指定预留X大小的内存,并从物理地址Y开始。例如,256M@16M表示预留 256MB 内存,从物理地址 16MB 处开始。手动指定通常用于特殊硬件或需要精确控制内存布局的场景。
修改 GRUB 配置后,需要更新 GRUB 配置:
sudo grub2-mkconfig -o /boot/grub2/grub.cfg # 对于基于RPM的系统
# 或者 sudo update-grub # 对于基于Debian的系统
然后重启系统,以使 crashkernel 参数生效。重启后,可以通过查看 /proc/cmdline 确认参数是否生效,并通过 cat /proc/iomem 或 dmesg | grep "Crash kernel" 来查看预留内存的信息。
# 查看命令行参数
cat /proc/cmdline
# 示例输出:... crashkernel=256M@0M ...
# 查看预留内存信息
dmesg | grep "Crash kernel"
# 示例输出:[ 0.000000] Reserving 256MB of memory for crashkernel (System RAM: 32000MB)
4.2 Kdump 服务配置 (/etc/kdump.conf)
kdump.conf 文件是 Kdump 的主要配置文件。它决定了 vmcore 文件的保存方式、路径、过滤级别等。
# /etc/kdump.conf 示例
# dump 文件的保存路径。通常设置为本地文件系统路径。
# 如果不指定,默认通常是 /var/crash/<日期时间>
# path /var/crash
# core_collector 指定用于收集和过滤 vmcore 的工具。
# 通常是 makedumpfile。
# 级别 1 会过滤掉大部分用户空间页面和全零页面。
# 级别 0 会保存所有内存。
# makedumpfile -c -d <level>
core_collector makedumpfile -l --message-level 7 -d 3
# 这是一个示例,将 vmcore 复制到远程 SSH 服务器
# ssh user@host
# ssh [email protected]
# 这是一个示例,将 vmcore 复制到 NFS 共享
# nfs server:/path/to/share
# dump 文件的文件名格式
# dump_name %HOST-%DATE
# blacklist 可以在 dump kernel 中禁用某些模块,以减少内存占用和启动时间。
# blacklist <module_name>
blacklist nouveau
# 如果 kdump 失败,可以指定一个 fallback 动作,例如重启
# default reboot
配置完成后,启动并启用 kdump 服务:
sudo systemctl enable kdump.service
sudo systemctl start kdump.service
可以通过 sudo systemctl status kdump.service 来检查服务状态。
4.3 测试 Kdump:主动触发内核恐慌
为了验证 Kdump 是否配置成功,我们可以主动触发一个内核恐慌。请注意:这会使你的系统崩溃并重启,务必在测试环境中进行,并保存所有重要数据!
最常用的方法是通过 /proc/sysrq-trigger 接口:
echo c | sudo tee /proc/sysrq-trigger
执行这条命令后,系统会立即发生内核恐慌。如果 Kdump 配置正确,系统会进入 dump kernel 模式,将 vmcore 文件保存到 /var/crash/ 目录下,然后重启。
重启后,检查 /var/crash/ 目录,应该能找到一个 vmcore 文件或一个包含 vmcore 文件的目录。
ls -l /var/crash/
# 示例输出:
# drwxr-xr-x. 3 root root 4096 Apr 20 15:30 2023-04-20-15:30:00
# ls -l /var/crash/2023-04-20-15:30:00/
# -rw-------. 1 root root 1234567 Apr 20 15:35 vmcore
5. 分析“遗言”:使用 crash 工具进行事后调试
有了 vmcore 文件,我们就有了诊断内核恐慌的“黑匣子”。Linux 内核提供了一个强大的离线调试工具——crash。
5.1 crash 工具简介
crash 工具是专门为 Linux 内核转储文件分析而设计的。它结合了 gdb 的调试能力和对内核内部数据结构的深刻理解。使用 crash,你可以查看内核日志、进程列表、内存映射、调用栈、各种数据结构等,从而定位崩溃原因。
5.2 准备分析环境
要使用 crash 工具,你需要两个核心文件:
vmlinux文件: 这是与崩溃内核完全匹配的、未压缩的内核映像文件。它包含了内核的符号表信息,crash工具需要这些信息来解析函数名、变量名和数据结构。通常位于/usr/lib/debug/boot/vmlinux-<kernel-version>或/boot/vmlinux-<kernel-version>(但可能没有符号表)。最保险的方法是安装内核调试符号包(kernel-debuginfo或linux-image-*-dbg)。vmcore文件: 这是 Kdump 生成的内存转储文件,通常位于/var/crash/<timestamp>/vmcore。
5.3 启动 crash 会话
打开终端,使用以下命令启动 crash:
crash /path/to/vmlinux /path/to/vmcore
例如:
crash /usr/lib/debug/boot/vmlinux-5.14.0-XXX.x86_64 /var/crash/2023-04-20-15:30:00/vmcore
成功启动后,你会看到 crash> 提示符。
5.4 crash 工具常用命令及其用途
以下是一些在分析 vmcore 时最常用的 crash 命令:
| 命令 | 描述 | 示例 |
|---|---|---|
log |
显示内核消息缓冲区(dmesg 的内容) |
log |
bt |
显示当前上下文(或指定任务)的调用栈回溯 | bt (显示当前CPU的栈) bt -a (显示所有活跃CPU的栈) bt <PID> (显示指定进程的栈) |
ps |
显示进程列表 | ps ps -l (显示更多详细信息) |
mod |
显示已加载的内核模块列表 | mod |
sym <symbol> |
查找内核符号(函数、变量)的地址或值 | sym panic sym init_task |
rd <address> |
读取内存地址的内容 | rd -S 0xffffffff81000000 64 (读取内核代码段64字节) |
struct <name> |
显示内核数据结构定义 | struct task_struct |
dis <address> |
反汇编指定地址的代码 | dis panic |
help <command> |
获取命令帮助 | help bt |
cpu |
显示当前 CPU 号码 | cpu |
set |
设置当前上下文(CPU、进程等) | set cpu 1 set task <PID> |
g |
显示通用寄存器 | g |
vm |
显示指定进程的虚拟内存映射 | vm <PID> |
q |
退出 crash 会话 |
q |
5.5 crash 分析案例:定位 NULL 指针解引用
假设我们之前通过 echo c > /proc/sysrq-trigger 触发了一个恐慌。现在我们用 crash 来分析它。
-
启动
crash:crash /usr/lib/debug/boot/vmlinux-$(uname -r) /var/crash/$(ls -t /var/crash/ | head -n 1)/vmcore -
查看内核日志 (
log):
log命令会显示内核在崩溃前打印的日志信息。你会看到包含Kernel panic - not syncing: sysrq: SysRq 'c' pressed的消息,以及在它之前的一些关键信息。crash> log ... [ 123.456789] sysrq: SysRq 'c' pressed, forcing crash [ 123.456790] Kernel panic - not syncing: sysrq: SysRq 'c' pressed [ 123.456791] CPU: 0 PID: 1234 Comm: bash Tainted: P OEL 5.14.0-XXX.x86_64 #1 [ 123.456792] Hardware name: ... [ 123.456793] Call Trace: [ 123.456794] dump_stack+0x7f/0xa0 [ 123.456795] panic+0x101/0x2c4 [ 123.456796] sysrq_handle_crash+0x15/0x20 [ 123.456797] __handle_sysrq+0x10b/0x1a0 [ 123.456798] write_sysrq_trigger+0x2c/0x40 [ 123.456799] proc_reg_write+0x54/0x90 [ 123.456800] vfs_write+0xcf/0x1f0 [ 123.456801] ksys_write+0x5e/0xd0 [ 123.456802] do_syscall_64+0x4a/0x80 [ 123.456803] entry_SYSCALL_64_after_hwframe+0x61/0xcb ...这里,
sysrq_handle_crash是触发恐慌的直接函数。 -
查看调用栈 (
bt):
bt命令会显示导致恐慌的 CPU 的调用栈。这通常是我们定位问题根源的关键。crash> bt PID: 1234 TASK: ffff888123456780 CPU: 0 COMMAND: "bash" #0 [ffffc90000003cb0] crash_kexec (ffff888123456780) at kernel/kexec_core.c:1342 #1 [ffffc90000003d10] __crash_kexec (ffff888123456780) at kernel/kexec_core.c:1342 #2 [ffffc90000003d50] panic (ffff888123456780) at kernel/panic.c:273 #3 [ffffc90000003e00] sysrq_handle_crash (ffff888123456780) at drivers/char/sysrq.c:137 #4 [ffffc90000003e20] __handle_sysrq (ffff888123456780) at drivers/char/sysrq.c:514 #5 [ffffc90000003e70] write_sysrq_trigger (ffff888123456780) at drivers/char/sysrq.c:560 #6 [ffffc90000003e90] proc_reg_write (ffff888123456780) at fs/proc/generic.c:283 #7 [ffffc90000003ec0] vfs_write (ffff888123456780) at fs/read_write.c:492 #8 [ffffc90000003f00] ksys_write (ffff888123456780) at fs/read_write.c:547 #9 [ffffc90000003f30] do_syscall_64 (ffff888123456780) at arch/x86/entry/common.c:86 #10 [ffffc90000003f60] entry_SYSCALL_64_after_hwframe (ffff888123456780) at arch/x86/entry/entry_64.S:120从
bt输出中,我们可以清楚地看到调用栈:bash进程通过系统调用写入/proc/sysrq-trigger,触发write_sysrq_trigger,进而调用sysrq_handle_crash,最终导致panic。这与我们的预期完全一致。 -
检查内核变量或结构体 (如果知道问题点):
如果你怀疑某个特定的内核变量或数据结构导致了问题,你可以使用sym和struct命令来检查它们。例如,如果你在
my_panic_module中故意制造了一个 NULL 指针解引用,并且你已经知道是my_panic_init函数中的critical_data_structure变量导致的问题,你可以尝试:crash> sym my_panic_init ffffc00000000000 (t) my_panic_init crash> list my_panic_init // 这会显示 my_panic_init 函数的源代码,帮助你定位问题行如果崩溃是由于某个模块的 bug 引起的,
bt命令的输出通常会包含模块的名称和相关的函数。你可以通过mod命令来获取模块的详细信息,例如其加载地址。crash> mod ... ffffc90000000000 my_panic_module (Live) (cf0000000000) ...然后你可以使用
dis命令反汇编模块中的函数,或者使用list命令(如果vmlinux中包含源代码路径信息)查看源代码。
通过这些工具和技巧,我们可以抽丝剥茧,从 vmcore 文件中提取出宝贵的信息,最终定位到内核恐慌的根本原因。
6. 最佳实践与进阶议题
6.1 crashkernel 内存大小的选择
- 过小: dump kernel 可能无法启动,或者无法加载必要的驱动和工具,导致无法成功收集
vmcore。 - 过大: 浪费系统宝贵的内存资源,减少主内核可用内存,可能反而增加 OOM 风险。
- 推荐:
crashkernel=auto是最简单的选择,它通常能很好地工作。对于大型服务器,256MB 到 512MB 通常足够。如果你的系统有很多内存(比如几十GB或上百GB),或者需要保存完整的 unfilteredvmcore,可能需要预留更多。
6.2 远程 Kdump
在生产环境中,将 vmcore 文件保存到本地磁盘可能不是最佳选择。如果磁盘本身就是导致崩溃的原因,或者本地存储空间不足,远程 Kdump 就变得非常重要。
- SSH:
kdump.conf支持配置ssh user@host。dump kernel 会通过网络将vmcore文件传输到指定的 SSH 服务器。 - NFS: 同样,也可以配置
nfs server:/path/to/share,将vmcore保存到 NFS 共享。
远程 Kdump 需要 dump kernel 包含网络驱动,并且能够正确配置网络。
6.3 makedumpfile 的过滤级别
makedumpfile 的 -d 参数用于指定过滤级别:
-d 0:不进行任何过滤,保存所有内存页。文件最大,但信息最完整。-d 1:过滤掉全零页、用户空间页和空闲页。这是推荐的默认级别,能显著减小文件大小,同时保留诊断所需的大部分信息。-d 2:在级别 1 的基础上,进一步过滤掉不活跃的内核页。-d 3:过滤掉所有不需要的页(包括所有用户空间页、所有空闲页、所有不活跃的内核页)。文件最小,但可能丢失一些边缘信息。
选择合适的级别需要在文件大小和信息完整性之间取得平衡。
6.4 panic_timeout 与其他内核参数
panic_timeout:通过/proc/sys/kernel/panic设置。0 表示挂起,正数表示在恐慌后等待多少秒然后重启。panic_on_oops:通过/proc/sys/kernel/panic_on_oops设置。如果为 1,任何 Oops 都将导致内核恐慌。这有助于在开发和测试环境中快速发现问题。panic_on_warn:通过/proc/sys/kernel/panic_on_warn设置。如果为 1,任何WARN()或WARN_ON()宏的触发都会导致内核恐慌。
6.5 crash 工具的进阶用法
- GDB 集成:
crash工具本身就内置了gdb功能。你可以使用gdb命令来访问gdb的所有功能,例如设置断点(虽然在离线调试中意义不大)、检查局部变量等。 - 脚本:
crash支持加载自定义脚本,自动化分析流程,例如自动解析特定数据结构或生成报告。 - 历史记录:
crash维护命令历史,可以使用上下箭头键访问。
通过我们今天的讨论,大家应该对内核恐慌不再感到神秘和恐惧。我们了解了它为何发生,内核在面临绝境时如何处理,以及Kdump机制如何巧妙地捕获这些宝贵的“遗言”。掌握Kdump的配置、触发和特别是使用crash工具进行事后分析,是每一位系统管理员和内核开发者不可或缺的技能。它不仅能帮助我们快速定位和解决问题,更是提升系统稳定性和可靠性的重要基石。内核恐慌虽然是系统最糟糕的状况,但有了Kdump,我们至少能够从失败中学习,从而让系统变得更加健壮。