Linux系统死锁排查实战:如何用watchdog检测soft lockup和hard lockup

你是否经历过这样的场景:一台运行着关键业务的Linux服务器,突然之间响应变得极其缓慢,甚至完全失去响应。登录终端后,top命令卡住,ssh连接时断时续,系统日志里开始出现一些令人不安的“hung task”或“BUG: soft lockup”字样。对于系统管理员和开发者来说,这种时刻无异于一场噩梦。系统并没有完全崩溃,但核心功能已经停滞,常规的诊断工具往往也陷入泥潭。此时,问题的根源很可能指向了内核层面的死锁。

与应用程序中因资源竞争导致的死锁不同,内核死锁通常意味着更底层、更严重的问题。它可能源于一个陷入无限循环的内核线程,一个未能正确释放的自旋锁,甚至是硬件中断被意外禁用。这类问题隐蔽性强,破坏力大,传统的用户态调试手段常常束手无策。幸运的是,Linux内核内置了一套强大的监控机制——watchdog(看门狗),它像一位沉默的哨兵,持续监视着系统的健康状态,专门用于检测soft lockup(软死锁) 和hard lockup(硬死锁) 这类棘手问题。

本文将从一线运维和开发者的实战视角出发,彻底抛开晦涩的原理堆砌,聚焦于当系统出现卡顿、无响应时,我们如何利用watchdog机制快速定位问题。你将掌握从日志解读、工具使用到问题根因分析的一整套实战流程,目标是让你在下次面对系统“假死”时,能够胸有成竹,精准出击。

1. 理解死锁:从现象到内核机制

在深入工具之前,我们必须清晰区分两种不同类型的死锁,这直接决定了后续的排查方向。

Soft Lockup(软死锁),顾名思义,是一种“软性”的停滞。想象一下,一个CPU核心上,某个内核线程或进程获得了CPU使用权后,便进入了漫长的循环或阻塞操作,并且在这个过程中它没有主动让出CPU(例如,没有调用schedule()或cond_resched())。由于Linux内核的完全公平调度器(CFS)在大多数情况下是非抢占式的(除非配置了CONFIG_PREEMPT),这个“贪婪”的任务就会一直霸占着CPU。其他优先级相同或更低的任务在这个核心上就得不到运行机会,从用户角度看,这个CPU似乎“卡住”了。但需要注意的是,中断(IRQ)仍然能够被正常响应。这就是为什么你还能通过其他CPU核心执行命令,或者收到watchdog发出的警告信息。

Hard Lockup(硬死锁) 则严重得多。它意味着某个CPU核心在长时间内无法处理任何中断。中断是硬件与CPU通信的生命线,负责处理磁盘I/O完成、网络包到达、定时器到期等关键事件。如果一个CPU关中断的时间过长(例如,在自旋锁中使用了spin_lock_irqsave()但后续逻辑出错,未能解锁和开中断),那么这个CPU就与外界失去了联系。它不仅无法调度其他任务,连最基本的定时器中断都无法响应,是系统彻底僵死的标志。

为了监控这两种状态,Linux内核的watchdog机制采用了巧妙的双保险设计:

  • 对于Soft Lockup:每个CPU上运行着一个高精度定时器(hrtimer)。内核会维护一个“喂狗”时间戳。只要CPU上有正常的线程调度,一个特定的看门狗线程就会定期更新这个时间戳。如果watchdog检测到在预设的阈值内(例如20秒)这个时间戳没有被更新,它就判定该CPU可能发生了软死锁。
  • 对于Hard Lockup:基于性能监控计数器(Performance Monitoring Unit, PMU)。内核利用PMU的NMI(不可屏蔽中断)功能。NMI是一种优先级极高的中断,即便CPU处于关中断状态也能被触发。watchdog配置PMU周期性地产生NMI。在NMI处理函数中,它会检查一个计数器。如果发现该计数器在连续多个NMI周期内都没有被递增,说明常规的中断处理路径已经失效,从而判定发生了硬死锁。

下面的表格清晰地对比了二者的关键差异:

特性Soft Lockup (软死锁)Hard Lockup (硬死锁)
核心表现CPU被单个任务长期独占,无法调度其他任务。CPU无法响应任何中断(包括定时器中断)。
中断状态可响应。NMI和外部中断可以正常处理。不可响应。常规中断被屏蔽,仅NMI可触发。
严重程度相对较轻,通常影响局部性能。非常严重,表明CPU核心功能基本丧失。
典型触发场景内核态死循环、长时间不调度的内核线程、Bug导致的任务卡死。错误地长期关中断、自旋锁死锁、硬件故障。
Watchdog检测原理监控“喂狗”时间戳是否定期更新。利用PMU/NMI监控中断处理路径是否存活。

注意:默认情况下,许多Linux发行版可能只启用了soft lockup检测。Hard lockup检测依赖于CPU的PMU支持和内核配置(CONFIG_HARDLOCKUP_DETECTOR)。

2. 实战准备:启用与配置系统Watchdog

在问题发生前,确保watchdog机制已经就绪,是高效排查的第一步。大多数现代Linux发行版的内核都默认编译了相关功能,但我们需要确认其状态并进行必要的配置。

首先,检查当前系统的watchdog配置。/proc文件系统提供了丰富的运行时信息:

# 检查soft lockup检测是否开启及其阈值(秒)
cat /proc/sys/kernel/watchdog
cat /proc/sys/kernel/watchdog_thresh

# 检查hard lockup检测状态(如果支持,输出1表示开启)
cat /proc/sys/kernel/nmi_watchdog

通常,watchdog_thresh的默认值是10秒。这意味着如果一个CPU超过10秒没有“喂狗”,就会触发soft lockup警告。你可以根据系统负载和容忍度动态调整这个值。对于负载很重的系统,适当调大阈值可以减少误报。

# 临时将阈值调整为30秒(重启后失效)
sudo sysctl -w kernel.watchdog_thresh=30

# 要永久生效,编辑 /etc/sysctl.conf 文件,添加:
# kernel.watchdog_thresh = 30
# 然后执行 sysctl -p

如果nmi_watchdog文件不存在或返回0,可能意味着内核编译时未启用hard lockup检测,或者当前CPU架构不支持。对于x86_64服务器,通常都是支持的。

当watchdog检测到问题时,信息会输出到哪里?主要有两个目的地:

  1. 内核日志:最常用的查看位置。使用dmesg命令或查看/var/log/kern.log(取决于发行版)。
  2. 系统控制台:如果配置了,消息会直接打印在终端上。

为了模拟一个soft lockup场景以进行演练,我们可以编写一个简单的内核模块。警告:此操作会导致系统卡顿,务必在测试环境进行!

// 示例:一个导致soft lockup的简单内核模块 (lockup_sim.c)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/delay.h>

static int __init lockup_init(void) {
    printk(KERN_INFO "Simulating soft lockup...\n");
    while (1) {
        // 空循环,不调用 cond_resched() 或 schedule()
        // 这将导致当前CPU核心被独占
    }
    return 0;
}

static void __exit lockup_exit(void) {
    printk(KERN_INFO "Lockup module removed.\n");
}

module_init(lockup_init);
module_exit(lockup_exit);
MODULE_LICENSE("GPL");

编译并插入此模块(需要内核头文件),你很快就能在dmesg中看到类似下面的错误信息,这是我们下一步分析的重点。

3. 诊断第一步:解读Watchdog告警日志

当系统出现异常,你的第一反应应该是查看内核日志。一条典型的watchdog告警信息包含了定位问题所需的关键线索。让我们来拆解一条常见的错误信息:

[ 1234.567890] watchdog: BUG: soft lockup - CPU#2 stuck for 22s! [bash:12345]

这条信息每一部分都至关重要:

  • [ 1234.567890]: 系统启动后的时间戳(秒)。用于判断问题发生的时间点。
  • watchdog: BUG: soft lockup: 明确指出了错误类型是软死锁。如果是硬死锁,则会显示hard lockup。
  • - CPU#2 stuck for 22s!: 这是核心信息。它告诉我们编号为2的CPU核心上发生了问题,并且已经“卡住”了22秒。这个时间应该略大于你设置的watchdog_thresh阈值。
  • [bash:12345]: 疑似导致问题的进程。这里显示的是进程名bash和其PID12345。需要特别注意:这并不一定是“罪魁祸首”,而很可能是在发生锁定时,正在该CPU上运行的“受害者”进程。真正的元凶可能是内核线程、中断处理程序,或者这个进程在内核态执行的某段代码。

对于hard lockup,日志格式类似,但可能更简洁,并且通常会伴随堆栈跟踪:

[ 1234.567890] NMI watchdog: Watchdog detected hard LOCKUP on cpu 2

拿到CPU编号和疑似进程PID后,我们可以开展进一步的调查。首先,可以尝试获取该进程的详细信息,特别是它当前在内核中正在做什么。

# 使用 crash 工具或 /proc/<pid>/ 信息进行分析(需要系统支持)
# 查看进程状态和堆栈(假设PID是12345)
cat /proc/12345/status
# 查看进程的内核态堆栈(需要root权限和内核符号)
sudo cat /proc/12345/stack 2>/dev/null || echo "可能需要启用内核配置"

提示:/proc/<pid>/stack 文件可能默认不提供信息。在生产环境中,更常见的做法是在触发lockup时,系统会自动打印出该CPU的堆栈信息(如果配置了CONFIG_PRINTK和CONFIG_DETECT_HUNG_TASK)。你需要在内核日志中查找CPU#2附近的堆栈跟踪(Call Trace)。

4. 高级排查工具与核心转储分析

当简单的日志信息不足以定位根因时,我们需要借助更强大的工具。perf和crash是分析此类内核级问题的利器。

使用perf进行实时剖析

如果系统尚未完全僵死,你可以快速登录并尝试使用perf记录问题CPU上的活动。

# 对指定CPU(例如CPU2)进行采样,持续记录30秒
sudo perf record -C 2 -g -- sleep 30

# 分析记录的数据,生成火焰图或查看热点函数
sudo perf report -n --stdio

在perf report的输出中,重点关注那些占用CPU时间接近100%的函数。如果是因为死循环导致的soft lockup,你很可能会看到某个函数(或其中某几行代码)被无限次采样。

配置与触发内核转储(kdump)

对于hard lockup或严重的soft lockup,系统可能最终会触发内核崩溃(panic)并重启。为了保留事故现场,kdump 机制是必不可少的。它能在第一个内核(生产内核)崩溃时,启动一个预留的小内存内核(捕获内核),将前一个内核的内存转储到文件中,供事后分析。

  1. 确保kdump已安装并配置:

    # 在RHEL/CentOS/Fedora上
    sudo yum install kexec-tools
    sudo systemctl enable kdump
    sudo systemctl start kdump
    
    # 在Ubuntu/Debian上
    sudo apt install kdump-tools
    sudo systemctl enable kdump
    sudo systemctl start kdump
    

    你需要确保/etc/kdump.conf配置正确,并且有足够的内存预留(通常在GRUB配置中通过crashkernel=参数设置)。

  2. 分析vmcore文件: 当崩溃发生后,转储文件通常保存在/var/crash/目录下。使用crash工具进行分析:

    sudo crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/<timestamp>/vmcore
    

    在crash交互环境中,你可以执行一系列命令来检查崩溃时的状态:

    crash> log -m 50      # 查看崩溃前的内核日志
    crash> bt -a          # 查看所有CPU的堆栈回溯
    crash> ps | grep D    # 查看处于“不可中断睡眠”状态的进程
    crash> kmem -i        # 查看内存使用概况
    

    通过分析这些信息,往往能精确找到导致锁定的代码位置和内核数据结构。

系统性排查清单

面对一个lockup告警,遵循一个系统的排查路径可以避免遗漏:

  • 第一步:信息收集
    • 记录完整的dmesg输出,特别是错误发生时间点前后。
    • 检查/var/log/messages、/var/log/syslog等系统日志。
    • 运行sar -u 1 10等命令,查看历史CPU使用率,确认是突发问题还是持续高负载。
  • 第二步:上下文分析
    • 确认lockup类型(soft/hard)和涉及的CPU。
    • 检查疑似进程:它是什么?是用户进程还是内核线程?它最近是否更新过?
    • 回顾系统变更:最近是否进行了内核升级、驱动安装、软件部署或配置更改?
  • 第三步:深入检查
    • 使用top或htop查看整体资源情况,其他CPU是否正常?
    • 检查/proc/interrupts,看问题CPU上的中断计数是否在增长?(对于soft lockup应该增长,hard lockup则停滞)。
    • 运行strace -p <PID>(如果可能)跟踪疑似进程的系统调用,看它卡在哪个调用上。
  • 第四步:根因假设与验证
    • 内核Bug:搜索Linux内核邮件列表(LKML)或发行版Bug追踪系统,看是否有类似报告。
    • 驱动问题:特别是新安装或更新的硬件驱动。
    • 硬件故障:内存错误(ECC错误)、CPU过热或电源不稳。检查dmesg中的EDAC(错误检测与纠正)或MCE(机器检查异常)信息。
    • 资源耗尽:虽然lockup不同于OOM,但极端的内存压力可能导致交换颠簸,间接引发调度异常。

5. 常见死锁场景与针对性解决方案

理论结合实践,我们来看几个真实世界中导致lockup的典型模式及其解决思路。

场景一:内核模块中的忙等待循环

这是导致soft lockup的最常见原因之一。开发者在内核模块中编写了类似下面的循环,却忘记了让出CPU:

// 错误示例:等待某个硬件标志位,但未让出CPU
while (!(readl(device_status_reg) & READY_FLAG)) {
    /* 空循环 - 这将导致soft lockup */
}

// 正确做法:在循环中插入让出CPU或延迟的调用
while (!(readl(device_status_reg) & READY_FLAG)) {
    if (need_resched()) // 检查是否需要调度
        cond_resched(); // 主动让出CPU
    udelay(10); // 或加入微小延迟,减少CPU占用
}

场景二:错误的自旋锁使用

自旋锁(spinlock)用于内核中短时间的互斥访问。错误使用会导致hard lockup,尤其是在单核环境或错误地关中断时。

// 危险操作:在持有自旋锁时执行可能睡眠的操作
spin_lock(&my_lock);
kmalloc(GFP_KERNEL, size); // GFP_KERNEL可能引起睡眠!
spin_unlock(&my_lock); // 如果睡眠了,这里可能永远无法执行

// 正确做法:使用不会睡眠的内存分配标志
spin_lock(&my_lock);
ptr = kmalloc(GFP_ATOMIC, size); // 使用GFP_ATOMIC
spin_unlock(&my_lock);

注意:spin_lock_irqsave()会保存中断状态并关中断,如果锁持有时间过长,就会阻止NMI之外的所有中断,极易引发hard lockup。务必确保锁内代码路径极短且不会睡眠。

场景三:中断处理程序(ISR)过长

中断处理程序要求快速执行完毕。如果一个ISR执行时间过长,它可能会阻塞其他中断(包括定时器中断),从而干扰watchdog的“喂狗”机制,甚至表现为lockup。解决方法是遵循“顶半部/底半部”模式,将耗时操作推到软中断、tasklet或工作队列中执行。

场景四:硬件或固件问题

并非所有lockup都是软件错误。我曾遇到过一次诡异的间歇性hard lockup,最终排查发现是服务器主板的某个电源管理单元(PMU)固件存在缺陷,在特定负载下会发送错误的中断信号,导致CPU核心混乱。这类问题的排查往往更困难,需要:

  • 更新主板BIOS和所有硬件固件。
  • 在不同硬件上复现问题。
  • 使用mcelog等工具检查CPU的机器检查错误。
  • 进行内存压力测试(如memtester)和CPU压力测试(如stress-ng)。

预防与监控策略

亡羊补牢不如未雨绸缪。在生产环境中,你可以建立主动监控体系:

  • 部署集中式日志收集:将内核日志实时发送到ELK、Splunk或Graylog等平台,并设置告警规则,对“soft lockup”、“hard lockup”、“BUG”等关键字进行实时告警。
  • 使用Prometheus + node_exporter:node_exporter的textfile收集器可以配合自定义脚本,解析dmesg中lockup事件的数量,将其转化为指标。
  • 压力测试与混沌工程:在非生产环境定期进行内核压力测试(如使用stress-ng —all 0模拟极端负载),并故意注入故障(如使用echo c > /proc/sysrq-trigger触发系统崩溃测试kdump),验证监控和恢复流程的有效性。

最后,记住一点:watchdog告警是一个症状,而不是根本原因。它像是一个发烧警报,告诉你系统“生病”了,但病因可能是感染、炎症或其他问题。日志中的CPU编号、进程PID和堆栈跟踪是你的听诊器和X光片,结合对系统近期变更的理解和对代码行为的洞察,你才能做出准确的诊断。保持内核和驱动程序的更新,在编写内核代码时对锁和循环保持敬畏,建立完善的监控,这三者是抵御系统死锁问题最坚实的防线。当告警再次响起时,希望你能从容地翻开这份指南,快速找到问题的命门。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐