一、软锁定:内核里的“无限循环”什么样
Linux 把 CPU 时间分成两类:用户上下文、中断上下文。内核线程、系统调用,都属于“可抢占”的用户上下文;而硬中断、软中断、NMI,则是“不可被普通抢占”的中断上下文。软锁定(soft lockup)指的是:某颗核在“可抢占”上下文里,持续占有 CPU 超过阈值(默认 20s),却一次都不让出调度器。换句话说,内核自己跑死循环了,而且自愿关掉抢占。结果是:
-
该核不响应任何普通中断,定时器、调度、RCU、负载均衡统统卡住;
-
其他核仍可运行,系统看似“活”,但负载飙高、响应迟钝;
-
由于抢占被关,内核线程无法迁移,top 里会看到某线程 100% 且跑满整个阈值。
触发条件通常是:自旋锁死循环、preempt_disable 后忘记 enable、内核模块在回调里做重度计算。软锁定不是 panic,而是“内核还有口气,但已无法自救”。
二、看门狗:给 CPU 绑一根“脉搏绳”
看门狗(watchdog)的核心思想:预期 CPU 每隔固定时间“报一次心跳”,若错过,就认为系统失去响应,于是拉闸复位。Linux 存在两层看门狗:
-
软看门狗(software watchdog):内核在高精度定时器里每 4s 写一次“喂狗”计数器;
-
硬看门狗(hardware watchdog):主板或 SoC 里的独立外设,一旦错过心跳,直接断言 reset 引脚。
软锁定检测正是依附软看门狗:每 CPU 都有一个“时钟戳”变量,时钟中断里更新;而看门狗线程(watchdog/x)每秒检查一次,若发现某核时钟戳超过 20s 未变,就打印 soft lockup 日志,并视配置触发 panic 或仅告警。硬看门狗则“冷眼旁观”:若软锁定导致整个系统都不再喂狗,它就出手复位。于是形成“软锁定→日志→硬狗超时→强制重启”的阶梯式守护链。
三、日志解读:那一行 soft lockup 到底在说什么
典型信息:
CPU#3 stuck for 22s ... [module: foo+0x1234]
字段拆解:
CPU#3 stuck for 22s ... [module: foo+0x1234]
字段拆解:
-
CPU#3:受害者核,注意不一定是肇事线程当前核,而是“时钟戳卡住的核”;
-
22s:持续关抢占时间,大于阈值才触发打印;
-
module: foo+0x1234:看门狗采样到的 PC 指针,往往落在死循环函数;
-
堆栈跟踪:软狗检测线程会打印受害核的 backtrace,包含调用链、锁信息、抢占计数。
若看到 preempt_count = 1,说明有人关抢占后没开;若 rcu_state 显示 expired,说明 RCU 也受害。日志是“尸体照片”,却足够让你定位到函数名,甚至源码行。
四、诱发场景:那些“日常”操作如何踩雷
-
自旋锁嵌套:外层锁 A,内层锁 B,又回调回 A,形成 ABBA 死锁,但自旋锁不睡眠,只会死循环;
-
中断风暴:网卡驱动在高负载下频繁进入硬中断,软中断里调用 local_bh_disable 后陷入重计算,导致软中断线程长时间关抢占;
-
内核模块 bug:ioctl 回调里 for(;;) 忘记 cond_resched(),看似无害,却在抢占关闭路径里跑飞;
-
实时进程失控:FIFO 调度策略的实时任务死循环,且优先级高于看门狗线程,软锁定检测自身都被饿死。
这些场景的共同特征:关抢占、不睡眠、不自愿调度。写内核代码时,只要牢记“禁止抢占 → 必须短 → 必须可调度”,就能规避 90% 的软锁定。
五、调试利器:从 sysrq 到 ftrace,给死循环“拍 X 光”
系统还活着,只是某核卡住,可用魔法键:
echo l > /proc/sysrq-trigger
立即打印所有 CPU 的堆栈,比软锁定日志更全;
echo t > /proc/sysrq-trigger
打印任务状态,可看锁持有信息。
若问题可复现,提前打开 ftrace:
echo function > current_tracer
echo preempt_disable > set_ftrace_filter
再跑负载,跟踪关抢占与开抢占的配对情况,找出“只关不开”的函数。对于动态加载的模块,可用 kprobe 在 local_irq_disable 处插桩,记录调用链。 traces 是“动态心电图”,能让你看到“脉搏”何时消失、消失前最后一次心跳在哪里。
echo l > /proc/sysrq-trigger
立即打印所有 CPU 的堆栈,比软锁定日志更全;
echo t > /proc/sysrq-trigger
打印任务状态,可看锁持有信息。
若问题可复现,提前打开 ftrace:
echo function > current_tracer
echo preempt_disable > set_ftrace_filter
再跑负载,跟踪关抢占与开抢占的配对情况,找出“只关不开”的函数。对于动态加载的模块,可用 kprobe 在 local_irq_disable 处插桩,记录调用链。 traces 是“动态心电图”,能让你看到“脉搏”何时消失、消失前最后一次心跳在哪里。
六、预防之道:锁、抢占、信号量的“三驾马车”
-
锁策略:能用互斥锁就不用自旋锁;必须自旋时,确保临界区无循环、无睡眠、无调度点;
-
抢占模型:关抢占后立即设定“最大停留时间”,可用 preempt_count_add() 配对,或用 local_irq_save() 缩短关中断窗口;
-
信号量与完成量:长时间等待外部事件,用 wait_for_completion_timeout(),让出 CPU,避免“忙等”。
此外,打开 CONFIG_LOCKDEP 与 CONFIG_PREEMPT_RT,让内核在调试期帮你跟踪锁依赖与实时瓶颈。记住:软锁定不是“性能问题”,而是“正确性问题”,预防成本远低于线上救火。
七、硬看门狗实战:选型、喂狗、与软狗协同
硬件看门狗形式多样:SuperIO 芯片、SoC 内部定时器、IPMI BMC 控制器。Linux 统一抽象为 /dev/watchdog 字符设备。用户空间可通过 ioctl 设置超时、喂狗、或触发“魔法关闭”。典型流程:
-
系统启动时,systemd 自动打开设备,记录超时时间;
-
内核软狗每 4s 喂一次;
-
若软锁定导致喂狗停止,倒计时归零,硬件狗断言 reset;
-
重启后,BMC 日志里留下“Watchdog reset”字段,供运维定位。
若担心“误杀”,可调长超时,或启用“预超时中断”——在还剩一半时间时,内核先收到 NMI,有机会打印堆栈,再决定是否继续等待。硬狗是“最后的大锤”,但锤子也有重量,调得太短会误伤正常高负载,调得太长又失去守护意义。经验值:30s 预超时 + 60s 复位,适合大多数生产环境。
八、容器与虚拟化:软锁定在云时代的“新面具”
在宿主机超售场景,vCPU 可能被调度器抢出物理核,导致“时钟中断”延迟,进而触发“虚假”软锁定。内核日志里会出现“clocksource watchdog: Marking clocksource ‘tsc’ as unstable”,紧接着是 soft lockup。解决路径:
-
改用高精度时钟源 hpet 或 acpi_pm;
-
在虚拟机配置里开启 hv_stimer 或 arch_timer,让半虚拟化时钟接管;
-
给容器绑核,避免 vCPU 被频繁迁移;
-
关闭宿主机的 C-State 深度节能,减少核心唤醒延迟。
云环境下的软锁定,往往是“时钟”而非“死循环”作祟,需要结合时钟源、调度延迟、steal time 综合判断,避免盲目重启。
九、用户空间看门狗:systemd、watchdogd 与自定义守护
若硬件狗不存在,可用 systemd 的 systemd-watchdog 模块:服务单元里声明 WatchdogSec=30s,进程每 30s 调用 sd_notify(0, "WATCHDOG=1"),否则 systemd 重启该服务。对于 Node.js、Java、Go 应用,可通过 libsystemd 包装 “喂狗”调用,把“业务死循环”转化为“服务重启”,而非整系统复位。自定义看门狗守护则更进一步:监控进程池、线程池、GC 停顿,若发现“逻辑死锁”但内核依旧活着,可选择性重启进程,保留系统其余部分。分层看门狗策略,让“复位”从“核按钮”降级为“服务重启”,最大限度减少用户感知。
十、故障复盘:一次真实软锁定的“解剖室”记录
生产环境凌晨 3 点,监控系统报“ CPU usage 100% 持续 5min”,但 SSH 可登录。查看日志:
soft lockup: CPU#7 stuck for 120s, module: iptable_nat
堆栈指向 nf_nat_packet 里的 while (skb) 循环。进一步用 ftrace 发现:某条连接跟踪项因 NAT 端口复用导致链表成环,iptables 在关中断路径里遍历链表,永不结束。修复:升级内核,补丁已在 5.15 中解决循环引用。复盘启示:
soft lockup: CPU#7 stuck for 120s, module: iptable_nat
堆栈指向 nf_nat_packet 里的 while (skb) 循环。进一步用 ftrace 发现:某条连接跟踪项因 NAT 端口复用导致链表成环,iptables 在关中断路径里遍历链表,永不结束。修复:升级内核,补丁已在 5.15 中解决循环引用。复盘启示:
-
软锁定不一定是“自己写的模块”,主线内核也可能踩坑;
-
堆栈里若出现第三方模块,先禁用再观察;
-
保持内核在稳定维护版本,比任何“调参”都有效。
记录完整时间线、保留 vmcore、对比升级前后轨迹,才能让“下一次”不再重演。
十一、心理建设:软锁定不是“失败”,而是“暴露”
很多团队把软锁定当成“偶发噪声”,重启掩盖,日积月累,最终在某次促销活动中集体爆发。正确心态是:每一次软锁定都是内核给出的“免费体检报告”。它告诉你:
-
锁设计不合理;
-
实时策略滥用;
-
模块缺少 cond_resched;
-
看门狗阈值过宽。
把报告纳入迭代,把“重启”改为“修复”,才能让系统从“看似稳定”走向“真正健壮”。记住:看门狗不是敌人,它是内核的“良心”,在系统失去心跳时,替你喊停。
尾声:让心跳持续,让复位稀少
软锁定与看门狗,是一对“生死双生”的机制:前者负责“呐喊”,后者负责“拉闸”。理解它们的原理、日志、调试与预防,就像给系统装上“心电图”(软狗)与“除颤器”(硬狗)。你无需祈祷系统永不死锁,只需确保:
-
锁尽可能短;
-
抢占尽可能开;
-
看门狗阈值尽可能贴合业务;
-
每一次复位都有迹可循、有据可查。
如此,当下一次凌晨电话响起,你可以从容地打开日志,指着那一行 soft lockup 说:“别怕,只是内核在提醒我们——该修心了。”