跳转至

第四篇:Hard Lockup (Buddy) — CPU 互检与 miss 阈值

源码:kernel/watchdog_buddy.c kernel/watchdog.c | 头文件:include/linux/nmi.h

系列目录:Hard/Soft Lockup 内核源码深度解析


1. Buddy 检测器概述

当系统没有可用的硬件 PMU 用于 perf NMI hardlockup 检测时,内核回退到纯软件的 Buddy 检测器。Buddy 的核心思想:

CPU 组成一个环形检测链,每个 CPU 通过 hrtimer 心跳检查下一个 CPU 是否还活着。

检测环 (4 CPU 示例):

  CPU0 ──检查──→ CPU1
   ↑              │
   │              │ 检查
   │              ↓
  CPU3 ←──检查── CPU2
  • watchdog_buddy.c 仅 103 行——极简设计
  • 无硬件依赖,纯软件实现
  • miss_thresh = 3(需要连续 3 次 hrtimer 中断 missed 才报 hard lockup)
  • 检测在 watchdog_timer_fn() 的 hrtimer 上下文中完成(非 NMI)
  • 没有 regs 可用——因为不是 NMI 中断

2. 全局状态

// [watchdog_buddy.c:9]
static cpumask_t __read_mostly watchdog_cpus;

// [watchdog.c:145-150]
static DEFINE_PER_CPU(atomic_t, hrtimer_interrupts);       // hrtimer 心跳计数
static DEFINE_PER_CPU(int, hrtimer_interrupts_saved);      // 上次快照
static DEFINE_PER_CPU(int, hrtimer_interrupts_missed);     // 连续 missed
static DEFINE_PER_CPU(bool, watchdog_hardlockup_warned);   // 去重标志
static DEFINE_PER_CPU(bool, watchdog_hardlockup_touched);  // touch 标志
static unsigned long hard_lockup_nmi_warn;                 // 全CPU BT 互斥

watchdog_cpus 是 buddy 检测器维护的"在线的受监控 CPU 集合"。只有在此集合中的 CPU 才参与互检。这个集合与 watchdog_cpumask 不同——后者是用户配置的期望 CPU,而 watchdog_cpus 是 buddy 运行时维护的实际在线 CPU。


3. 初始化 — probe

// [watchdog_buddy.c:22-26]
int __init watchdog_hardlockup_probe(void)
{
    watchdog_hardlockup_miss_thresh = 3;
    return 0;
}

与 perf 探测不同,buddy 的 probe 永远成功。它做的唯一事情是将 watchdog_hardlockup_miss_thresh 设为 3——表示需要连续 missed 3 次 hrtimer_interrupts 递增才算 hard lockup。

比例关系:

hrtimer 每 4s 触发一次
miss_thresh = 3 → 3 × 4s = 12s 后才判定 hard lockup

对比 perf: miss_thresh = 1,但 NMI 周期 ≈ 10s
总体检测延迟相近 (~10-12s)

4. CPU 环形索引

// [watchdog_buddy.c:11-20]
static unsigned int watchdog_next_cpu(unsigned int cpu)
{
    unsigned int next_cpu;

    next_cpu = cpumask_next_wrap(cpu, &watchdog_cpus);
    if (next_cpu == cpu)
        return nr_cpu_ids;   // 自己是唯一在线的CPU → 无可检查对象

    return next_cpu;
}

cpumask_next_wrapcpu 开始,在 watchdog_cpus 中找到下一个已设置的位。如果 next_cpu 绕回到 cpu 自身(说明 watchdog_cpus 中只有这个 CPU),返回 nr_cpu_ids

示例

watchdog_cpus = {0b1011} = CPU0, CPU1, CPU3 在线

watchdog_next_cpu(CPU0) → CPU1
watchdog_next_cpu(CPU1) → CPU3  (跳过 CPU2)
watchdog_next_cpu(CPU3) → CPU0  (wrap)
watchdog_next_cpu(CPU2) → nr_cpu_ids (CPU2 不在 mask 里)

5. 启用 — watchdog_hardlockup_enable()

// [watchdog_buddy.c:28-60]
void watchdog_hardlockup_enable(unsigned int cpu)
{
    unsigned int next_cpu;

    /*
     * 新 CPU 在在线之前被标记 online,但在 hrtimer 中断首次
     * 运行之前,其他 CPU 检查它会导致误报。
     * 在新 CPU 上 touch watchdog,延迟检查至少 3 个采样周期。
     */
    watchdog_hardlockup_touch_cpu(cpu);

    /*
     * 我们要检查下一个 CPU。如果该 CPU 之前已经在线过,
     * 它的 hrtimer_interrupts_saved 可能不为 0。
     * 在下一个 CPU 上也 touch,避免误报。
     */
    next_cpu = watchdog_next_cpu(cpu);
    if (next_cpu < nr_cpu_ids)
        watchdog_hardlockup_touch_cpu(next_cpu);

    /*
     * 内存屏障:确保在此 CPU 被标记之前,touch 对其他 CPU 可见。
     * 对应端在 watchdog_buddy_check_hardlockup() 中的 smp_rmb()。
     */
    smp_wmb();

    cpumask_set_cpu(cpu, &watchdog_cpus);
}

为什么要双重 touch?

考虑以下场景:

初始状态: watchdog_cpus = {CPU0, CPU1}
检测链: CPU0 → 检查 → CPU1

现在 CPU2 hotplug 上线:

            CPU0                  CPU1                  CPU2
            ====                  ====                  ====
hrtimer:    [kick]                [kick]                [hrtimer 尚未启动]
                                  检查 CPU0             
                                  ↑ 如果此时 CPU2 加入...

如果 CPU2 立即被加入 watchdog_cpus,CPU1 的 watchdog_next_cpu(1) 可能返回 CPU2。但 CPU2 的 hrtimer_interrupts_saved 还是初始值 0(尚未有任何 hrtimer tick),CPU1 看到 hrtimer_interrupts 未增长 → 误报。

解决方案: 1. watchdog_hardlockup_touch_cpu(cpu) — 在 CPU2(新 CPU)自身 touch,使其 watchdog_hardlockup_touched = true。任何检查 CPU2 的 CPU 看到此标志 → 跳过检查 2. watchdog_hardlockup_touch_cpu(next_cpu) — 在 CPU3(CPU2 的下一个 CPU,如果存在)也 touch,防止 CPU2 加入后立即检查 CPU3 时产生误报 3. smp_wmb()cpumask_set_cpu() — 保证 touch 写入在 CPU 标记为在线之前对其他 CPU 可见

内存屏障成对使用

写入端 (enable/disable) 读取端 (buddy_check)
smp_wmb() (写屏障) smp_rmb() (读屏障)
enable 端:                          check 端:
  watchdog_hardlockup_touch_cpu()      next_cpu = watchdog_next_cpu()
  smp_wmb()           ────────→        smp_rmb()
  cpumask_set_cpu()                    watchdog_hardlockup_check(next_cpu)

保证:如果 smp_rmb() 之后 watchdog_next_cpu() 返回了新 CPU,那之前必然能看到 watchdog_hardlockup_touched 已被设置。


6. 禁用 — watchdog_hardlockup_disable()

// [watchdog_buddy.c:62-84]
void watchdog_hardlockup_disable(unsigned int cpu)
{
    unsigned int next_cpu = watchdog_next_cpu(cpu);

    /*
     * CPU 下线会改变检测链:之前检查这个 CPU 的 CPU 现在需要跳过它
     * 去检查 next_cpu。如果这个 CPU 刚完成了对 next_cpu 的检查并
     * 更新了 hrtimer_interrupts_saved,然后前一个 CPU 在一个采样
     * 周期内检查它,会触发误报。
     * 在 next_cpu 上 touch 来防止这个。
     */
    if (next_cpu < nr_cpu_ids)
        watchdog_hardlockup_touch_cpu(next_cpu);

    smp_wmb();

    cpumask_clear_cpu(cpu, &watchdog_cpus);
}

下线时序分析

检测链: CPU0 → CPU1 → CPU2 → CPU0

CPU1 即将下线:

                       CPU0                  CPU1                  CPU2
                       ====                  ====                  ====
hrtimer tick:          [kick]                [kick]                
                                             检查 CPU2:
                                             save hrtimer_int(C2)=N

                        [kick] ← 下一 tick
                        watchdog_next_cpu(0)
                        此时 CPU1 已不在 mask
                        → 返回 CPU2
                        检查 CPU2:
                        hrtimer_int(C2) = N (未变!)
                        → 误报! (因为 CPU1 刚检查过并保存)

CPU1 刚对 CPU2 做了 watchdog_hardlockup_update_reset(),把 hrtimer_interrupts_saved(CPU2) = N。然后 CPU1 下线,CPU0 的检查目标从 CPU1 变为 CPU2,CPU0 读取 hrtimer_interrupts_saved(CPU2) = N,当前 hrtimer_interrupts(CPU2) = N(还没到下一个 tick)→ 认为 missed → 误报

防护watchdog_hardlockup_touch_cpu(next_cpu) 在 CPU2 上设置 touched 标志,CPU0 检查时发现 touched → 跳过。


7. 互检核心 — watchdog_buddy_check_hardlockup()

// [watchdog_buddy.c:86-103]
void watchdog_buddy_check_hardlockup(int hrtimer_interrupts)
{
    unsigned int next_cpu;

    /* 检查下一个 CPU 是否 hard lockup */
    next_cpu = watchdog_next_cpu(smp_processor_id());
    if (next_cpu >= nr_cpu_ids)
        return;

    /*
     * 确保由于 watchdog_hardlockup_enable()/disable() 引起的
     * watchdog_next_cpu() 更改,能在检查前看到 touch。
     */
    smp_rmb();

    watchdog_hardlockup_check(next_cpu, NULL);
}

调用链

watchdog_timer_fn()              [watchdog.c:795]
  └─ watchdog_hardlockup_kick()  [watchdog.c:199]
       ├─ atomic_inc_return(hrtimer_interrupts)  // 本 CPU 心跳+1
       └─ watchdog_buddy_check_hardlockup()      // 检查下一个 CPU
            [watchdog_buddy.c:86]
            └─ watchdog_hardlockup_check(next_cpu, NULL)
                 [watchdog.c:207]
                 └─ is_hardlockup(next_cpu)      // 下一CPU停跳了?

注意参数:Buddy 传入的 regs = NULL,因为 buddy 检查在 hrtimer 上下文(软中断),不是 NMI。这意味着如果本 CPU 自己发生 hard lockup,show_regs() 拿不到精确的寄存器上下文,只能 dump_stack()


8. 完整判定链路

8.1 检测流程图

本 CPU hrtimer tick (每 4s) ─── watchdog_timer_fn()
                           watchdog_hardlockup_kick()  [watchdog.c:199]
                    ┌────────────────┼────────────────┐
                    │ 本 CPU 心跳+1  │    buddy 检查   │
                    │ atomic_inc(    │                 │
                    │ hrtimer_ints)  │                 │
                    └────────────────┘                 │
                                    watchdog_buddy_check_hardlockup()
                                    [watchdog_buddy.c:86]
                                            smp_rmb()   │
                                    watchdog_hardlockup_check(next_cpu, NULL)
                                    [watchdog.c:207]
                                           ┌───────────┴────────────┐
                                           │ touched?               │
                                           ├── 是 → reset → return  │
                                           ├── 否                   │
                                           │    is_hardlockup()?    │
                                           │    ├── 否 → return     │
                                           │    └── 是              │
                                           │         pr_emerg      │
                                           │         show_regs/    │
                                           │         dump_stack    │
                                           │         panic?        │
                                           └───────────────────────┘

8.2 is_hardlockup() 回顾

// [watchdog.c:183-197]
static bool is_hardlockup(unsigned int cpu)
{
    int hrint = atomic_read(&per_cpu(hrtimer_interrupts, cpu));

    // 计数器增长 → CPU 还能处理 hrtimer 中断 → 活着
    if (per_cpu(hrtimer_interrupts_saved, cpu) != hrint) {
        watchdog_hardlockup_update_reset(cpu);
        return false;
    }

    // 计数器未增长 → 累计 missed
    per_cpu(hrtimer_interrupts_missed, cpu)++;
    if (per_cpu(hrtimer_interrupts_missed, cpu) % watchdog_hardlockup_miss_thresh)
        return false;

    return true;  // missed 累计到 miss_thresh=3 → HARD LOCKUP!
}

8.3 检测时间线 (Buddy)

假设 CPU1 在 T=0 之后锁死:

本CPU tick:   T=0    T=4     T=8     T=12    T=16
hrtimer_int   CPU1=5  CPU1=5  CPU1=5  CPU1=5  CPU1=5
                ↓                  ↓
CPU0→CPU1:    check   check   check   check   check
missed:        0→1     1→2     2→0!    0→1     1→2
             %3=1≠0  %3=2≠0  判定!  %3=1≠0  %3=2≠0
                                   ↑ BUG!

判定时间:从 CPU1 锁死开始需 3×4=12s

miss%3==0 时返回 true——3 次连续 missed。如果中途 hrtimer_interrupts 增长(CPU 恢复),watchdog_hardlockup_update_reset 将 missed 重置为 0。


9. 多种 CPU 拓扑下的检测行为

9.1 正常 4 CPU 环

watchdog_cpus = {0b1111} = {CPU0, CPU1, CPU2, CPU3}

检测链: CPU0 → CPU1 → CPU2 → CPU3 → CPU0 (wrap)

每 4s:
  CPU0 hrtimer: inc(CPU0.ints), buddy_check(CPU1)
  CPU1 hrtimer: inc(CPU1.ints), buddy_check(CPU2)
  CPU2 hrtimer: inc(CPU2.ints), buddy_check(CPU3)
  CPU3 hrtimer: inc(CPU3.ints), buddy_check(CPU0)

9.2 奇数 CPU:最后一个检查谁?

watchdog_cpus = {0b111} = {CPU0, CPU1, CPU2}

检测链: CPU0 → CPU1 → CPU2 → CPU0 (wrap)

每个 CPU 都有且只有一个"被检查对象"。环形结构天然处理奇数和偶数 CPU。

9.3 单 CPU

watchdog_cpus = {0b1} = {CPU0}

watchdog_next_cpu(CPU0) = nr_cpu_ids
→ buddy_check_hardlockup: return (无可检查对象)
→ 单 CPU 系统下 buddy 不检测 hard lockup (但也无所谓——无法调度就是 soft lockup 了)

9.4 CPU 下线中间状态

初始: {CPU0, CPU1, CPU2}, CPU1 正在下线

时刻1: CPU1 disable → touch(CPU2) → smp_wmb() → clear_cpu(CPU1)
时刻2: CPU0 tick → watchdog_next_cpu(0) → CPU2 → check CPU2
        看到 touched → reset → 不报

此后:
  检测链: CPU0 → CPU2 → CPU0 (跳过 CPU1)

10. arch_touch_nmi_watchdog()

// [watchdog.c:152-163]
notrace void arch_touch_nmi_watchdog(void)
{
    /*
     * 使用 __raw 因为某些调用路径允许抢占。
     * 如果抢占可用,中断也应该可用——此时不需要关心
     * watchdog 是否会超时。
     */
    raw_cpu_write(watchdog_hardlockup_touched, true);
}
EXPORT_SYMBOL(arch_touch_nmi_watchdog);

这是在本 CPU 上设置 touched 标志,表示"我知道我要做长时间操作,别误报"。常用于: - 内核调试器入口 - kdb/kgdb 断点 - 长时间 disable interrupt 的代码段

// [watchdog.c:165-168]
void watchdog_hardlockup_touch_cpu(unsigned int cpu)
{
    per_cpu(watchdog_hardlockup_touched, cpu) = true;
}

跨 CPU 版本——在指定的其他 CPU 上设置 touched 标志。被 enable/disable 路径使用。


11. 硬锁报告 (watchdog.c:207-294 对 buddy 的执行路径)

watchdog_hardlockup_check(cpu, regs=NULL)
  ├─ touched? → 是 → reset → return
  ├─ is_hardlockup(cpu)? → 否 → return
  ├─ scx_hardlockup(cpu)? → 是 → return
  ├─ watchdog_hardlockup_warned(cpu)? → 是 → return (去重)
  ├─ pr_emerg("CPU%u: Watchdog detected hard LOCKUP on cpu %u")
  │   this_cpu=报告者, cpu=受害者
  ├─ print_modules() + print_irqtrace_events(current)
  ├─ cpu == this_cpu?
  │   ├─ 是: show_regs(regs)  // regs=NULL → dump_stack()
  │   └─ 否: trigger_single_cpu_backtrace(cpu)
  ├─ 全 CPU backtrace (可选)
  └─ hardlockup_panic? → nmi_panic(NULL, "Hard LOCKUP")
     注意: regs 为 NULL → nmi_panic 可能用其他方法获取上下文

Buddy 模式下 regs = NULL 的后果: - show_regs(regs) 退化为 dump_stack()(只有调用栈,没有寄存器值) - nmi_panic(regs, ...) 退化为普通 panic() - 诊断信息比 perf 模式少——这是 buddy 的固有局限


12. 与 perf 检测器的交互

buddy 和 perf 互斥——一个内核镜像只使用一种 hardlockup 检测器。它们都通过 watchdog_hardlockup_probe() weak 函数接入,但具体使用哪个由编译时 CONFIG_HARDLOCKUP_DETECTOR_PERF / CONFIG_HARDLOCKUP_DETECTOR_BUDDY Kconfig 决定。

共享的代码路径在 watchdog.c

// [watchdog.c:143] CONFIG_HARDLOCKUP_DETECTOR_COUNTS_HRTIMER
// 两个检测器共享此配置——都依赖 hrtimer_interrupts 计数

// [watchdog.c:309-311] weak 函数声明
void __weak watchdog_hardlockup_enable(unsigned int cpu) { }
void __weak watchdog_hardlockup_disable(unsigned int cpu) { }

// [watchdog.c:296-300] 当 CONFIG_HARDLOCKUP_DETECTOR_COUNTS_HRTIMER
// 未开启时,kick 也是空函数
#if !defined(CONFIG_HARDLOCKUP_DETECTOR_COUNTS_HRTIMER)
static inline void watchdog_hardlockup_kick(void) { }
#endif

编译时选择关系:

CONFIG_HARDLOCKUP_DETECTOR
  ├─ CONFIG_HARDLOCKUP_DETECTOR_PERF    → watchdog_perf.c   (启用)
  │   └─ watchdog_hardlockup_probe()   → perf 版本
  │   └─ watchdog_hardlockup_enable()  → perf 版本
  │   └─ watchdog_hardlockup_disable() → perf 版本
  └─ CONFIG_HARDLOCKUP_DETECTOR_BUDDY   → watchdog_buddy.c  (启用)
      └─ watchdog_hardlockup_probe()   → buddy 版本
      └─ watchdog_hardlockup_enable()  → buddy 版本
      └─ watchdog_hardlockup_disable() → buddy 版本

13. 完整场景走查

场景:CPU2 跑飞,中断被关闭

初始: {CPU0, CPU1, CPU2, CPU3} 全部在线

时间轴:

T=0s  各 CPU hrtimer:
        CPU0: ints{0→1}, buddy_check CPU1 → CPU1 OK
        CPU1: ints{0→1}, buddy_check CPU2 → CPU2 OK
        CPU2: ints{0→1}, buddy_check CPU3 → CPU3 OK
        CPU3: ints{0→1}, buddy_check CPU0 → CPU0 OK

       ⚡ CPU2 进入 spin_lock_irqsave 死循环,中断被关闭

T=4s  各 CPU hrtimer:
        CPU0: ints{1→2}, buddy_check CPU1 → CPU1 OK
        CPU1: ints{1→2}, buddy_check CPU2
              → hrtimer_int(CPU2)=1, saved=1 → 未增长
              → missed(CPU2)=0→1, 1%3=1≠0 → 暂不报
        CPU2: **中断关闭,hrtimer 无法触发**
              → hrtimer_int(CPU2) 仍为 1
        CPU3: ints{1→2}, buddy_check CPU0 → CPU0 OK

T=8s  各 CPU hrtimer:
        CPU0: ints{2→3}, buddy_check CPU1 → CPU1 OK
        CPU1: ints{2→3}, buddy_check CPU2
              → hrtimer_int(CPU2)=1, saved=1 → 未增长
              → missed(CPU2)=1→2, 2%3=2≠0 → 暂不报
        CPU2: **仍卡死**
        CPU3: ints{2→3}, buddy_check CPU0 → CPU0 OK

T=12s 各 CPU hrtimer:
        CPU0: ints{3→4}, buddy_check CPU1 → CPU1 OK
        CPU1: ints{3→4}, buddy_check CPU2
              → hrtimer_int(CPU2)=1, saved=1 → 未增长
              → missed(CPU2)=2→3, 3%3=0 → BINGO!
              → 判定 HARD LOCKUP on CPU2
              → pr_emerg("CPU1: Watchdog detected hard LOCKUP on cpu 2")
              → trigger_single_cpu_backtrace(CPU2) // 尝试获取CPU2的栈
              → hardlockup_panic? (默认0,不 panic)
              → watchdog_hardlockup_warned(CPU2) = true
        CPU2: **仍卡死**
        CPU3: ints{3→4}, buddy_check CPU0 → CPU0 OK

T=16s 各 CPU hrtimer:
        CPU0: ints{4→5}, buddy_check CPU1 → CPU1 OK
        CPU1: ints{4→5}, buddy_check CPU2
              → is_hardlockup(CPU2): 仍卡死
              → 但 watchdog_hardlockup_warned(CPU2)=true → return (去重)
        ...

14. Buddy 优缺点总结

方面 评价
硬件依赖 无——适合没有 PMU 或 NMI 的架构 (ARM, RISC-V 等)
代码复杂度 极低——watchdog_buddy.c 仅 103 行
检测延迟 12s (默认),比 perf 的 ~10s 略长
诊断精度 较低——regs=NULL,无寄存器快照
CPU 下线安全性 通过 smp_rmb/smp_wmb 精心处理竞态
单 CPU 系统 不检测 hard lockup(也无法检测——环破环)
虚机误报 无此问题——基于 hrtimer 而非 PMU cycles
多 CPU 一致性 环形互检,任意 CPU 卡死都能被另一个 CPU 查到
与 softlockup 耦合 watchdog_timer_fn 的 hrtimer 上下文中运行,与 softlockup 检测共享同一 tick

15. 四篇文章总结

┌────────────────────────────────────────────────────────────────┐
│                     Lockup Detector 全景                        │
├────────────┬───────────────────┬────────────────────────────────┤
│  层次      │  机制              │  判定                          │
├────────────┼───────────────────┼────────────────────────────────┤
│ 初始化     │ lockup_detector_  │ 硬件探测 → 配置 → hrtimer启    │
│            │ init → setup()    │ 动 → sysctl 注册               │
├────────────┼───────────────────┼────────────────────────────────┤
│ Soft Lockup│ hrtimer →         │ softlockup_fn 能否被调度?      │
│            │ stop_one_cpu      │ touch_ts 是否过期?             │
│            │ _nowait(soft-     │ now - touch_ts > 2×thresh?     │
│            │ lockup_fn)        │                                │
├────────────┼───────────────────┼────────────────────────────────┤
│ Hard Lockup│ Perf NMI 溢出     │ hrtimer_interrupts 计数停止?   │
│ (Perf)     │ → callback        │ missed % 1 == 0?               │
│            │                   │                                │
│ Hard Lockup│ buddy 环形互检    │ hrtimer_interrupts 计数停止?   │
│ (Buddy)    │ → buddy_check     │ missed % 3 == 0?               │
├────────────┼───────────────────┼────────────────────────────────┤
│ CPU Hotplug│ lockup_detector_  │ 上线: enable, 下线: disable     │
│            │ online/offline_cpu │ buddy: 双重touch + smp_wmb     │
├────────────┼───────────────────┼────────────────────────────────┤
│ /proc 接口 │ sysctl             │ watchdog, nmi_watchdog,        │
│            │                   │ soft_watchdog, thresh, cpumask  │
└────────────┴───────────────────┴────────────────────────────────┘

关键源文件索引

文件 行数 核心内容
kernel/watchdog.c 1400 lockup 检测核心:init、softlockup、is_hardlockup、sysctl
kernel/watchdog_perf.c 314 perf NMI hardlockup 检测器
kernel/watchdog_buddy.c 103 buddy 环形互检 hardlockup 检测器
include/linux/nmi.h 233 公共 API、位掩码常量
init/main.c:1693 lockup_detector_init() 调用点

下一篇文章

您已读完整个系列。返回 目录 查看概览。


💬 评论