watchdog: BUG: soft lockup - CPU#3 stuck for 22s! 怎么排查——从寄存器 dump 到自旋锁争用根因

watchdog: BUG: soft lockup - CPU#3 stuck for 22s! 完整排查:从寄存器 dump 到自旋锁争用根因

你的服务器 dmesg 里反复冒出这一行:

复制代码
watchdog: BUG: soft lockup - CPU#3 stuck for 22s! [kworker/3:1:1234]

机器要么卡住几十秒、SSH 敲不进去,要么直接重启。你去搜中文答案,十篇有八篇叫你 sysctl kernel.watchdog_thresh=30 把阈值调大。调完报错确实少了,但下一次它不打招呼,直接变成 Hard LOCKUP panic。

这篇给出能一路定位到具体哪把自旋锁的排查路径:先讲清 22s 这个数字怎么来的、方括号里的进程名为什么常常不是元凶,再给四个回合的取证命令、内核看门狗的源码机制、5 个厂商 KB 的真实根因,最后一份自检脚本。核心结论先给:调大阈值只是把检测线往后挪,不解决任何根因。

TL;DR

  1. 软锁(soft lockup)的官方定义是:内核态循环超过 20 秒没有让出 CPU 给其他任务跑。定时器中断还在,所以能上报;连中断都不响应了叫硬锁(hard lockup)。
  2. watchdog_thresh 默认 10 秒,软锁阈值 = 2 × 10 = 20 秒;看门狗 hrtimer 周期是 2*thresh/5 = 4 秒。所以日志里常见 22s、23s、24s,而不是整数 20s。
  3. 方括号里的 [kworker/3:1:1234] 是当时占住该 CPU 的任务 ,不一定是元凶;真正的线索是紧跟其后的 RIP: 那一行。
  4. RIP 落在 native_queued_spin_lock_slowpath 或 _raw_spin_lock* 上,基本可判定为自旋锁争用,这是生产环境里占比最高的一类根因。
  5. 排查顺序:先 sysrq-l 抓所有 CPU 栈,再 perf top -a -g 找热点函数,最后区分是自旋锁、中断风暴还是内存回收路径。

一、先分清:soft lockup 和 hard lockup 不是一回事

分不清这两个,后面全白忙。它们是两套检测机制、两种日志、两种危险程度。

项目 soft lockup(软锁) hard lockup(硬锁)
官方定义 内核态循环超过 20 秒,不让其他任务跑 内核态循环数秒,连中断都没机会跑
还能响应什么 定时器中断仍工作,能打印日志 中断不响应
检测手段 看门狗 kthread 时间戳(hrtimer 回调) NMI / PMU 溢出回调
默认阈值 20 秒(2 × watchdog_thresh) 10 秒(1 × watchdog_thresh)
日志样子 watchdog: BUG: soft lockup - CPU#x stuck for 22s! watchdog: BUG: hard lockup - CPU#x / Hard LOCKUP panic
危险程度 通常能自愈,但可能是硬锁前兆 基本必 panic,需 kdump 取证

关键判断:看到 soft lockup,先别急着庆祝「只是软的」 。多个真实案例里,软锁和 RCU stall 并发出现,最终收敛成 Kernel panic - not syncing: Hard LOCKUP。软锁是警报,不是结论。

二、先看懂日志字段:22s 是怎么算出来的、谁才是元凶

一条完整的软锁日志信息量很大,逐字段拆开看:

复制代码
watchdog: BUG: soft lockup - CPU#3 stuck for 22s! [kworker/3:1:1234]
RIP: 0010:native_queued_spin_lock_slowpath+0x122/0x200
Call Trace:
 _raw_spin_lock
 cfs_percpt_lock [libcfs]
 LNetMDUnlink [lnet]
Modules linked in: ...drbd(OE)...
字段 含义 怎么用
watchdog: 固定前缀,来自 pr_fmt 宏(kernel/watchdog.c 第 13 行) 用它过滤日志
CPU#3 发生软锁的 CPU 编号 与后面 CPU: 3 寄存器行一致
stuck for 22s 距上次成功调度的秒数,约等于 2 × watchdog_thresh 数值大小反映卡了多久,不是根因
[kworker/3:1:1234] comm:pid,当时占住该 CPU 的任务 只是受害者,不一定是元凶
RIP: CPU 卡住时执行的指令地址 最关键 ,native_queued_spin_lock_slowpath 即自旋锁争用
Call Trace: 内核栈 从顶往下持续不变的那一层最可能是元凶
Modules linked in: 已加载模块,(OE) 表示 out-of-tree 快速锁定第三方模块

关于「谁才是元凶」,内核 RCU 文档给了一条非常实用的经验:多次 stall 之间,内核栈里从栈顶往下保持同一层不变的那个函数 ,最接近真正的卡点。单个 [comm:pid] 会骗你,跨多次抓取的稳定栈层不会。

三、排查回合制:四个回合从报警到定位

第一回合:确认检测器状态和阈值(但别停在这里)

复制代码
cat /proc/sys/kernel/watchdog          # 1 = 检测开启
cat /proc/sys/kernel/watchdog_thresh   # 默认 10,软锁阈值 = 2 倍
cat /proc/sys/kernel/soft_watchdog     # 软锁检测开关
cat /proc/sys/kernel/nmi_watchdog      # 硬锁(NMI)检测开关

这一步只能回答「检测是不是被人关过、阈值是不是被人调大过」。它回答不了根因,很多教程却把它当成了唯一答案。

第二回合:卡住的那一刻抓现场

这是最值钱的一步,必须在卡住时立刻执行,事后补不回来:

复制代码
echo l > /proc/sysrq-trigger   # 打印所有活动 CPU 的内核栈,一次拿全
echo w > /proc/sysrq-trigger   # 打印所有 D 状态(不可中断睡眠)任务
echo d > /proc/sysrq-trigger   # 打印所有被持有的锁,定位锁争用
cat /proc/<pid>/stack          # 单个目标内核线程的栈(注意:running 任务可能为空)

sysrq-l 拿到的多份栈,横向对比就能看出「大家都堵在同一个函数上」,这是自旋锁争用最典型的特征。

第三回合:找热点函数

复制代码
perf top -a -g                       # 实时看哪个内核函数在烧 CPU
perf record -a -g -F 999 -- sleep 30
perf report

软锁场景下,perf top 常年排第一的就是 native_queued_spin_lock_slowpath。一旦命中它,方向就从「谁在跑」转成「大家在抢哪把锁」。

第四回合:区分自旋锁 / 中断风暴 / 内存回收

复制代码
cat /proc/interrupts                                  # 看是否有某个 IRQ 计数疯涨
dmesg -T | grep -iE "soft lockup|rcu.*stall|hung_task|watchdog"
cat /sys/module/rcupdate/parameters/rcu_cpu_stall_suppress

如果 soft lockup、rcu_sched detected stalls、task blocked for more than 120 seconds 三者同时出现,基本可以判定是同一条链路的多个探针在报警,而不是三个独立故障。这一点中文资料里很少讲清。

四、根因:内核看门狗到底是怎么检测的(源码级)

看门狗实现在 kernel/watchdog.c,核心几行如下:

复制代码
// kernel/watchdog.c
#define pr_fmt(fmt) "watchdog: " fmt          // 第 13 行,日志前缀来源
int __read_mostly watchdog_thresh = 10;       // 第 50 行,默认阈值

工作机制分三层:

  1. 每个 CPU 起一个看门狗 kthread(实时 FIFO 优先级),配上 hrtimer,周期是 2*watchdog_thresh/5,默认 4 秒。
  2. 每次 hrtimer 回调 watchdog_timer_fn() 更新该 CPU 的时间戳,同时给硬锁检测踢心跳。
  3. 如果某个 CPU 的时间戳超过 2*watchdog_thresh 没被更新,softlockup_fn() 就打印 stuck for %us!。任务在途中的喂狗动作走 touch_softlockup_watchdog()。

这解释了 22s 这个数字:阈值线是 20 秒,检测周期 4 秒,所以上报点落在 20 到 24 秒之间,日志里写成 22s、23s 都很正常。

硬锁检测的位置这几年变过,引用时要注意版本:老内核和多数生产内核(RHEL/CentOS 系)在 kernel/watchdog_hld.c,走 Perf/NMI 溢出回调 watchdog_overflow_callback();上游新版本已把核心合并回 kernel/watchdog.c,并拆出 kernel/watchdog_buddy.c。写工单或搜文档时,按你的内核版本对号入座,别拿新树的文件名去套老内核。

常见触发路径,按生产环境出现频率排:

触发路径 特征 典型场景
自旋锁持有过长 / 争用 RIP 在 native_queued_spin_lock_slowpath,多 CPU 同栈 文件系统、网络、DRBD/Ceph 等模块
关抢占下的长循环 内核态大循环没 cond_resched() 第三方 out-of-tree 模块
中断风暴 / 软中断过长 /proc/interrupts 某 IRQ 疯涨,[ksoftirqd/x] 上榜 网卡、NVMe、驱动异常
内存回收路径 栈里有 lru_gen_*、shrink_many、khugepaged 大内存 + THP + MGLRU
虚拟化超分 单 vCPU 小机器,低负载也报 VPS、超卖云主机

五、五个真实根因(来自厂商 KB 与社区)

下面五个案例都有原始日志和内核版本,可作为对照排查的模板。

案例 内核版本 日志特征(RIP / 栈) 根因 出处
Lustre/LNet 自旋锁 RHEL 7.6(3.10.0) native_queued_spin_lock_slowpath 栈含 cfs_percpt_lock[libcfs] 多 CPU 争同一把 libcfs 自旋锁,软锁与 RCU stall 并发,最终 Hard LOCKUP Red Hat 5618071
DRBD + 内核回归 RHEL 8.7(4.18.0) dequeue_work_batch[drbd],rcu_sched kthread starved for 59998 jiffies 升级到该内核版本后回归叠加 DRBD 自旋锁争用,RCU 宽限线程被饿死 Red Hat 7010399
lru_gen 回收 + THP RHEL 9.5(5.14.0) shrink_many -> lru_gen_shrink_node,khugepaged stuck 26s Multi-Gen LRU 回收自旋锁争用,THP 分配放大压力 Red Hat 7077348
Ceph delayed cap RHEL 8.3(4.18.0) _raw_spin_lock,栈含 ceph_check_delayed_caps Ceph delayed cap 检查中 igrab 锁争用 Red Hat 6967188
单 vCPU VPS Rocky 8.9(4.18.0) new_slab,栈含 tcp_make_synack -> __alloc_skb 未确认内核 bug,倾向 CPU 饥饿/超分(悬案,恰好说明不是每次都能定性) Rocky 论坛

把五个案例横向排开,能看到一条规律:软锁几乎从不孤立发生。四个有定论的案例里,自旋锁争用是共同内核,RCU stall 与 hung task 是同场的伴生报警。

六、解决方案:分清「止血」和「治本」

参数 作用 属于哪一类 怎么用
kernel.softlockup_panic=1 软锁即 panic,触发 kdump 取证 生产建议开,把现场留在 vmcore 里
kernel.hardlockup_panic=1 硬锁即 panic 取证 同上
kernel.watchdog_thresh=N 调检测阈值(0 到 60,设 0 直接关闭检测) 只为掩盖 不建议当解法
kernel.watchdog_cpumask 限定监控哪些 CPU 调优 nohz_full 隔离核场景
nosoftlockup / nowatchdog 启动参数,关日志 / 关看门狗 只为掩盖 只在压测环境短暂使用
rcu_cpu_stall_suppress 静音 RCU stall 报警 只为掩盖 同上,不解决 stall

为什么调大 watchdog_thresh 是掩盖而不是修复:它只挪动检测线,不改变「某段内核代码长时间不让出 CPU」这个事实。调大之后有两笔代价:一是那条唯一能定位问题的栈会迟到甚至错过,你失去了取证窗口;二是如果循环是无界的,它终究会 hang 或 panic,只是从「可见的软锁」拖成「不可见的硬锁」。

对照一下现成的反面教材:Autodesk 的官方 KB 直接把 kernel.watchdog_thresh=30 写成解决方案;Proxmox 社区给的是加 nosoftlockup 启动参数;SUSE KB 建议延长看门狗周期。这些都是止血式 workaround。而 Red Hat 在虚机多 CPU 同时软锁的案例里,给的治本建议是「降低 hypervisor 负载、消除 CPU 超分」。

止血和治本要分清标注 :先把 softlockup_panic 打开保住现场,再去动根因;不要用调阈值把报警按住,然后当它没发生过。

七、如何自检你是否中招

一条命令扫出所有相关报警,并统计它们是否成组出现:

复制代码
dmesg -T | grep -icE "soft lockup"        # 软锁次数
dmesg -T | grep -icE "rcu.*stall"         # RCU stall 次数
dmesg -T | grep -icE "blocked for more than"   # hung task 次数

dmesg -T | grep -A 25 "soft lockup" | tail -30   # 打印命中行及其后 25 行

这里特意没用「遇到空行就停」的 awk 写法:dmesg -T 每一行都带时间戳前缀,内核日志里根本不存在真正的空行,那种写法会一路打印到日志末尾,把现场淹掉。用 grep -A 固定打印命中行及其后 25 行,才拿得到干净、可直接贴进工单的现场。

判断标准:只要三项计数同时大于 0,就不是偶发抖动,按本文第三节四个回合完整走一遍。如果只有软锁、RIP 落在 native_queued_spin_lock_slowpath,优先怀疑自旋锁争用,并去比对 Modules linked in 里的第三方模块。

八、四个常见误区

排查软锁时,下面四个坑几乎人人踩过一遍。列出来对照,能省下几小时。

误区 为什么错 正确做法
把 [comm:pid] 当元凶 它只是当时恰好占住该 CPU 的受害者,真正卡住的可能是别的 CPU 在抢锁 看 RIP:,并对比多次抓取里保持不变的栈层
拿「调大 watchdog_thresh」当修复 只把检测线往后挪,根因仍在,最终会从软锁演变成 Hard LOCKUP 先开 softlockup_panic 保住 vmcore,再动根因
只看单次日志就下结论 单次栈拿到的可能正是受害者 用 sysrq-l 一次抓全部 CPU 栈,横向比对公共层
把 RCU stall 当成独立故障另开工单 它和软锁常常同根并发,分开查等于把一条链路拆成三份 三者同现时按一条链路查,重点找公共的自旋锁

判断自己有没有踩第一个坑,方法很简单:看那条 [comm:pid] 换不换 。同一个 [ksoftirqd/0] 反复出现,说明卡点确实在软中断;每次都是不同进程,却都堵在同一个 RIP,那元凶是那把锁,不是进程。

九、启示

  1. 日志里 [comm:pid] 是被卡住的受害者,RIP: 才是指认元凶的证据。 拿前者定位,十次有八次找错人。
  2. 调 watchdog_thresh 是关警报,不是修故障。 判断标准很简单:改完之后「内核还会不会在同一个函数上长时间不让出 CPU」,会,就是掩盖。
  3. 软锁、RCU stall、hung task 同场出现时,按一条链路查,不要开三个工单。 它们大概率是同一把锁的不同探针。
  4. out-of-tree 模块要单独怀疑。 Modules linked in: 里的 (OE) 标记是强信号,升级内核后新出现的软锁尤其要往这里查。
  5. 单 vCPU 小机器上反复软锁,先怀疑超分而不是内核 bug。 案例五就是反面教材:同样的日志,根因在宿主机。

原始出处

本文首发于 CSDN 专栏《运维漏洞指南》。

相关推荐
DongQiShanRen1 小时前
裁决台账双向互校(上):名册与实物的第一道对账
java·linux·运维·数据库·人工智能·自然语言处理·数据挖掘
ShineWinsu1 小时前
对于Redis:AOF持久化的解析
linux·数据库·redis·缓存·面试·持久化·aof
wuminyu2 小时前
Virtual Thread重投递至ForkJoinPool任务队列过程解析
java·linux·c语言·jvm·c++
xuhe22 小时前
Codex解决新模型无法使用/model选择的问题
linux·ai·codex
well06122 小时前
Linux 文件相关底层知识
linux·运维·服务器
羔羊++3 小时前
26_实验二十五_busybox构建根文件系统
linux
xUxIAOrUIII3 小时前
日常笔记-1005-1
linux·笔记·python·macos
nazisami3 小时前
程序地址空间
linux·虚拟地址
不会就选b3 小时前
Linux之TCP<2>
linux·网络·tcp/ip