Linux Kernel Watchdog 硬锁死与软锁死检测深度实战

生产环境中,内核死锁、中断风暴或长时间关中断是系统管理员最头疼的故障之一。传统的 sysrq 和 kdump 虽然能捕获事后现场,但无法在故障发生的毫秒级时间窗内给出实时告警。Linux 内核内置的 Watchdog 子系统(softlockup + hardlockup)提供了无需额外硬件的实时锁死检测能力——它是系统可靠性的最后一道软件防线。


一、为什么需要内核 Watchdog?

操作系统内核本质上是一个高并发的运行时:中断处理、软中断、工作队列、RCU 回调、调度器 tick 都在争抢 CPU。当一个 bug 导致长时间关中断(cli)、自旋锁死循环、或软中断风暴时,用户态进程看似无法感知,但整个系统实质上已经"卡死"。

传统的看门狗需要外部硬件(如 iLO/iDRAC 板卡),而 Linux 内核从 2.6 时代引入了纯软件实现的 Watchdog 机制:

  • 软锁死(Soft Lockup):某个 CPU 被一个可抢占的上下文(如内核线程、软中断)长时间占据,调度器 tick 仍然工作,但普通进程长时间得不到执行。
  • 硬锁死(Hard Lockup):某个 CPU 完全停止响应,本地APIC定时器中断(或NMI)都未能触发——意味着关中断(local_irq_disable)后未恢复,或发生了死循环而不响应中断。

两者的核心差异在于:软锁死时中断仍然工作,硬锁死时中断已被关闭。这一差异直接决定了检测机制的设计思路。


二、软锁死检测:hrtimer 驱动的调度监视器

2.1 核心数据结构

软锁死的检测基于高精度定时器(hrtimer),每个 CPU 维护一个独立的检测窗口:


// include/linux/sched.h
struct task_struct {
    // ...
#ifdef CONFIG_SOFTLOCKUP_DETECTOR
    /* 软锁死检测时间戳 */
    unsigned long softlockup_timestamp;   // jiffies 时刻
    int softlockup_count;                 // 连续未调度次数
#endif
};

// kernel/watchdog.c
static DEFINE_PER_CPU(struct hrtimer, softlockup_hrtimer);
static DEFINE_PER_CPU(unsigned long, softlockup_timestamp);
static DEFINE_PER_CPU(unsigned long, watchdog_touch_ts);

2.2 检测原理

每个 watchdog_thresh(默认 10 秒,即 2 * HZ * watchdog_thresh 中的半周期检查),hrtimer 回调函数 softlockup_fn 执行以下判断链:


┌──────────────────────────────────────────────────────────────┐
│  hrtimer 到期回调 (每 watchdog_thresh/2 秒)                   │
├──────────────────────────────────────────────────────────────┤
│  1. 读取当前 jiffies → now                                    │
│  2. 计算 delta = now - per_cpu(softlockup_timestamp[cpu])    │
│  3. 若 delta > 2 * watchdog_thresh → 触发软锁死告警           │
│  4. 否则:重新启动 hrtimer                                    │
│                                                              │
│  每个时钟 tick 中:                                           │
│  - 调度器更新当前任务的运行时间戳                               │
│  - touch_softlockup_watchdog() 刷新 timestamp                │
└──────────────────────────────────────────────────────────────┘

关键实现逻辑 (kernel/watchdog.c):


static enum hrtimer_restart softlockup_fn(struct hrtimer *hrtimer)
{
    unsigned long now = jiffies;
    unsigned long delta = now - __this_cpu_read(softlockup_timestamp);

    if (delta < 2 * watchdog_thresh) {
        /* 正常:重新设置定时器 */
        hrtimer_forward_now(hrtimer, 
            ms_to_ktime(watchdog_thresh / 2 * 1000));
        return HRTIMER_RESTART;
    }

    /* 软锁死!当前 CPU 超过 2*watchdog_thresh 未被调度切换 */
    softlockup_hrtimer, now, delta);
    
    /* 打印全栈回溯并可选触发 panic */
    if (softlockup_panic)
        panic("softlockup: hung tasks");
        
    return HRTIMER_RESTART;
}

2.3 刷新机制:为什么关中断不会误报?

每当进程被调度器切换时,scheduler_tick() → watchdog_tick() → touch_softlockup_watchdog() 刷新当前 CPU 的时间戳。这意味着:

  • 只要调度器还在运行(软锁死的前提),时间戳就会被刷新
  • 如果一个内核线程死循环但不主动让出 CPU,调度器最终会在时间片耗尽时 tick,若此时仍无法抢占(如 preempt_disable()),则 softlockup_count 累加
  • 仅当连续错过 2 个完整检测周期(默认 20 秒)才上报软锁死

三、硬锁死检测:NMI Watchdog 与本地 APIC

硬锁死的检测比软锁死困难得多:既然中断已被关闭,常规手段无法执行。Linux 利用 NMI(不可屏蔽中断) 来穿透关中断状态。

3.1 NMI Watchdog 的硬件基础

现代 x86 CPU 通过本地 APIC(Advanced Programmable Interrupt Controller)提供 NMI 能力:


┌─────────────────────────────────────────────────┐
│  Intel APIC NMI 路径                             │
├─────────────────────────────────────────────────┤
│  Local APIC Timer                               │
│  ├─ 模式:Periodic(周期性)或 TSC-Deadline     │
│  ├─ 向量:NMI_VECTOR (=2)                       │
│  ├─ 目标:当前 CPU(Self-IPI)                  │
│  └─ 行为:即使 IF=0(关中断)也能触发           │
├─────────────────────────────────────────────────┤
│  NMI 不可屏蔽特性:                              │
│  - 不受 EFLAGS.IF 影响                           │
│  - 独立于普通中断向量表                          │
│  - 典型延迟 < 1μs(等同本地中断延迟)            │
└─────────────────────────────────────────────────┘

3.2 内核 NMI Watchdog 实现

NMI Watchdog 在 kernel/watchdog_hld.c 中实现,核心机制:


// kernel/nmi.c(简化)
static DEFINE_PER_CPU(unsigned long, hrtimer_interrupts);
static DEFINE_PER_CPU(unsigned long, hardlockup_timestamp);
static DEFINE_PER_CPU(bool, hardlockup_happened);

/*
 * NMI handler: 由 APIC 定时器周期性触发
 * 关键:即使关中断,NMI 也能进入此 handler
 */
static noirq void hardlockup_handler(struct pt_regs *regs)
{
    u64 now = local_clock();
    u64 delta = now - __this_cpu_read(hardlockup_timestamp);

    /* 检查距离上次 hrtimer tick 的时间 */
    if (delta < hardlockup_thresh) {
        /* 正常:hrtimer 仍在运行 */
        __this_cpu_write(hardlockup_timestamp, now);
        return;
    }

    /*
     * 硬锁死!hrtimer 已停止超过阈值。
     * 这意味着 CPU 关中断后未恢复,或进入了死循环
     * 不响应中断处理。
     */
    __this_cpu_write(hardlockup_happened, true);
    
    /* 触发硬锁死告警 */
    hardlockup_detected(regs, delta);
}

arch/x86/kernel/apic/apic.c 中的定时器配置:


static void setup_nmi_watchdog(void)
{
    if (!nmi_watchdog_enabled)
        return;

    /* 设置 APIC LVT Timer 为 NMI 模式 */
    apic_write(APIC_LVT_TIMER, APIC_LVT_DM_NMI | APIC_LVT_TIMER_PERIODIC);
    
    /* 设置分频和计数 */
    apic_write(APIC_TMICT, watchdog_clk_div * watchdog_thresh_cycles);
    
    /* 启用定时器 */
    apic_write(APIC_LVTT, APIC_LVT_MASKED & ~1); // unmask
}

3.3 检测时序图


时间轴(ms): 0    2.5    5    7.5   10   12.5   15   17.5   20
            |     |     |     |     |     |     |     |     |
hrtimer:    TICK  TICK  TICK  TICK  TICK  TICK  TICK  TICK  ← 软锁死正常时运行
                              ↓
                        local_irq_disable()  
                              ↓
                    [CPU 关中断,循环死锁]
                              ↓
NMI(2.5s):  NMI   NMI   NMI   NMI   NMI   NMI   NMI   NMI   NMI
                              ↓                ↓
                   检查htimer_interrupts    发现中断计数
                   发现无新 tick            停止增加 → 硬锁死!

核心判断逻辑:NMI handler 检查 hrtimer(或本地 APIC 周期性中断)是否在合理窗口内触发过。如果 NMI 触发间隔远超预期(说明普通定时器中断已停止),则判定为硬锁死。

3.4 perf NMI Watchdog 替代方案

从 Linux 3.10+ 开始,perf 事件机制也可以作为 NMI Watchdog:


# 使用 perf 事件作为 NMI watchdog(更高效,避免双重 NMI)
echo 1 > /proc/sys/kernel/nmi_watchdog
# 或
sysctl kernel.nmi_watchdog=1

# 查看当前 watchdog 状态
cat /proc/sys/kernel/nmi_watchdog        # 0=禁用 1=启用
cat /proc/sys/kernel/watchdog            # 软锁死全局开关
cat /proc/sys/kernel/watchdog_thresh     # 阈值(默认10秒)

perf NMI Watchdog 的优势在于复用 perf 基础设施,不需要单独的 APIC 定时器配置,且能更精确地追踪性能事件。


四、内核参数与生产配置

4.1 关键 sysctl 参数

参数 默认值 说明
kernel.nmi_watchdog 1 启用/禁用硬锁死检测(NMI watchdog)
kernel.soft_watchdog 1 启用/禁用软锁死检测
kernel.watchdog 1 全局 watchdog 总开关
kernel.watchdog_thresh 10 软锁死检测阈值(秒)
kernel.hardlockup_panic 0 硬锁死时是否 panic
kernel.softlockup_panic 0 软锁死时是否 panic
kernel.hardlockup_all_cpu_backtrace 0 硬锁死时是否打印所有 CPU 回溯

4.2 Linux 5.x+ 的重要变化

Linux 5.0 引入了 cgroup v2 集成,使得 watchdog 可以按 cgroup 分组管理:


// 新特性:per-cgroup 的 softlockup 权限控制
// 允许对不受信任的 cgroup 关闭软锁死检测(避免容器内误报)
echo 0 > /sys/fs/cgroup/mycontainer/memory.watchdog_thresh

Linux 6.x 进一步增加了 BPF 跟踪集成:


# 使用 BPF 追踪 watchdog 事件
bpftrace -e 'tracepoint:watchdog:softlockup_detected { 
    printf("softlockup on cpu %d\n", args->cpu); 
}'

4.3 生产环境推荐配置


# /etc/sysctl.d/99-watchdog.conf
kernel.watchdog_thresh = 15                 # 云环境适当放宽到15秒
kernel.hardlockup_panic = 1                 # 硬锁死立即 panic 重启
kernel.softlockup_panic = 0                 # 软锁死先告警,不立即 panic
kernel.hardlockup_all_cpu_backtrace = 1     # 硬锁死时打印所有CPU栈
kernel.nmi_watchdog = 1                     # 启用 NMI watchdog

五、实战:模拟锁死与检测验证

5.1 模拟硬锁死(关中断死循环)


// 注意:这会导致实例无响应,仅用于测试
#include <linux/kernel.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/sched.h>

static int hardlockup_thread(void *data)
{
    pr_info("Starting hardlockup simulation...\n");
    
    /* 关中断,进入死循环 → 若干秒后 NMI 检测硬锁死 */
    local_irq_disable();
    while (true)
        cpu_relax();  // PAUSE 指令,降低功耗
    
    return 0;
}

static int __init test_init(void)
{
    kthread_run(hardlockup_thread, NULL, "hardlockup_sim");
    return 0;
}

预期输出:


[  123.456] watchdog: BUG: soft lockup - CPU#2 stuck for 23s! [hardlockup_sim:1234]
[  133.789] NMI watchdog: BUG: hard lockup - CPU#2 stuck for 24s! [hardlockup_sim:1234]
[  133.790] Modules linked in: test_lockup(O)
[  133.791] CPU: 2 PID: 1234 Comm: hardlockup_sim Tainted: G           O
[  133.792] Hardware name: Dell Inc. PowerEdge R740/0KMX1P, BIOS 2.3.10
[  133.793] Call Trace:
[  133.794]  hardlockup_thread+0x23/0x30 [test_lockup]

5.2 模拟软锁死(关抢占死循环)


/* 关抢占但保持中断 → 软锁死(中断仍能响应,调度器被禁用)*/
preempt_disable();
while (true)
    cpu_relax();

5.3 使用 QEMU/KVM 调试


# 启动 QEMU 并启用 NMI watchdog
qemu-system-x86_64 \
  -kernel vmlinuz \
  -append "nmi_watchdog=1 watchdog_thresh=5 console=ttyS0" \
  -enable-kvm \
  -nographic \
  -m 2G

# 触发 NMI(从宿主机)
echo 'm' | sudo tee /proc/sysrq-trigger  # 手动触发 NMI

六、高级调试:当 Watchdog 触发时我们能得到什么

6.1 硬锁死时的全 CPU 回溯

当某个 CPU 发生硬锁死时,内核会向所有其他 CPU 发送 NMI IPI,强制它们打印调用栈:


// kernel/watchdog_hld.c
static void hardlockup_detected(struct pt_regs *regs, u64 delta)
{
    /* 打印当前(卡死)CPU 的栈 */
    pr_emerg("BUG: hard lockup - CPU#%d stuck for %llus!\n",
             smp_processor_id(), delta / NSEC_PER_SEC);
    show_regs(regs);
    dump_stack();
    
    /* 触发其他 CPU 打印回溯 */
    if (hardlockup_all_cpu_backtrace) {
        trigger_all_cpu_backtrace();
        /*
         * 通过 NMI IPI 发送,其他 CPU 的 NMI handler
         * 会调用 dump_cpu_task() 打印各自栈
         */
    }
}

6.2 Hung Task Detector 的互补

Watchdog 检测的是 CPU 级别的卡住,而 Hung Task Detector 检测的是进程级别的不响应:


# 启用 hung task 检测(默认启用)
cat /proc/sys/kernel/hung_task_timeout_secs    # 默认 120 秒
cat /proc/sys/kernel/hung_task_panic           # 超时是否 panic
cat /proc/sys/kernel/hung_task_check_count     # 最大检测进程数

两者的协作关系:

  • Hung Task:每个进程一个超时(可调度但不响应调度)
  • Soft Lockup:CPU 粒度(关抢占或长循环)
  • Hard Lockup:CPU 粒度(关中断)

┌─────────────────────────────────────────────────────┐
│  Linux 锁死检测三重防线                              │
├──────────┬──────────┬──────────┬───────────────────┤
│ 机制     │ 粒度     │ 前提     │ 硬件需求          │
├──────────┼──────────┼──────────┼───────────────────┤
│Hung Task │ 进程     │ 调度器活 │ 无                │
│Soft      │ CPU      │ 中断正常 │ hrtimer (Timer)   │
│Hard      │ CPU      │ 中断关闭 │ NMI / APIC        │
└──────────┴──────────┴──────────┴───────────────────┘

6.3 ftrace 追踪 watchdog 行为


# 追踪 softlockup detector 的 hrtimer 触发
cd /sys/kernel/debug/tracing
echo 'softlockup' > set_event
echo 'sched_switch' >> set_event
echo 'hrtimer_expire_entry' >> set_event
echo 1 > tracing_on

# 触发软锁死场景后查看轨迹
cat trace

七、Watchdog 的实现细节与边界条件

7.1 为什么 NMI 处理需要注意 re-entrancy?

NMI handler 不能安全地调用大部分内核 API(无锁语义、无阻塞),因此:


/* NMI handler 限制清单 */
❌ 不可调用可能导致睡眠的函数 (kmalloc(GFP_KERNEL), mutex_lock...)
❌ 不可调用 printk_ratelimit() (可能触发调度)
✓ 使用 printk_deferred() (延迟打印,避免 NMI 死锁)
✓ 使用原子操作 (atomic_inc, atomic_cmpxchg)
✓ 使用 per-cpu 变量避免锁竞争

7.2 Watchdog 与 CPU Hotplug 的交互

CPU 热插拔时需要暂停/恢复 watchdog:


// kernel/watchdog.c
static int watchdog_cpu_dying(unsigned int cpu)
{
    /* CPU 下线:停止其 hrtimer,避免迁移时误报 */
    hrtimer_cancel(&per_cpu(softlockup_hrtimer, cpu));
    /* 标记 CPU 为离线,跳过检测 */
    cpumask_clear_cpu(cpu, &watchdog_cpus);
    return 0;
}

static int watchdog_cpu_online(unsigned int cpu)
{
    /* CPU 上线:重新初始化并启动 watchdog */
    watchdog_enable(cpu);
    return 0;
}

7.3 Watchdog 在虚拟化环境中的行为

  • KVM Guest:需要 kvm.nested=1 + kvm.ignore_msrs=0 才能让 Guest 使用 NMI watchdog
  • AWS/GCP 云环境:部分实例类型不支持嵌套 APIC 虚拟化,硬锁死检测会降级为软锁死模式
  • 检测降级:dmesg 中会出现 NMI watchdog: Perf event create on CPU 0 failed with -2 提示

八、总结

Linux Kernel Watchdog 系统是操作系统可靠性的关键底层机制:

  1. 软锁死检测:基于 hrtimer 超时,依赖调度器仍在运行,检测"可抢占上下文长时间不释放"
  1. 硬锁死检测:基于 NMI/APIC 的不可屏蔽中断能力,是检测"关中断死锁"的唯一手段
  1. 三重防线:Hung Task + Soft Lockup + Hard Lockup 覆盖从进程到 CPU 的完整层级

对于生产系统,建议始终启用 watchdog 并根据 SLA 调整阈值。关键系统应配置 hardlockup_panic=1 让故障节点自动重启迁移,而非挂起等待人工介入。理解 Watchdog 的内部机制,也是内核开发者定位复杂死锁问题的必备功底。


参考资源

  • Documentation/sysctl/kernel.rst — watchdog 参数官方文档
  • kernel/watchdog.c — 软锁死检测实现
  • kernel/watchdog_hld.c — 硬锁死检测实现(x86 APIC 相关在 arch/x86/kernel/apic/apic.c)
  • Intel SDM Vol. 3A Chapter 10 — APIC 与 NMI 硬件规范
  • tools/perf/Documentation/perf-record.txt — perf NMI watchdog 用法
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部