内核报错 kernel paging request 排查:分清驱动还是内存故障
服务器半夜报警拉到控制台一看屏幕上滚动着一行刺眼的BUG: unable to handle kernel paging request at ffff8800...。这行字对不少运维人来说既熟悉又头疼熟悉是因为它一出现通常意味着系统已经离宕机不远了头疼是因为紧接着的问题就来了——这到底是内存坏了还是驱动写崩了我在生产环境里处理过不少这类故障说句实话不存在“一招定生死”的判断方法。这条报错本身只是内核在访问一个无效虚拟地址时抛出的异常任何运行在内核态的可疑代码或者一块悄悄翻转比特位的内存条都可能造成它。要分清是硬件还是软件你得把 dmesg、系统日志、硬件错误记录放在一起看必要的时候再做一轮压力和换件测试。下面这些内容适合正在机房排查问题的运维同事也适合刚接触 Linux 服务器、想搞懂内核日志的人。1. 从一行 kernel 日志反推发生了什么1.1 分页请求到底在“请求”什么CPU 访问内存时不是直接拿物理地址干活而是经过 MMU内存管理单元查一张“页表”把虚拟地址翻译成物理地址。如果某一个虚拟地址在页表里压根没有对应项或者权限不对CPU 就会触发一次缺页异常。对用户态进程来说这种异常很常见比如进程访问了未映射的内存内核直接给它一个信号进程自己就崩溃了日志里也只会看到一个 “Segmentation fault”。但内核自己的代码跑在更底层当它访问一个地址触发缺页异常、而内核又认为这个地址“不应该无法解析”时就无法像处理用户进程那样优雅地赶走了。系统会打印一行BUG: unable to handle kernel paging request at 地址随后把现场一股脑丢出来。整个过程大体上是一个“内核版的段错误”只不过它发生在最高权限的上下文里轻则让系统进入不稳定状态重则直接 panic 重启。所以单看这行报错只能说明两件事第一触发访存的指令在内核态第二目标地址没有映射或者映射无效。至于“是谁去访问的”“为什么会访问到一个无效地址”全要看日志后面打印的 RIP、Oops、Call Trace 和寄存器信息。1.2 日志里除了 BUG 还要看什么一段完整的报错通常长下面这个样子不用在意具体地址和函数名关键是格式和字段BUG: unable to handle kernel paging request at ffff8800a1b0c000 IP: [ffffffff81234567] do_something0x2b/0x80 Oops: 0000 [#1] SMP Modules linked in: demo_net(O) nf_tables ip_tables ... CPU: 3 PID: 1234 Comm: kworker/3:0 Not tainted 5.4.0-xxx Hardware name: ... RIP: 0010:[ffffffff81234567] do_something0x2b/0x80 RSP: 0018:ffff8800... EFLAGS: 00010246 CR2: ffff8800a1b0c000 Call Trace: [ffffffff81237789] some_caller0x12/0x40 [ffffffff8105f865] handle_irq_event0x65/0x1e0 ... Code: 48 8b 00 48 89 4c 24 08 48 85 c0 75 f4 48 83 c4 10 5b 41 5c 41 5d 41 5e 41 5f c3 ...第一行里at ffff8800a1b0c000给出的是触发异常的虚拟地址一般也会出现在后面的CR2寄存器里。IP行指明是哪一条指令、在哪个函数里出的事比如do_something0x2b表示偏移到该函数第 0x2b 字节处。Oops: 0000 [#1] SMP里的0000是缺页错误码#1表示这是系统启动后第几次 Oops如果看到了#3、#4那基本可以确定它不是一次性偶发问题。后面的Modules linked in看似啰嗦其实信息量很大它列出了当前加载的所有内核模块如果有第三方驱动模块挂在里面排查方向会立刻被拉过去。要注意的是日志里的地址分为内核空间地址和用户空间地址。类似ffff8800...、ffffffff...这种高位地址通常属于内核或模块所在的虚拟区间而类似0000000000000048这种特别小的地址多半是空指针加偏移典型代码逻辑 bug。这一条本身就能作为初步锚点但还不能直接下结论内存坏块也可能让代码跑到一个看似可笑的地址上。2. 驱动嫌疑哪些信号把矛头指向软件2.1 RIP 落点最先要看的“案发坐标”判断是不是驱动问题第一步永远盯住RIP。如果报错的IP或RIP后面直接跟了一个模块名比如RIP: [ffffffffa08f52d0] demo_net_poll0x1e/0xb0 [demo_net]那说明出事的指令在名为demo_net的网卡驱动模块里。这种解析结果是最响亮的信号——至少驱动和故障现场有直接关系。后面 Call Trace 里如果有中断处理入口、NAPI 轮询入口一类的调用链基本能锁定是网卡驱动在收包或发包时访问了一个已经被释放的内存对象。但这里有个非常容易踩的坑驱动模块名出现在 RIP 里不一定代表驱动就是根因。内存坏块如果破坏了该驱动代码所在页、驱动内部的指针、甚至页表本身崩溃现场也会落在驱动代码里。所以看到 RIP 在模块里时还要结合“是否稳定复现”来交叉验证。2.2 Call Trace、调用链和模块加载时间Call Trace 展示的是从异常点往回追溯的调用路径它回答的问题是这条指令是被谁调进来的。比如一次网络收包路径上的崩溃调用链会从handle_irq_event一路走到demo_net_isr、再到demo_net_poll一次磁盘 I/O 路径上的崩溃调用链会带着块设备层、驱动队列函数等符号。如果除了当前 RIP 外整个调用链里其他函数几乎都来自同一个驱动模块或同一条子系统路径那软件 bug 的嫌疑会非常大。再配合时间线驱动是什么时候装的、内核什么时候升级过、固件版本什么时候更新过往往就能把范围缩小到一两次变更窗口里。我个人的习惯是把每次 Oops 单独存成一个文件然后把RIP和Call Trace提取出来做对比。软件问题有个明显特征反复崩溃时 RIP 经常落在同一个模块的同一个偏移附近Call Trace 也高度雷同。这是内存问题一般不具备的。2.3 常见的驱动“背锅”场景生产环境里这种 Oops 最常见的触发场景无非三类。第一类是 out-of-tree 驱动也就是厂商单独发布、不随主线内核走的驱动常见于网卡、HBA 卡、GPU 加速卡这类设备。它们在一个内核版本下编译好结果升级内核后没有重新编译或者没有跟随新内核接口适配加载后就开始在特定路径上出问题。内核日志里如果出现Tainted: P O一类的标记P 表示加载了专有模块O 表示加载了树外模块排查优先级要往上提。第二类是硬件和固件版本不匹配。比如某个网卡型号更新了光模块但网卡固件还是老版本驱动在读取模块信息时可能拿到非法数据再把这个数据当成指针搬运一搬就搬到没有映射的地址上。这类问题通常出现在重启后首次收发包、链路切换、或者插拔模块之后。第三类是驱动在热移除或多队列环境下自我踩踏。网络设备关闭队列、绑定/解绑 CPU、链路 down/up 的瞬间驱动里残留的中断或任务还在访问已经释放的队列结构也会指向无效内存。这类问题跟特定操作强相关并不是随时都在崩所以在时间上要抓“最后一次人为动作是什么”。3. 内存嫌疑随机地址、ECC 报警与物理故障3.1 地址会说话随机 CR2 与大段空洞内存问题的最大特点用一个词概括就是“随机”。同一个系统反复 Oops第一次指向ext4的某个函数第二次落在网络协议栈里第三次干脆RIP指向一个不在任何已加载模块范围内的地址这种分布没有规律。而且CR2里的地址一会儿是高位的ffff8800...一会儿是像ffff9c01...这样明显偏向另一个内存区段的值甚至出现了“看起来合理但完全不知道属于谁”的地址说明很可能是有物理颗粒在吞数据。如果日志里还伴随其他诡异现象比如打印出来的十六进制 dump 内容明显混乱、同一段内核函数代码反汇编出来是乱码、或者 Oops 的日志行本身都出现了丢字符那基本可以判定不是代码逻辑问题因为代码不会时好时坏内存才会。这时候我不建议继续盯着调用链猜而是主动去查硬件错误日志。3.2 EDAC 与正确错误日志是真正的红色预警服务器如果用的是 ECC 内存内存控制器会自己发现并纠正单比特错误但它在后台会记账。这份记录就是区分驱动问题和内存问题的黄金证据。先看内核里有没有加载 EDAC 驱动然后直接读计数器grep -H . /sys/devices/system/edac/mc/mc*/ce_count grep -H . /sys/devices/system/edac/mc/mc*/ue_countce_count是可纠正错误计数ue_count是不可纠正错误计数。前者出现一两个还可以解释为瞬时宇宙射线但如果持续增长或者直接把内存条的位置都报出来了那就没什么可犹豫的了内存故障的优先级立刻提到驱动之前。很多厂商的服务器固件也会在 Oops 前留下 SEL 事件用 IPMI 工具可以翻出来ipmitool sel elist我见过最典型的镜头是在数据库节点反复崩溃几周后运维决定换驱动结果某天偶然打开 IPMI 事件记录发现里面密密麻麻全是某个 DIMM 插槽地址的报错记录那一刻才找到真正原因。3.3 内存控制器和缓存也属于“内存问题”还要提一个容易忽略的点所谓“内存坏了”不一定是 DRAM 颗粒本身也包括内存控制器、CPU 内部缓存、甚至主板布线上的接触不良。有些服务器跑内存检测工具能跑一整晚不出错但一进生产就崩最后查出是 CPU 插槽接触面氧化、内存控制器过热降频后内部数据通路出错。这类问题有一个比较特殊的观察窗口如果 Oops 总是出现在系统负载升高、CPU 温度上升之后或者只在某个 NUMA 节点上的进程频繁报错而另一个 NUMA 节点完全正常那就要把“整机内存”这个概念拆开看逐个 NUMA 节点、逐个 DIMM 插槽去隔离验证。硬件的故障粒度往往比我们想象的小很多。4. 现场取证崩溃后我建议做的第一件事4.1 保存完整日志别只截屏一行很多人看到unable to handle kernel paging request的第一反应是抓起手机拍照但手机拍到的顶多是最前面几行。我建议把完整上下文都存下来至少包括 Oops 前后的几十行日志因为真正的线索往往藏在前面的蛛丝马迹里比如网卡驱动在 Oops 前几秒报过TX timeout或者文件系统刚打印过attempt to access beyond end of device。在 Linux 上最快的方式是dmesg -T | tail -300 /tmp/oops_$(date %Y%m%d_%H%M%S).log journalctl -k --since 10 minutes ago /tmp/oops_$(date %Y%m%d_%H%M%S).log同时去翻/var/log/messages、/var/log/kern.log把系统发生 Oops 的时间点对齐好。如果服务器还有带外管理卡的串口日志、屏幕截图也一并导出。第一次崩溃的原始材料是最完整的第一手现场后续任何换件和验证都该以此为基准。4.2 kdump 能救你一命如果服务器重启了dmesg信息会丢失这时候靠 kdump 保存的 vmcore 就成了唯一能回到案发现场的钥匙。生产环境无论多忙我都建议挑维护窗口把 kdump 配置好。平时它不占什么资源但崩溃发生时它会保存下完整的内存镜像事后可以用配套工具分析出崩溃时各寄存器、各线程栈的准确状态。假设没有 kdump至少也要保证sysctl kernel.panic_on_oops1并且把控制台日志输出到串口或带外管理卡让重启前最后一屏内容能被完整记录。否则故障发生后你看到的可能只是“重启后一切正常”的假象下次崩溃遥遥无期平台风险也一直在。4.3 从 BMC/IPMI 拉一份硬件传感器记录崩溃前后的供电、温度、风扇转速数据能帮助排除过热导致的内存时序不稳定。服务器端口上如果能看到 IPMI顺手执行ipmitool sensor list ipmitool sel elist | tail -100重点看有没有Critical、Non-recoverable、Correctable ECC、Uncorrectable ECC这类事件。很多内存类故障在系统日志里只表现为随机的 Oops但在 BMC 的 SEL 记录里早就写好了答案。这一步不要跳过它往往比你想的更省时间。5. 验证阶段压力测试与隔离替换的顺序5.1 内存压力测试怎么跑才算数如果日志层面还没有定论那就得靠压力测试和替换法。内存压力测试分在线和离线两种离线阶段用可启动的内存自检工具最直接。具体做法是把服务器切换到维护模式用带 memtest 类工具的启动盘拉起系统让它循环执行测试。这类工具有时跑几分钟就能报几十个错误但也有需要跑到十几小时甚至几十小时才暴露的偶发问题所以时间窗口要留足。在线阶段可以用系统内存压力工具来压它能让内存控制器和数据总线高强度工作同时配合业务流量观察系统是否复现 Oops。一个比较常见的做法是在测试前先把 swap 和缓存策略调整成更“激进”的状态让内存分配更频繁、换页更密集但这种调整在生产库上要谨慎别为了测试把业务给搭进去。说句实在话内存测试全绿也不代表 100% 保险。内存颗粒对温度和电压很敏感有时候冷启动测试完全正常跑业务升温后才出错。遇到这种情况可以用风扇策略、机房温度变化来复现或者把目标 DIMM 先降频跑一阵观察错误是否消失。5.2 驱动的快速隔离实验驱动侧的隔离实验比换内存简单。先确认可疑驱动对应哪个设备ethtool -i eth0 lspci -vvv -s 03:00.0看驱动名、固件版本、总线信息是否匹配。然后可以尝试临时调整驱动行为比如关闭网卡的多队列、关掉硬件卸载功能、把中断合并策略改为保守模式看看问题是否更容易复现或完全消失。这种逐步变化的排查思路能有效把“驱动代码路径”和“内存物理路径”区分开。如果怀疑某个树外驱动最干净的实验是换回系统自带驱动或主板自带网口让可疑设备停止工作。比如某块 HBA 卡驱动有问题就把磁盘接到另一块控制器上跑观察是否复现同样崩溃。只要故障跟随设备走而不是跟随内存插槽走答案自然就浮现了。5.3 换件测试的正确顺序换件是生产环境里最不愿意做但又最直接的手段。这里我建议的顺序是先换内存条再换驱动最后换主板/CPU。为什么先换内存因为内存条是故障概率最高的部件而且换一根内存条的成本远低于换一块主板。如果有 ECC 报错或者压力测试已经定位到 DIMM 插槽直接按槽位换掉。换完继续观察 Oops 是否回到同样的 RIP如果问题原样回来那就别继续烧内存的钱了回到驱动和内核层面来。换驱动也不是简单替换包最好先做一次版本基线记录当前内核版本、驱动模块路径、固件版本然后要么回滚到之前稳定过的组合要么升级到厂商明确声称修复了问题的版本。换完驱动后至少观察一两个业务高峰周期确认调用链里的同一个 RIP 不再出现。最后才是换 CPU 和主板因为它们涉及的因素最多不到万不得已不要动。6. 案例回放两个方向完全不同的 oops6.1 案例一RIP 稳定指向某网卡模块的同一偏移早先某项目上有台虚拟化宿主机升级内核后一个星期里崩了三次每次报的都是unable to handle kernel paging request。第一次看到日志时RIP 落在某个网卡驱动的收包轮询函数里Call Trace 一路全是 NAPI 收包路径而且三次崩溃的 RIP 偏移几乎一模一样连 Call Trace 都雷同。这已经基本不像是内存随机故障的样子了硬件侧又没有 EDAC 报警于是把注意力全部集中到驱动上。对比变更记录发现这台机器升级内核后第三方网卡驱动没有跟着重新编译还是旧版本模块接口不兼容导致了 use-after-free。解决方案就是拿到厂商提供的新版驱动重新编译安装之后连续几个月没有复现。这个案例留给我的经验是同一偏移反复出现时先别急着怀疑内存尤其是在驱动发生过变更的时间节点之后。6.2 案例二RIP 每次不同EDAC 给出答案另一台数据库服务器则是另一种画风。它一个多月内零散崩溃了四次第一次 RIP 在某个文件系统函数里第二次跑到网络软中断路径第三次干脆 panic 在中断上下文Call Trace 各不相同。我一开始怀疑是内核版本引入了新 bug但把日志拿出来对比时发现每次 CR2 地址都不一样而且 RIP 解析出来的模块也没规律可言。真正给出答案的是/sys/devices/system/edac/下的计数器以及服务器带外管理卡的 SEL 记录里面明确记着某个内存插槽发生过多条不可纠正错误。后续用内存自检工具跑了几圈果然在那个位置报出大量错误更换内存条后问题彻底消失。这个案例说明当软件层面找不出稳定规律时硬件错误日志和压力测试才是最快路径。6.3 内存和驱动问题同时出现的特殊情况也会遇到两者同时存在的复杂情况。比如系统内存里已经埋着一根不太稳定的内存条但平时只是偶发可纠正错误没有明显症状。某天驱动升级恰好引入了一个边界情况访问了一个本来合法但已经被内存错误悄悄破坏的对象于是 Oops 的现场看起来全是指向驱动。这种“叠加问题”最坑人因为你无论只修内存还是只换驱动另一侧的问题仍然会以其他形式继续冒头。处理这种叠加问题没有捷径只能把两套排查并行走一边跟踪硬件错误日志一边保留驱动版本变动的可能性清单直到某个变量被彻底排除。我在这种情况下更愿意先把驱动回滚到旧版本尽快消除不稳定因素再安排维护窗口处理内存。7. 我踩过的坑先换内存再查驱动的教训这里想单独说说我自己出过的糗事。早年间遇到一次反复崩溃因为日志里 RIP 落在某个驱动模块上我第一反应是驱动 bug换了驱动、改了内核参数折腾了整整两天。结果最后在带外管理卡日志里翻到几个星期前就有 ECC 报警换内存才是真正的解药。后来又遇到一次地址特征看起来很“内存”我立刻让现场换内存结果换了两根条子还在崩。最后发现是一块 HBA 卡的固件在和内核队列深度参数打架驱动的 DMA 映射逻辑在特定负载下会访问已经被释放的内存。那次经历之后我养成习惯无论第一眼看起来像什么方向都必须先收集齐三样东西——完整 Oops 日志、硬件错误日志、变更记录否则不下结论。还有一个小经验是不要把panic_on_oops设置成 1 就不管日志。有些环境为了快速恢复会把这个参数打开机器崩完马上重启日志在内存里一闪而过连 dmesg 都没来得及写盘。如果在维护窗口里能做到最好配好串口日志或 kdump否则每次崩溃都是一次“没有尸检报告的死亡”排查效率会低很多。8. 长期监控让下一次 Oops 不再无迹可寻聊了这么多核心思路其实就一句话unable to handle kernel paging request本身只是症状不是病灶。病灶到底在内存颗粒还是驱动模块需要从地址随机性、调用链一致性、ECC 计数和变更历史这四个维度反复交叉验证。为了让这套判断流程真正可落地我建议在每台关键服务器上提前做好三件事。第一部署硬件错误采集工具把 ECC 和内存控制器的报错持续记录下来第二启用并验证 kdump至少确保崩溃发生时内存镜像可落盘第三建立驱动、固件、内核版本的变更台账这样每次 Oops 出现时都能快速回答“这里最近动过什么”。我在实际处理中越来越觉得服务器硬件故障排查到最后往往不是拼技术深度而是拼谁手上的现场记录更完整。把这些工具和习惯补上下一次面对这行红色报错时你大概率能比我当年更快地找到真凶。