资讯动态

Linux内核崩溃排查实战:从kdump到竞态条件定位

发布时间:2026/10/17 21:28:16 来源:尧图企业网站定制
1. 一次内核崩溃引发的排查复盘内核崩溃这件事很多人第一反应是重启就好了。我最初也是这么想的。直到某次线上环境连续三天在凌晨两点左右触发内核 panic重启后跑不到六小时又挂才意识到问题的严重性。那台机器承载着核心数据同步任务每次崩溃都意味着至少半小时的数据延迟业务方已经开始在群里所有人了。这篇文章要聊的就是Linux内核bug排查这件事。具体来说我会把这次排查的完整过程拆开揉碎从日志分析、现场保护、复现路径构造到最终定位到一个内存管理子系统的竞态条件每一步都讲清楚为什么这么做、踩过哪些坑、有哪些可以复用的技巧。适合谁看如果你正在被内核崩溃、系统hang死、OOM误报、驱动异常这类问题折磨或者你是个后端开发/运维/SRE迟早要跟内核打交道那这篇内容应该能帮你省下不少深夜加班的时间。内核排查跟应用层排查最大的区别在于你没有printf可用。应用层崩了加个日志、打个堆栈问题基本就清楚了。内核崩了整个系统直接躺平你只能靠崩溃前留下的那点线索去反推。这就好比刑侦破案现场已经被破坏了你得从弹壳、指纹、监控录像的碎片里还原真相。所以内核排查的核心能力其实是现场保护和线索提取。我这次遇到的问题表面上看是内存分配失败导致的panic但真正的原因藏得很深。整个排查过程大概持续了一周中间走了不少弯路也积累了一些我觉得值得分享的经验。下面我会按照实际排查的顺序来展开但会把每个环节的原理和操作细节都补全方便你直接抄作业。2. 内核崩溃现场的第一反应与信息采集2.1 崩溃日志到底该怎么读内核崩溃时控制台会打印一大段信息很多人看到就懵了。其实这段信息是有固定结构的我把它拆成几个关键部分Oops/panic头部告诉你崩溃类型是空指针解引用、页错误、还是主动触发的BUG()。CPU寄存器快照RIP指令指针指向崩溃时执行的指令地址这是最关键的线索。调用栈Call Trace函数调用链从下往上看能还原出代码执行路径。内核版本与模块列表确认环境排除版本不匹配导致的误判。我那次崩溃的日志头部是这样的脱敏后BUG: unable to handle kernel NULL pointer dereference at 0000000000000018 RIP: 0010:mem_cgroup_charge0x3a/0x1a0 Call Trace: IRQ page_add_new_anon_rmap0x2f/0x80 do_anonymous_page0x1c5/0x6d0 __handle_mm_fault0x5e3/0x8a0 handle_mm_fault0x9c/0x160 do_page_fault0x1e2/0x4a0看到mem_cgroup_charge里偏移0x3a处访问了NULL0x18第一反应是某个cgroup相关的指针为空。但这里有个陷阱RIP指向的地址不一定是问题根源。编译器优化、内联、指令重排都可能让RIP偏移看起来很奇怪。所以不能只看这一行必须结合调用栈和寄存器一起分析。提示如果崩溃日志被截断了优先找RIP、Call Trace、CR2页错误地址这三个字段。CR2能告诉你到底访问了哪个非法地址比RIP更直接。2.2 现场保护别急着重启我见过太多人一看到panic就条件反射按重启键。这是排查内核bug最大的忌讳。重启意味着所有现场信息丢失你只能等下次崩溃而下次崩溃可能几天后才出现。正确的做法是配置kdump让内核在崩溃时自动把内存镜像dump到磁盘。这是事后分析的基础。保留串口日志如果机器有串口接上串口线所有内核输出都会实时打印到另一台机器上。没有串口的话配置netconsole把日志通过网络发出去。记录崩溃时间点精确到秒方便跟其他监控数据对齐。kdump的配置其实不复杂但有几个坑我踩过# 检查内核是否支持kdump grep CONFIG_CRASH_DUMP /boot/config-$(uname -r) # 安装kexec-tools yum install kexec-tools # 或 apt install kexec-tools # 配置crashkernel预留内存 # 在grub里加上 crashkernel256M # 注意预留内存不能太小否则dump会失败注意crashkernel的大小要根据物理内存来定。内存越大dump文件越大预留空间也要相应增加。我一般按物理内存的1/16来预留最小256M。如果dump失败先检查这个参数。2.3 用crash工具做初步分析拿到vmcore文件后用crash工具打开crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/vmcore进去之后我习惯先跑几个固定命令命令作用我的使用场景bt查看崩溃时的调用栈确认崩溃线程和上下文log查看内核日志缓冲区还原崩溃前的最后输出ps查看进程状态确认是否有异常进程vm查看内存使用判断是否OOM相关kmem -s查看slab分配情况排查内存泄漏那次bt的输出显示崩溃发生在软中断上下文IRQ标记这很关键。软中断上下文里不能睡眠如果代码路径里有任何可能睡眠的操作就会出问题。顺着这个线索我重点看了mem_cgroup_charge的调用链发现它是在匿名页缺页异常处理中被调用的。3. 从调用栈反推问题根源3.1 理解缺页异常的处理流程要搞清楚为什么mem_cgroup_charge会崩得先明白匿名页缺页异常的处理流程。我用一个生活化的类比来解释想象你去图书馆借书分配内存。正常情况下你先查目录页表发现书不在架上页表项为空然后去书库找分配物理页找到后登记借阅记录建立映射最后把书给你返回用户空间。这个过程在内核里对应CPU访问虚拟地址发现页表项无效触发缺页异常。内核的do_page_fault接管判断异常类型。对于匿名页没有文件背景的内存调用do_anonymous_page。分配物理页框然后调用page_add_new_anon_rmap建立反向映射。在建立映射前需要向内存控制组memcg记账这就是mem_cgroup_charge。问题就出在第5步。mem_cgroup_charge需要访问当前进程所属的memcg结构但如果这个结构在某些并发场景下被释放了就会访问到空指针。3.2 竞态条件的发现过程单看一次崩溃很难判断是竞态。我的做法是统计多次崩溃的共性崩溃时间都在凌晨2点左右正好是定时任务批量启动的时间。崩溃CPU不固定说明不是某个CPU的硬件问题。崩溃进程都是同一个数据同步进程的子线程。调用栈完全一致。这些共性指向一个结论问题跟特定进程的行为相关且跟并发有关。接下来我在测试环境构造复现。思路是用多线程频繁创建和销毁cgroup同时触发大量匿名页分配。大概跑了三小时复现了。复现后立刻用crash分析发现崩溃时memcg的引用计数为0但代码路径还在使用它。这里涉及一个内核内存管理的经典问题引用计数与生命周期管理。memcg结构在进程退出时会被释放但如果此时还有缺页异常在处理中且该异常路径持有的是旧的memcg指针就会出问题。修复方案通常是在关键路径上加引用计数保护或者用RCU机制延迟释放。3.3 用ftrace追踪函数调用为了确认竞态窗口我用了ftrace。ftrace是内核自带的追踪工具不需要额外安装功能却非常强大。# 挂载tracefs mount -t tracefs none /sys/kernel/tracing # 设置追踪函数 echo mem_cgroup_charge /sys/kernel/tracing/set_ftrace_filter echo function /sys/kernel/tracing/current_tracer echo 1 /sys/kernel/tracing/tracing_on # 跑测试用例 # ... # 查看结果 cat /sys/kernel/tracing/trace通过ftrace的输出我看到了mem_cgroup_charge和mem_cgroup_put的调用时序。在某些时间窗口内put先于charge执行导致charge访问了已释放的结构。这就是竞态的实锤。实操心得ftrace的function_graph模式比function模式更直观能看到函数进出和耗时。但开销也更大生产环境慎用。测试环境随便跑。4. 内核调试的常用武器库4.1 printk与动态调试虽然我说内核里没有printf但printk是有的。只是它有几个限制日志级别、缓冲区大小、不能睡眠。用printk调试内核模块时我一般用pr_debug配合动态调试// 在代码里加 pr_debug(memcg pointer: %p, refcnt: %d\n, memcg, atomic_read(memcg-refcnt)); // 运行时开启 echo file memcontrol.c p /sys/kernel/debug/dynamic_debug/control动态调试的好处是默认不输出需要时再开对性能影响小。生产环境排查偶发问题时特别有用。4.2 kprobe与eBPFkprobe可以在几乎任何内核函数上插桩eBPF则是更现代的方案。我这次排查后期用了eBPF来统计memcg的生命周期// 伪代码示意 kprobe/mem_cgroup_charge { start[tid] nsecs; } kretprobe/mem_cgroup_charge { time hist(nsecs - start[tid]); delete(start[tid]); }这样能看到mem_cgroup_charge的耗时分布。如果某些调用耗时异常长说明可能发生了锁竞争或内存回收这些都是竞态的温床。注意eBPF需要内核版本支持一般4.9且需要CONFIG_BPF相关配置。老内核只能用kprobe或systemtap。4.3 锁与并发问题的排查思路内核并发问题排查有个基本套路确认是否真的并发看崩溃是否只在多CPU、多线程场景出现。找共享数据崩溃点访问的数据结构哪些字段是共享的查保护机制共享数据有没有锁、引用计数、RCU保护验证保护是否失效用ftrace或kprobe观察锁的获取释放时序。我这次的问题共享数据是memcg结构保护机制是引用计数。失效原因是引用计数的获取和释放不在同一个临界区中间有窗口。修复方案是在do_anonymous_page路径上提前获取引用确保整个缺页处理期间memcg不会被释放。5. 常见内核问题的速查与避坑5.1 内核问题速查表现象可能原因排查方向空指针解引用结构体未初始化、竞态释放看RIP偏移、检查引用计数页错误非法地址访问、权限问题看CR2、检查页表软锁死死循环、锁未释放看NMI watchdog、检查锁OOM内存泄漏、cgroup限制看meminfo、slab统计内核hang死锁、硬件问题看所有CPU栈、检查锁顺序文件系统只读磁盘错误、内核bug看dmesg、检查磁盘健康5.2 我踩过的坑坑一只看RIP不看调用栈。RIP偏移可能因为内联而指向无关函数调用栈才是执行路径的真相。坑二忽略时间相关性。我一开始没注意崩溃都在凌晨2点浪费了两天时间。后来把崩溃时间和cron任务对齐立刻有了方向。坑三在生产环境直接开ftrace。ftrace的function模式全量追踪时开销很大我在生产环境开了一次直接把机器负载拉满差点引发二次故障。后来改成只在测试环境用或者用set_ftrace_filter限制范围。坑四忘记保存原始vmcore。分析过程中可能会用crash修改内存视图如果没保存原始文件后面想重新分析就麻烦了。我现在的习惯是vmcore文件只读分析时复制一份。5.3 内核排查的心态建设内核bug排查是个耐心活。我这次一周才定位中间有几次觉得肯定是硬件问题想放弃。但每次想放弃时我就问自己如果是硬件问题为什么每次崩溃的调用栈都一样这个反问帮我坚持了下来。另外不要一个人死磕。内核社区、邮件列表、IRC频道都是资源。我最后是在一个内核开发者的邮件列表里搜到了类似问题的讨论才确认了修复方向。搜索关键词要精准比如mem_cgroup_charge NULL pointer race condition比kernel crash有效得多。6. 从修复到验证的完整闭环6.1 补丁的编写与测试定位到问题后修复本身反而简单了。核心改动是在do_anonymous_page里提前获取memcg引用// 修改前 mem_cgroup_charge(page, vma-vm_mm, GFP_KERNEL); // 修改后 memcg get_mem_cgroup_from_mm(vma-vm_mm); if (!memcg) return -ENOMEM; mem_cgroup_charge(page, memcg, GFP_KERNEL); mem_cgroup_put(memcg);但改完不能直接上生产必须经过单元测试内核有tools/testing/selftests跑相关用例。压力测试用stress-ng或自己写的多线程测试跑至少24小时。回归测试确认没有引入新的崩溃或性能退化。我那次跑了72小时压力测试期间用perf监控了内存分配延迟确认没有明显退化才上线。6.2 验证修复效果上线后怎么确认修好了我的做法是监控崩溃率之前每天1-2次修复后一周零崩溃。监控memcg生命周期用eBPF统计memcg创建销毁次数确认没有异常。保留回滚方案内核补丁回滚比应用麻烦所以提前准备了旧内核镜像万一有问题能快速切换。提示内核补丁上线后至少观察一个完整的业务周期比如一周确认没有在特定业务场景下触发新问题。6.3 沉淀与分享问题解决后我把整个排查过程整理成了内部文档包括崩溃日志的完整分析路径kdump配置模板ftrace和eBPF的使用示例竞态问题的通用排查框架这份文档后来帮团队里其他人解决了两个类似问题。内核排查的经验就是这样一次踩坑长期受益。7. 一些额外的实战建议如果你刚开始接触内核排查我建议从这几个方向入手第一熟悉crash工具。把bt、log、ps、vm、kmem这几个命令练熟大部分崩溃分析都够用了。第二学会读内核源码。不用全读但崩溃涉及的函数一定要读。我这次把mm/memory.c和mm/memcontrol.c的相关函数读了三遍才理清调用关系。第三建立自己的排查清单。每次遇到新问题把排查步骤记下来慢慢就形成体系了。我的清单现在有二十多条覆盖了从日志采集到补丁验证的全流程。第四关注内核版本差异。同一个函数在不同内核版本里可能行为完全不同。我这次的问题在5.10上有在5.15上已经被重构掉了。所以排查前先确认内核版本再看对应版本的源码。第五善用社区资源。内核邮件列表、bugzilla、各种技术博客都是宝库。但搜索时要注意很多问题已经被修复了你要找的是跟你内核版本匹配的讨论。最后说个我自己的体会内核排查最难的其实不是技术而是在信息不足的情况下做判断。你永远拿不到完整的信息永远要在不确定性中前进。这时候经验就很重要了而经验只能靠一次次实战积累。所以别怕遇到内核问题每解决一个你的能力边界就往外扩了一圈。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑