解析 ‘Kernel Panics’:当内核遇到致命错误时,它是如何收集‘遗言’(kdump)并安全自尽的?

各位同仁,各位技术爱好者,大家下午好!

今天我们齐聚一堂,探讨一个在操作系统领域,尤其是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_faultdo_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中定义)通常会执行的步骤,我们对其进行概念性简化:

  1. 打印恐慌信息: 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(); // 打印调用栈
    
        // ... 其他步骤 ...
    }
  2. 禁用中断和抢占: 为了防止在恐慌处理过程中发生新的中断或上下文切换,导致更复杂的问题或数据损坏,panic() 会立即禁用所有中断并关闭抢占。

  3. 同步文件系统(尽力而为): 内核会尝试将所有脏数据(dirty data)从内存缓存同步到磁盘。但这通常是一个尽力而为的操作,因为内核可能已经处于严重损坏状态,文件系统同步可能无法成功。

  4. 通知注册的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);
  5. 触发 Kdump / crash_kexec() 这是核心步骤。如果系统配置了 kdump(我们稍后会详细讨论),panic() 函数会调用 crash_kexec()。这个函数的作用是准备并启动一个预先加载的、专门用于捕获内存转储的第二个内核(dump kernel)。这是收集“遗言”的关键。

    // panic() 函数中调用 crash_kexec() 的简化
    if (kexec_crash_loaded()) {
        crash_kexec(NULL); // 切换到dump kernel
    }
  6. 最终行为: 如果 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 的工作原理可以分为几个阶段:

  1. 内存预留:

    • 在系统启动时(引导加载程序阶段),通过 crashkernel= 引导参数,为 dump kernel 预留一块独立的、连续的内存区域。这块内存不会被主内核使用,即使主内核崩溃,这块内存也是“干净”的。
    • 例如:crashkernel=256M@16M 表示从物理地址16MB开始预留256MB内存。
  2. 加载 dump kernel

    • 主内核启动后,kdump 服务(通常是 systemd 单元 kdump.service)会使用 kexec -p 命令,将 dump kernel 及其 initramfs(包含 makedumpfile 等工具)加载到预留的内存区域中。
    • kexec -p 是一个特殊的 kexec 模式,它不是立即执行新内核,而是将新内核加载到内存中,并设置好入口点和寄存器,等待被触发。
  3. 触发 Kdump

    • 当主内核检测到致命错误并调用 panic() 时,如果 kdump 已配置且加载成功,panic() 会调用 crash_kexec()
    • crash_kexec() 会利用 kexec 系统调用,将 CPU 的控制权从崩溃的主内核直接转移到预加载的 dump kernel。这一过程跳过了常规的 BIOS/UEFI 初始化,因此速度非常快,并且不会清除 CPU 的寄存器状态,最大程度地保留了崩溃现场。
  4. dump kernel 启动与收集:

    • dump kernel 启动后,它是一个独立的、极简的 Linux 内核。它的 initramfs 中包含了必要的工具,如 makedumpfile
    • dump kernel 会将崩溃主内核的整个内存(或其重要部分,通过 makedumpfile 过滤)从原始内存地址空间复制到指定的文件系统路径(通常是 /var/crash)。
    • makedumpfile 是一个非常重要的工具,它可以过滤掉不必要的内存页(如全零页、用户空间页),对数据进行压缩,从而大大减小 vmcore 文件的大小。
  5. 系统重启:

    • 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 离线调试工具 用于加载 vmlinuxvmcore 文件,进行事后分析

4. 配置与测试 Kdump:让系统拥有“黑匣子”

为了让我们的系统在崩溃时能够留下“遗言”,我们需要正确配置 Kdump。

4.1 引导加载程序配置 (GRUB)

首先,编辑 GRUB 配置文件,通常是 /etc/default/grub。找到 GRUB_CMDLINE_LINUXGRUB_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/iomemdmesg | 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 工具,你需要两个核心文件:

  1. vmlinux 文件: 这是与崩溃内核完全匹配的、未压缩的内核映像文件。它包含了内核的符号表信息,crash 工具需要这些信息来解析函数名、变量名和数据结构。通常位于 /usr/lib/debug/boot/vmlinux-<kernel-version>/boot/vmlinux-<kernel-version>(但可能没有符号表)。最保险的方法是安装内核调试符号包(kernel-debuginfolinux-image-*-dbg)。
  2. 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 来分析它。

  1. 启动 crash

    crash /usr/lib/debug/boot/vmlinux-$(uname -r) /var/crash/$(ls -t /var/crash/ | head -n 1)/vmcore
  2. 查看内核日志 (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 是触发恐慌的直接函数。

  3. 查看调用栈 (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。这与我们的预期完全一致。

  4. 检查内核变量或结构体 (如果知道问题点):
    如果你怀疑某个特定的内核变量或数据结构导致了问题,你可以使用 symstruct 命令来检查它们。

    例如,如果你在 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),或者需要保存完整的 unfiltered vmcore,可能需要预留更多。

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,我们至少能够从失败中学习,从而让系统变得更加健壮。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注