资讯详情

资讯详情

Linux内核Panic实战排查:从日志捕获到根因定位

半夜两点被一通“服务器连不上了控制台一堆英文”的电话叫醒远程管理界面里滚动着那行让人血压飙升的字符Kernel panic - not syncing。做过Linux运维或者内核相关开发的人应该都能体会那种“完了今晚别想睡了”的心情。Kernel Panic意味着操作系统最核心的内核部分在运行时撞上了无法恢复的致命错误只能选择立即停机。而真正让人头疼的往往不是Panic本身而是Panic发生后你手上什么都没有串口没接、内存转储没配、控制台输出没保留机器一重启就跟失忆一样只剩下“刚才好像崩了”这个模糊线索。这篇文章我想从实战角度把Kernel Panic分析这件事完整拆一遍。不讲教科书理论只讲遇到问题时要做什么、为什么这么做、怎么做最快。内容主要面向三类人常年维护生产环境的运维工程师、写内核模块或底层驱动的开发同学以及刚接触系统编程、想搞明白内核“死亡现场”长什么样的学习者。读完你至少能独立处理一次典型的Panic现场。1. Kernel Panic的底层机制内核究竟在哪一步“翻车”1.1 Panic的本质内核主动执行的自毁程序先得搞清楚一点Kernel Panic不是“内核崩溃了”这么笼统而是内核主动触发的一个应急流程。内核运行在CPU的最高特权级管理着内存、进程、设备驱动、文件系统等所有底层资源。一旦它发现自己已经无法保证系统安全运行——比如访问了非法内存地址、数据结构被写坏、某个关键锁永远拿不到——继续跑下去只会造成更严重的破坏于是调用panic()函数打印关键日志然后要么挂起、要么按配置自动重启。这跟用户态程序崩溃有本质区别。你用Python写个脚本挂了最多是那个进程退出系统其他部分照常运行。但内核一旦Panic整台机器都会失去响应因为内核就是所有进程的“地基”地基塌了上面什么都站不住。内核在Panic时会执行几个动作先把崩溃信息打印到控制台和日志缓冲区然后尝试同步文件系统缓存到磁盘这个动作有时候会失败因为触发Panic的原因可能正是I/O子系统挂了最后根据panic_timeout内核参数决定行为——0表示永久挂起正数表示多少秒后自动重启。很多人问我为什么有时候Panic后机器会自动重启有时候卡死不动关键就在这里。另外有个概念需要区分Oops和Panic。Oops是非致命的内核错误内核会记录错误、杀掉当前进程然后尝试继续运行。但Oops说明内核的内存状态可能已经被破坏后续行为不可预测很多团队会把panic_on_oops1开启让Oops也直接升级成Panic并重启用一次快速重启换取一个干净状态。1.2 触发Panic的常见场景硬件、驱动、文件系统与内存错误从这些年处理的案例来看触发Panic的场景大致可以归成几类。我整理了一个表方便对照排查方向触发场景典型日志特征高危对象内存硬件故障MCE、Machine Check Exception、随机地址访问错误内存条、CPU Cache、ECC失效的服务器磁盘I/O异常buffer I/O error、Unable to handle kernel paging request硬盘坏道、SSD固件Bug、控制器故障驱动BugRIP指向某个.ko模块内的函数如nv_ioctl0x21/0xd0 [nvidia]显卡驱动、网卡驱动、定制外设驱动文件系统损坏EXT4-fs error、superblock相关报错异常断电后的分区、有坏道的磁盘内核模块不匹配module verification failed、disagrees about version of symbol外部编译模块、DKMS模块极端资源耗尽Out of memory、Killed process后仍无法收敛内存超售严重的容器宿主机实际生产环境里驱动Bug和内存条故障占了大头。写驱动本身就不容易尤其是设备驱动里直接操作用户态传入的缓冲区、DMA地址没有做边界校验、或者模块卸载时没有正确释放资源都可能在特定路径下踩出Panic。硬件故障则更隐蔽往往内存条已经坏了一根系统还在勉强运行直到某个进程恰好访问到那块坏掉的物理页内核才“突然暴毙”。另一个容易踩坑的是内核模块与当前内核版本不匹配。比如热词里提到的NVIDIA驱动加载问题unable to load the kernel module nvidia.ko这种通常不是Panic但由它引发的连锁反应——比如旧驱动模块访问了新内核不再兼容的接口——就可能直接导致系统崩溃。后面我会专门用这类案例做一次完整复盘。2. 先保住现场三类日志捕获方案的取舍2.1 串口控制台日志最朴素也最稳的方案Panic分析最怕的不是Panic本身而是Panic之后你拿不到日志。机器重启完dmesg里的内容已经变成“从开机到现在的正常日志”崩溃现场的信息完全丢失。所以第一步要做的就是想尽一切办法把崩溃那一刻的输出留下来。串口控制台是最老派也最可靠的方式。它不依赖磁盘、不依赖网络只要内核参数里加了控制台重定向panic时的输出就会顺着串口线发出去。配置方法很简单编辑/etc/default/grub在GRUB_CMDLINE_LINUX里加上consolettyS0,115200服务器通常用ttyS0嵌入式平台可能是ttyAMA0或ttyS0然后执行sudo update-grub sudo reboot重启后用串口线接上电脑打开minicom、PuTTY或者screen连上去就能看到完整的启动日志和控制台输出。虚拟机场景更简单很多虚拟化平台可以直接把串口重定向到文件或者伪终端不需要物理连线。串口的优势是稳定、实时缺点是必须有物理访问或带外管理通道而且日志量大之后不好回溯。但它依然是我处理陌生机器时最先配置的东西——别的方案可能配不出来串口基本不会失手。2.2 kdump拿到完整vmcore代价是预留内存与磁盘如果要深入分析Panic根因串口日志往往不够最理想的是拿到一份完整的内存转储文件也就是vmcore。这时需要用到kdump机制。kdump的原理是启动时预留一块内存crashkernel平时不用Panic发生时一个专门用于捕获的内核会在这块内存里启动把崩溃内核的内存现场写成文件。配置步骤如下# Ubuntu/Debian 系 sudo apt install linux-crashdump kdump-tools # 确认 /etc/default/grub 中包含 crashkernel 参数 GRUB_CMDLINE_LINUXcrashkernel512M # 更新引导并重启 sudo update-grub sudo reboot # 重启后确认参数生效 cat /proc/cmdline | grep crashkernelcrashkernel大小要根据物理内存量调整。我自己的经验是内存在16G以内的机器给256M到512M就够了内存更大的生产机器建议用crashkernel1G或让系统自动计算设置过小可能导致捕获内核起不来设置过大则浪费内存。配置好之后可以用一个“人工制造Panic”的方式来验证echo c | sudo tee /proc/sysrq-trigger。这行命令强制内核触发Panic重启后去/var/crash下找生成的vmcore文件。注意这个验证动作会让机器立即崩溃要在业务低峰期操作并且提前告知相关同事。vmcore文件非常大通常等于物理内存大小所以存放vmcore的磁盘分区务必留足空间。我见过有人没注意磁盘容量Panic发生后转储写到一半磁盘满了等于白忙一场。2.3 pstore/ramoops无盘、嵌入式的救命稻草如果目标是嵌入式设备、或者磁盘经常出问题的机器kdump可能不实用——毕竟Panic可能正是磁盘I/O导致的。这时候可以用pstore/ramoops把崩溃日志写进一块保留的内存区域系统重启后这块内存的内容还在可以从/sys/fs/pstore里读出来。开启方式同样是在内核参数里加配置GRUB_CMDLINE_LINUXramoops.mem_address0x80000000 ramoops.mem_size0x200000 sudo update-grub sudo rebootmem_size根据需求调整我一般给2M到4M能存下最近一两次崩溃的核心日志。重启后查看ls /sys/fs/pstore/ cat /sys/fs/pstore/dmesg-ramoops-0pstore方案的优点是轻量不依赖磁盘和网络缺点是只能存少量文本日志没有内存转储那么完整。它特别适合网络设备、智能终端这类没有大型存储介质的场景。我的习惯是嵌入式设备优先配pstore服务器优先配kdump串口两边可以同时开互补不冲突。3. 拆解Panic日志从RIP到Call Trace的阅读方法3.1 一份典型日志的逐行拆解拿到日志之后怎么从一大堆输出里快速定位关键信息我拿一段典型的Panic日志做范例模拟的是一段驱动代码访问非法内存时的崩溃现场BUG: unable to handle kernel paging request at 0000000000000008 IP: 0000000000000008 PGD 0 P4D 0 Oops: 0000 [#1] PREEMPT SMP PTI Modules linked in: my_drv(O) nfsd xt_MASQUERADE ... CPU: 1 PID: 10086 Comm: test Tainted: P W O 5.4.0-26-generic RIP: 0010:[0000000000000008] Code: bad RIP value. RSP: 0018:ffffb3a2020ff9b0 EFLAGS: 00010246 RAX: 0000000000000000 RBX: ffff9c1f3f3d6c00 RCX: ffff9c1f40000000 RIP: 0010:my_func0x21/0x50 [my_drv] Call Trace: [ffffffffa0000031] my_func0x21/0x50 [my_drv] [ffffffff84e1f1a0] do_vfs_ioctl0xa0/0x6e0 [ffffffff84e1f86a] ksys_ioctl0x8a/0xc0 [ffffffff84e1f8e8] __x64_sys_ioctl0x18/0x20 [ffffffff84de0f0b] do_syscall_640x5b/0x1b0第一行BUG: unable to handle kernel paging request at 0000000000000008是结论内核访问了一个无法映射的地址这里是0x8。这个地址一看就很“可疑”——它几乎是空指针加了个偏移典型的坏函数指针或未初始化指针。接下来IP: 0000000000000008说明CPU试图跳转到这个非法地址去执行代码也就是“执行流断了”。结合RIP: 0010: [0000000000000008]中显示的bad RIP value基本可以判定驱动里调用了一个野指针函数。Oops: 0000 [#1]中#1代表这是本次启动后第一个Oops/Panic。CPU: 1 PID: 10086 Comm: test告诉我们触发Panic的进程是test跑在1号CPU上这对后续复现很重要。3.2 从RIP、Kernel Offset、Tainted标志里找线索日志中RIP是核心中的核心。在模块场景下RIP通常显示为函数名偏移/函数总长度例如my_func0x21/0x50 [my_drv]。这句话直接告诉我们崩溃点在my_drv模块的my_func函数内偏移0x21处。配合反汇编就能精确定位到是哪条指令出了问题。Tainted字段也容易被忽略但它能快速判断这台机器加载了哪些“非标准”内核代码。常见标志包括P加载了私有/非GPL许可的模块O加载了外部模块out-of-treeE模块未签名或签名校验失败W存在内核警告G所有模块都是GPL通常是好状态日志中出现Tainted: P W O这类组合意味着大概率有外部驱动参与排查优先级应该优先放在外部模块上。还有个容易忽略的点是Kernel Offset。现代内核开启了KASLR每次启动加载地址都不同。分析vmcore时日志会打印类似Kernel Offset: 0x2a800000 from 0xffffffff81000000的信息这个值决定了符号地址换算的基准用crash工具时它会自动处理但手动用addr2line时必须注意换算关系。3.3 addr2line与crash工具把十六进制地址翻译成人话拿到RIP地址怎么定位到源码行两个常用工具addr2line和crash。addr2line适合快速换算但需要内核调试符号文件。以Ubuntu为例安装linux-image-$(uname -r)-dbgsym后/usr/lib/debug/lib/modules/$(uname -r)/vmlinux就是带符号的内核镜像。换算命令addr2line -e /usr/lib/debug/lib/modules/$(uname -r)/vmlinux -f 0xffffffff81000123如果是模块崩溃还需要先知道模块加载基址。get fromdmesg或crash的mod命令然后用“崩溃地址 - 模块基址 模块内偏移量”去对应符号表。实际操作中我更推荐直接上crash工具它会自动处理KASLR和模块基址问题。crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/vmcore.xxx crash bt # 查看崩溃时的调用栈 crash log # 查看内核日志 crash mod # 列出已加载模块及地址 crash dis -f my_func # 反汇编具体函数 crash sym my_func0x21 # 解析符号附近地址bt命令输出每个进程的内核栈和日志里的Call Trace相比更完整dis命令能直接看崩溃点前后的汇编指令判断是空指针解引用、数组越界还是其他问题。我处理vmcore时基本流程永远是bt - log - mod - dis按这个顺序走一遍大多数问题就能浮出水面。4. 实战复盘NVIDIA驱动模块加载引发的Panic排查4.1 故障现象重启后卡在驱动加载阶段来一个比较有代表性的实战案例。有同事在Ubuntu 22.04上给机器装NVIDIA显卡驱动安装过程没有报错但重启后系统启动到一半直接卡死控制台输出最后几行是NVIDIA相关的内容接着就是内核Panic。因为当时没有配置任何日志保留方案只能靠手机拍下屏幕上的崩溃信息。日志里反复出现nvidia.ko相关字样其中一行类似nvidia: disagrees about version of symbol module_put这说明一个很典型的问题驱动模块和当前内核版本不匹配。NVIDIA官方驱动虽然会自带预编译模块但宿主机内核一旦升级比如Ubuntu自动安全更新旧模块就可能面临内核导出符号API变化导致加载失败或者加载后行为异常。更麻烦的是很多Ubuntu系统默认加载了开源的nouveau驱动NVIDIA闭源驱动和它同时存在会发生资源冲突严重时直接Panic。这解释了为什么安装时很正常、重启后却崩了——安装只是把文件放好真正加载模块并初始化硬件时才爆发冲突。4.2 排查路径看日志、查模块、对符号第一步永远是确认Panic发生的具体位置。当时拍的屏幕里显示类似这样的Call TraceCall Trace: [ffffffffc0a33e20] nv_ioctl0x21/0xd0 [nvidia] [ffffffffc0a32f5b] nv_kern_ioctl0x3b/0x80 [nvidia] [ffffffff84e1f1a0] do_vfs_ioctl0xa0/0x6e0 [ffffffff84e1f86a] ksys_ioctl0x8a/0xc0 [ffffffff84de0f0b] do_syscall_640x5b/0x1b0RIP落在nv_ioctl0x21/0xd0 [nvidia]也就是NVIDIA驱动的ioctl入口附近。为什么会走到这里大概率是用户态程序比如nvidia-smi或某个调用GPU接口的进程发了一个ioctl请求驱动在处理时触发了崩溃。结合Tainted: P W O确认这是一个外部模块并且有输出被贪心进程写坏的风险。接下来我执行了以下步骤定位# 查看当前内核版本 uname -r # 查看已加载模块及其版本 modinfo nvidia | head -20 # 查是否有nouveau在跑 lsmod | grep -E nouveau|nvidia结果发现nvidia模块版本是470系列内核已经是6.8而470系列驱动官方只支持到5.15左右同时nouveau在lsmod里没有出现说明驱动安装脚本已经通过blacklist禁用了它那么崩溃的主要嫌疑就集中在模块与内核版本不匹配上。为了进一步验证我下载了匹配当前内核的驱动程序包550系列在本地dkms重新编译后再加载Panic没有再出现。4.3 修复动作与验证结果修复动作分成三步清理旧模块、安装匹配版本驱动、验证加载。# 1. 彻底清理旧NVIDIA驱动 sudo apt purge nvidia-* libnvidia-* # 2. 确认nouveau已被屏蔽 echo -e blacklist nouveau\noptions nouveau modeset0 | sudo tee /etc/modprobe.d/blacklist-nouveau.conf sudo update-initramfs -u # 3. 使用DKMS方式安装新版驱动 sudo apt install dkms linux-headers-$(uname -r) sudo sh NVIDIA-Linux-x86_64-550.xx.run --dkms sudo reboot重启后执行nvidia-smi如果能看到GPU列表和驱动版本说明驱动模块加载恢复正常。这个案例最想强调的一点是NVIDIA驱动这种外部内核模块必须和当前内核版本严格对应。很多人只记显卡型号却忘了驱动是“跑在当前内核上”的内核一升级驱动就得跟着重编。顺带提一个很多人踩过的坑Ubuntu自动升级内核后旧内核还在但新内核可能没有对应的头文件。装驱动前务必确认linux-headers-$(uname -r)已安装否则DKMS编译时会报“找不到内核头文件”最后驱动根本装不上。这类问题的排查入口就是modinfo nvidia与uname -r先核对版本再动手改系统。5. 生产环境如何降低Panic影响配置与经验5.1 关键内核参数让panic后自动重启分析Panic是一回事降低Panic对业务的影响是另一回事。生产环境里我的底线是“Panic可以发生但机器必须自动恢复”否则一台关键服务器死机到天亮损失远大于服务短暂中断。最基础的两个内核参数# 让Panic后10秒自动重启 sudo sysctl -w kernel.panic10 # 让Oops也升级为Panic并执行上述重启逻辑 sudo sysctl -w kernel.panic_on_oops1 # 写入sysctl.conf持久化 echo kernel.panic 10 /etc/sysctl.conf echo kernel.panic_on_oops 1 /etc/sysctl.confkernel.panic10的含义是Panic发生后延迟10秒重启给可能的人工干预留一点窗口也避免快速重启造成日志彻底丢失。panic_on_oops1把非致命的Oops也变成Panic有些人觉得没必要但对稳定性要求高的服务来说一个内存状态已经被写坏的内核继续跑比重启的风险更大。5.2 监控、watchdog与升级策略参数配置之外硬件层面的watchdog也很值得关注。Linux内核自带的iTCO_wdt驱动支持Intel平台的TCO硬件看门狗一旦系统软死锁比如中断被关死、内核线程全部卡住硬件看门狗会在超时后强制重启这类故障软件日志根本来不及写。配置方法不复杂我一般用systemd的看门狗功能# /etc/systemd/system.conf 中开启 RuntimeWatchdogSec60监控方面Panic前的征兆往往比Panic本身更早出现。比如MCE事件、dmesg里频繁的I/O错误、Memory Error日志都是需要重点盯的指标。建议把rasdaemon这类工具装上让它把硬件错误记录记录下来内存条早期故障往往能从这些日志里发现苗头。升级策略上我给生产环境的建议是内核升级永远先在测试环境跑至少一周重点观察驱动模块兼容性外部模块统一用DKMS管理这样内核升级后模块会自动重新编译避免出现“新内核旧模块”的尴尬组合。NVIDIA驱动那个案例本质上就是升级策略没做好造成的。5.3 我踩过的坑和长期习惯这些年处理过不少Panic有两点长期习惯让我少走很多弯路。第一日志保留方案永远比分析工具重要。没有现场日志再好的crash工具都是巧妇难为无米之炊。我经手的新服务器第一件事就是配置kdump和串口输出哪怕表面上状态良好——等出事再想起来就晚了。第二不要急着重启。遇到Panic很多人第一反应是“赶紧重启恢复业务”这可以理解但在重启之前哪怕用手机拍下屏幕上所有输出也好先把现场资料留住。处理过一次极其郁闷的故障一台机器Panic后自动重启所有日志清零最后靠业务方描述“好像是跑批任务时崩的”来盲猜定位效率极低。最后分享一个小技巧如果系统还没完全失去响应可以试试SysRq组合键AltSysRqc能主动触发一次内核崩溃AltSysRqt会打印当前所有任务状态。这组键在某些半卡死状态下能帮你抢在彻底死机前拿到一份宝贵的现场快照尤其是最后几个按键的输出经常直接告诉你系统到底卡在哪个进程上。配置好kdump和pstore之后建议再花十分钟把这组键的规则记到本子角落真正遇到问题时它可能比一堆分析工具更早给你答案。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →