news 2026/10/17 21:28:00

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux内核崩溃排查实战:从kdump到竞态条件定位

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_charge+0x3a/0x1a0 Call Trace: <IRQ> page_add_new_anon_rmap+0x2f/0x80 do_anonymous_page+0x1c5/0x6d0 __handle_mm_fault+0x5e3/0x8a0 handle_mm_fault+0x9c/0x160 do_page_fault+0x1e2/0x4a0

看到mem_cgroup_charge里偏移0x3a处访问了NULL+0x18,第一反应是某个cgroup相关的指针为空。但这里有个陷阱:RIP指向的地址不一定是问题根源。编译器优化、内联、指令重排都可能让RIP偏移看起来很奇怪。所以不能只看这一行,必须结合调用栈和寄存器一起分析。

提示:如果崩溃日志被截断了,优先找RIP、Call Trace、CR2(页错误地址)这三个字段。CR2能告诉你到底访问了哪个非法地址,比RIP更直接。

2.2 现场保护:别急着重启

我见过太多人一看到panic就条件反射按重启键。这是排查内核bug最大的忌讳。重启意味着所有现场信息丢失,你只能等下次崩溃,而下次崩溃可能几天后才出现。

正确的做法是:

  1. 配置kdump:让内核在崩溃时自动把内存镜像dump到磁盘。这是事后分析的基础。
  2. 保留串口日志:如果机器有串口,接上串口线,所有内核输出都会实时打印到另一台机器上。没有串口的话,配置netconsole把日志通过网络发出去。
  3. 记录崩溃时间点:精确到秒,方便跟其他监控数据对齐。

kdump的配置其实不复杂,但有几个坑我踩过:

# 检查内核是否支持kdump grep CONFIG_CRASH_DUMP /boot/config-$(uname -r) # 安装kexec-tools yum install kexec-tools # 或 apt install kexec-tools # 配置crashkernel预留内存 # 在grub里加上 crashkernel=256M # 注意:预留内存不能太小,否则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会崩,得先明白匿名页缺页异常的处理流程。我用一个生活化的类比来解释:

想象你去图书馆借书(分配内存)。正常情况下,你先查目录(页表),发现书不在架上(页表项为空),然后去书库找(分配物理页),找到后登记借阅记录(建立映射),最后把书给你(返回用户空间)。

这个过程在内核里对应:

  1. CPU访问虚拟地址,发现页表项无效,触发缺页异常。
  2. 内核的do_page_fault接管,判断异常类型。
  3. 对于匿名页(没有文件背景的内存),调用do_anonymous_page。
  4. 分配物理页框,然后调用page_add_new_anon_rmap建立反向映射。
  5. 在建立映射前,需要向内存控制组(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与eBPF

kprobe可以在几乎任何内核函数上插桩,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 锁与并发问题的排查思路

内核并发问题排查有个基本套路:

  1. 确认是否真的并发:看崩溃是否只在多CPU、多线程场景出现。
  2. 找共享数据:崩溃点访问的数据结构,哪些字段是共享的?
  3. 查保护机制:共享数据有没有锁、引用计数、RCU保护?
  4. 验证保护是否失效:用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);

但改完不能直接上生产,必须经过:

  1. 单元测试:内核有tools/testing/selftests,跑相关用例。
  2. 压力测试:用stress-ng或自己写的多线程测试跑至少24小时。
  3. 回归测试:确认没有引入新的崩溃或性能退化。

我那次跑了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、各种技术博客都是宝库。但搜索时要注意,很多问题已经被修复了,你要找的是跟你内核版本匹配的讨论。

最后说个我自己的体会:内核排查最难的其实不是技术,而是在信息不足的情况下做判断。你永远拿不到完整的信息,永远要在不确定性中前进。这时候经验就很重要了,而经验只能靠一次次实战积累。所以别怕遇到内核问题,每解决一个,你的能力边界就往外扩了一圈。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 21:26:43

GreatSQL数据库原理实验:部署、建表、查询与避坑全流程

简介&#xff1a;郑州大学计算机与人工智能学院《数据库系统原理》课程实验报告&#xff0c;适合数据库初学者、正在完成同类实验的高校学生及复习备考者。报告完整记录了数据库原理实验的关键环节&#xff0c;重点展示万里数据库GreatSQL在CentOS虚拟机中的安装、配置与使用流…

作者头像 李华
网站建设 2026/10/17 21:14:56

滑块验证码动态参数cb定位方法:前端JS分析与断点调试完整思路

做前端分析的人&#xff0c;迟早会遇到滑块验证码。前两年我刚开始接触这类问题时&#xff0c;也是一头雾水&#xff0c;尤其看到请求里那个神秘的cb参数&#xff0c;长度不一、每次都在变&#xff0c;根本不知道它是从哪儿冒出来的。后来跟着请求链路一步步往下查&#xff0c;…

作者头像 李华
网站建设 2026/10/17 21:13:11

AstronRPA开源平台:融合AI Agent的企业级RPA部署与实战指南

我最初想找类似影刀那种商业工具的替代品&#xff0c;看到AstronRPA时确实愣了一下——科大讯飞把一个企业级RPA AI Agent平台给开源了。这篇我会从项目架构拆起&#xff0c;再带大家把控制台、执行器都跑起来&#xff0c;最后聊聊和影刀、n8n这种工具的横向差异&#xff0c;以…

作者头像 李华
网站建设 2026/10/17 21:12:52

深度学习100道选择题:从题库设计到能力评估的落地路径

简介&#xff1a;这是一份面向深度学习初学者与备考人群的专项练习资料&#xff0c;以100道选择题的形式覆盖机器学习与深度学习的核心知识点&#xff0c;帮助学习者在刷题中检验理论掌握程度、查漏补缺。题目涉及监督与无监督学习、梯度下降、交叉验证、准确率与F1分数等评估指…

作者头像 李华
网站建设 2026/10/17 21:10:07

Capsule Networks原理与实战:动态路由与姿态矩阵详解

1. 项目概述&#xff1a;为什么Capsule Networks不是“又一个新网络”&#xff0c;而是对神经网络底层逻辑的一次重审Capsule Networks胶囊网络&#xff08;二&#xff09;——这个标题里藏着一个被多数人忽略的关键信号&#xff1a;“&#xff08;二&#xff09;”。它不是孤立…

作者头像 李华
网站建设 2026/10/17 21:07:29

Unity游戏数据管理:Excel转SQLite实战指南

1. 为什么非得把Excel塞进Unity的SQLite里&#xff1f;——一个被低估的数据管理真相很多人第一次听说“Excel转SQLite再进Unity”&#xff0c;第一反应是&#xff1a;这不折腾吗&#xff1f;Excel双击就开&#xff0c;Unity里写个CSV读取几行代码完事&#xff0c;何必绕这么大…

作者头像 李华