资讯动态

Linux内核崩溃排查实战:内存越界、死锁与引用计数泄漏

发布时间:2026/10/18 19:27:01 来源:尧图企业网站定制
1. 一次内核崩溃引发的排查复盘内核崩了。不是应用层那种抛个异常、打个日志就能定位的崩溃是整台机器直接失联SSH 断连业务全挂只能走带外管理去重启。这种场面但凡经历过一次你就知道为什么内核排查和用户态排查完全是两码事——用户态崩了有 core dump有堆栈有日志内核崩了运气好给你留个 panic 日志运气不好直接静默死机连个遗言都没有。这篇内容围绕的就是这类问题Linux 内核层面的 bug 排查。我会把最近一段时间实际处理过的几个典型案例拆开来讲包括内存越界、空指针解引用、锁竞争导致的死锁、引用计数泄漏这几类高频问题。适合谁看如果你已经在做内核模块开发、驱动调试或者运维过程中遇到过莫名其妙的宕机、OOM、soft lockup那这篇内容应该能帮你少走一些弯路。如果你刚接触内核也能从中了解到内核排查的基本思路和工具链不至于面对一台挂掉的机器完全无从下手。先说一个基本认知内核 bug 排查的核心难点不在于找到错误而在于复现错误和保留现场。很多内核问题具有极强的时序依赖性和环境依赖性同一个模块在 A 机器上跑三个月没事在 B 机器上跑三天就崩差别可能只是一个内存分配器的行为差异或者一个 CPU 调度时机的不同。所以整个排查过程本质上是在跟不确定性做斗争。2. 内核排查的底层逻辑与工具选型2.1 为什么内核排查比用户态排查难一个量级用户态程序出问题你有完整的虚拟内存空间有操作系统提供的信号机制有 gdb、valgrind、strace 这一整套成熟工具。进程崩了core dump 文件往那一放gdb 一挂堆栈清清楚楚。但内核不一样内核是所有进程共享的没有独立的地址空间隔离一个模块的越界写可能破坏的是另一个完全不相关子系统的数据结构。更麻烦的是内核崩溃时你往往没有可用的用户态环境来运行调试工具——机器都挂了你拿什么跑 gdb这就引出了内核排查的第一个核心原则现场保留优先于一切。很多人一看到机器挂了第一反应是赶紧重启恢复业务结果重启之后所有内存现场全部丢失panic 日志如果没配置持久化也一起没了。正确的做法是在重启之前尽可能把能拿到的信息全部保存下来——串口日志、panic 输出、vmcore 文件、dmesg 缓冲区内容一个都别放过。2.2 工具链选型从 printk 到 kdump 到 ftrace内核排查的工具链大致可以分为几个层次不同场景用不同工具选错了工具就是浪费时间。最基础的是printk也就是内核版的 printf。别看不起它很多问题就是靠合理的 printk 定位的。但 printk 有个致命问题它会影响时序。你在一个锁竞争的热点路径上加 printk可能直接改变竞争窗口导致 bug 消失——这就是所谓的海森堡 bug你一观测它就变了。所以 printk 要用但要有策略地用不能到处乱撒。往上一层是ftrace和perf。ftrace 可以在不修改代码的情况下跟踪内核函数调用对时序影响相对小。perf 则擅长性能剖析和热点定位。这两个工具适合排查性能相关的内核问题比如调度延迟、中断处理过长、锁持有时间异常。再往上是kdump crash。这是排查内核崩溃的杀手锏。kdump 机制会在内核 panic 时启动一个备用的 crash kernel把主内核的内存镜像 dump 成 vmcore 文件。然后你用 crash 工具加载 vmcore 和对应的 vmlinux带调试符号的内核镜像就能像 gdb 调试用户态程序一样分析内核崩溃现场。bt 看堆栈struct 看数据结构rd 读内存search 搜符号基本上用户态调试能做的事它都能做。最高层次是KASAN、KCSAN、lockdep这类动态检测工具。KASAN 检测内存越界和 use-after-freeKCSAN 检测数据竞争lockdep 检测锁依赖关系中的死锁风险。这些工具需要在编译内核时开启对应配置会带来一定的性能开销但在测试环境中开启它们能在 bug 造成实际危害之前就把它揪出来。工具适用场景性能开销是否需要重新编译内核printk逻辑错误、状态跟踪中到高否ftrace函数调用跟踪、延迟分析低否需内核支持perf性能剖析、热点定位低到中否kdumpcrash崩溃后现场分析无崩溃后才启动需配置KASAN内存越界、UAF 检测高2-3倍是lockdep死锁风险检测中是2.3 排查策略二分法与假设驱动工具选好了策略也很关键。内核排查最忌讳的是漫无目的地看代码。几万行内核代码你一行行看看到明年也看不出问题。有效的策略是假设驱动 二分法。假设驱动是指根据现象先提出一个或几个可能的假设然后设计实验去验证或排除。比如机器在跑某个网络压力测试时崩溃你的假设可能是网卡驱动的 DMA 操作有越界、协议栈的某个锁有竞争、内存分配失败路径处理有误。然后针对每个假设设计验证方法。二分法则适用于定位引入 bug 的代码变更。如果你知道某个版本之前是好的某个版本之后开始崩那就可以用 git bisect 在内核提交历史中二分查找快速定位到引入问题的那个 commit。这个方法看起来笨但极其有效尤其是面对那些看起来完全不可能有问题的代码时。3. 四类高频内核 Bug 的深度拆解3.1 内存越界最隐蔽也最致命内存越界是内核 bug 里的头号杀手。用户态越界写通常就是当前进程崩掉影响范围可控。内核态越界写轻则破坏相邻数据结构导致逻辑错乱重则直接踩坏页表或调度器数据结构整机崩溃。我处理过一个典型案例某个字符设备驱动在 ioctl 处理函数中从用户态拷贝数据到内核缓冲区时没有严格校验用户传入的长度参数。用户传了一个比实际缓冲区大的长度驱动直接 memcpy把内核栈上的返回地址给覆盖了。结果就是函数返回时跳到了一个非法地址panic。这类问题的排查思路是首先看 panic 时的堆栈如果堆栈显示在一个完全不相关的函数里崩溃或者返回地址明显不对那大概率是栈被破坏了。然后用 crash 工具检查崩溃时的栈内存看看有没有被异常数据覆盖的痕迹。如果开启了 KASAN那更简单KASAN 会直接告诉你哪次访问越界了、越界了多少字节、访问地址属于哪个对象。注意内核中所有从用户态拷贝数据的操作必须使用 copy_from_user 而不是直接 memcpy。copy_from_user 会做地址合法性检查而且能正确处理用户态页面被换出的情况。直接 memcpy 用户态指针是内核开发中的大忌。3.2 空指针解引用看似简单排查起来要命空指针解引用在内核里太常见了。用户态空指针解引用操作系统给你发个 SIGSEGV进程崩了完事。内核态空指针解引用如果没被 panic_on_oops 拦住可能只是打印一个 oops 然后继续跑但此时内核状态已经不可信了后续可能引发更严重的问题。有个案例让我印象很深一个内核模块在初始化时从设备树中读取配置参数如果设备树里没有对应节点of_find_node_by_name 返回 NULL但代码没有检查返回值就直接访问了 node-properties。结果就是空指针解引用。这个问题在开发板上跑没事因为设备树配置完整到了客户现场设备树被裁剪过节点没了一加载模块就 oops。排查空指针问题的关键是看 oops 信息里的Unable to handle kernel NULL pointer dereference at virtual address 0000000000000000这一行以及后面的 PC 值和堆栈。PC 值告诉你崩在哪个函数堆栈告诉你调用链。然后结合源码看那个函数里哪个指针可能为 NULL。3.3 锁竞争与死锁时序的魔鬼锁的问题是最难排查的因为它的触发依赖精确的时序。两个线程以不同的顺序获取两把锁大多数时候没事偶尔交叉执行就死锁了。这种问题可能跑几天才出现一次复现难度极大。lockdep 是排查这类问题的利器。它不需要问题实际发生只需要在代码中存在不合理的锁获取顺序lockdep 就会在运行时检测到并打印警告。我强烈建议在所有测试版本的内核中开启 lockdep它带来的性能开销远小于它帮你避免的调试成本。如果问题已经发生了机器卡死但没 panic那首先要确认是不是死锁。方法是通过 sysrq 触发所有 CPU 的堆栈打印echo l /proc/sysrq-trigger看看是不是有多个 CPU 卡在同一个锁上。如果是再看这些 CPU 的调用链找出锁的获取顺序。3.4 引用计数泄漏慢性毒药引用计数泄漏不会立刻导致崩溃但会慢慢积累最终导致内存耗尽或者对象无法释放。kobject、device、module 这些内核对象都有引用计数用 kref 或 atomic_t 管理。泄漏的原因通常是在某个错误处理路径上忘记调用 put或者 get 和 put 不配对。排查引用计数泄漏最直接的方法是看 /sys/kernel/debug/ 下对应子系统的调试信息很多子系统会导出对象的引用计数状态。另外kmemleak 工具可以检测内核内存泄漏它会定期扫描内存找出那些没有任何指针指向但也没有被释放的内存块。4. 从崩溃现场到根因定位的完整实操4.1 第一步保留现场配置 kdumpkdump 的配置分几步。首先要在内核命令行中预留一块内存给 crash kernel通过 crashkernel256M 这样的参数指定。然后在用户态安装 kexec-tools配置 /etc/kdump.conf 指定 vmcore 的保存路径。最后启动 kdump 服务确认 crash kernel 已经加载。# 检查 crash kernel 是否已预留 cat /proc/cmdline | grep crashkernel # 启动 kdump 服务 systemctl start kdump systemctl enable kdump # 确认 kdump 状态 systemctl status kdump # 手动触发一次 crash 测试谨慎操作 echo c /proc/sysrq-trigger重启后vmcore 文件会保存在 /var/crash/ 目录下。用 crash 工具加载crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/xxx/vmcore进入 crash 交互界面后常用的命令有bt查看崩溃时的堆栈bt -a查看所有 CPU 的堆栈log查看内核日志缓冲区ps查看进程列表struct 结构体名 地址查看指定地址的结构体内容rd 地址读取内存dis 函数名反汇编指定函数4.2 第二步解读 panic 信息panic 信息里包含了很多关键线索但很多人只看最后几行就懵了。正确的解读顺序是先看触发 panic 的原因比如 BUG: unable to handle kernel paging request at ffff... 或者 kernel BUG at mm/slub.c:1234。这告诉你问题的类型。然后看 CPU 寄存器的值特别是 RIP指令指针、RSP栈指针、CR2页错误地址。RIP 告诉你崩在哪条指令CR2 告诉你访问了哪个非法地址。接着看 Call Trace这是函数调用链。从下往上看最下面的是最外层的调用者最上面的是崩溃点。注意看有没有 ? 标记带问号的帧表示可能不准确。最后看 Code: 那一行这是崩溃点附近的机器码结合反汇编可以确认具体是哪条指令出了问题。4.3 第三步用 crash 做深度分析进入 crash 后我通常按这个顺序操作# 1. 先看崩溃时的堆栈 crash bt # 2. 看所有 CPU 在干什么排除死锁可能 crash bt -a # 3. 看内核日志找 panic 前的异常信息 crash log | tail -100 # 4. 如果怀疑内存越界看崩溃地址附近的内存 crash rd ffff888012345000 64 # 5. 如果怀疑某个结构体被破坏打印它 crash struct my_device ffff888012345000 # 6. 查看模块加载地址确认符号对应关系 crash mod有一个技巧很实用如果你怀疑某个函数有问题可以用 dis 反汇编它然后对照源码看汇编层面到底做了什么。有时候编译器优化会让源码和实际执行逻辑产生差异看汇编能发现一些源码层面看不出来的问题。4.4 第四步复现与验证找到疑似根因后必须设计实验验证。验证方法包括如果怀疑是某个 commit 引入的用 git bisect 确认如果怀疑是某个条件触发的构造最小复现用例如果怀疑是内存越界开启 KASAN 重新跑测试如果怀疑是锁问题开启 lockdep 重新跑测试只有能稳定复现并验证修复方案有效才算真正完成了排查。5. 排查过程中踩过的坑与速查手册5.1 那些年我们误判过的根因坑一把现象当原因。看到 OOM 就以为是内存泄漏结果查了半天发现是某个进程突然申请了大量内存而那个进程本身是正常的真正的问题在于内核的内存回收机制没有及时触发。OOM 只是结果不是原因。坑二忽略配置差异。开发环境内核配置和产线环境不一样开发环境开了 DEBUG 选项很多问题被检测出来了产线环境为了性能关掉了这些选项问题就变成了静默崩溃。排查时一定要确认两边内核配置的差异。坑三过度依赖 printk。前面说过printk 会改变时序。我遇到过一个问题加了 printk 就复现不了去掉就复现。后来用 ftrace 替代 printk 才定位到问题。坑四忽视硬件因素。有些内核 bug其实是硬件问题比如内存条故障导致随机位翻转CPU 缓存问题导致数据不一致。排查到一定程度如果毫无头绪不妨跑一下内存测试和 CPU 压力测试。5.2 常见问题速查表现象可能原因排查工具快速验证方法整机静默死机死锁、硬件故障sysrq bt -a触发 sysrq 看 CPU 堆栈panic 带非法地址内存越界、UAFKASAN、crash开启 KASAN 重跑OOM 但内存充足内存碎片、cgroup 限制/proc/buddyinfo、cgroup 信息检查碎片和限制配置随机 oops竞态、未初始化变量KCSAN、lockdep开启检测工具重跑模块加载失败符号依赖、版本不匹配dmesg、modinfo看 dmesg 具体报错性能突然下降锁竞争、中断风暴perf、ftraceperf top 看热点5.3 独家避坑技巧技巧一在测试内核中默认开启 KASAN 和 lockdep。性能损失在测试环境可以接受但能帮你提前发现大量问题。产线内核再关掉这些选项。技巧二给 panic 日志加时间戳和 CPU 编号。多核系统上不同 CPU 的日志交织在一起没有时间戳根本理不清顺序。通过内核参数 printk.time1 开启时间戳。技巧三用 pstore 保存崩溃日志。pstore 可以把 panic 日志保存到 ACPI ERST 或 RAMoops 区域重启后还能读取。对于没有配置 kdump 的环境这是最后的救命稻草。技巧四建立内核崩溃案例库。每次排查完一个问题把 panic 信息、根因、修复方案整理成文档。下次遇到类似现象先查案例库能省大量时间。技巧五不要忽视 warning。内核的 WARN_ON 和 WARN_ON_ONCE 打印的警告很多人觉得不影响运行就忽略了。但这些警告往往是更严重问题的前兆认真对待每一个 warning能避免很多 panic。6. 内核排查能力的长期建设内核排查这件事工具和技巧是一方面更重要的是知识积累和思维方式。你需要对内核的各个子系统有基本的了解——内存管理、进程调度、文件系统、网络协议栈、设备驱动模型不要求每个都精通但至少要知道它们大致是怎么工作的出了问题时该往哪个方向想。我个人的习惯是每读一个子系统的代码就画一张数据结构关系图和调用流程图。这些图在排查问题时极其有用因为内核崩溃往往表现为一个看似不相关的子系统出了问题如果你有全局的数据结构视图就能快速判断出崩溃点附近的数据结构可能被谁破坏了。另外多看看内核社区的问题讨论和补丁提交。很多你遇到的问题别人已经遇到过了而且可能有现成的修复方案。内核邮件列表、bugzilla、各种技术社区都是宝贵的资源。但要注意看别人的解决方案时一定要理解背后的原理不能照搬因为环境差异可能导致同样的方案在你这里不生效。最后说一点关于心态的。内核排查有时候确实很折磨人一个问题可能查一周都没有进展。这时候不妨换个思路或者先放一放让潜意识去处理。我有很多次都是在洗澡或者散步的时候突然想到关键线索的。保持耐心保持好奇每一个被你揪出来的内核 bug都是对系统稳定性的一份贡献。

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

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

免费获取报价 →
↑