软锁定(softlockup)检测器和硬锁定(hardlockup)检测器(又名 nmi_watchdog)

Linux 内核可以充当看门狗(watchdog),用于检测软锁定和硬锁定。

“软锁定”(softlockup)被定义为这样一种 bug:它导致内核在内核模式下循环超过 20 秒(详见下文“实现”部分),而没有给其他任务运行的机会。检测到软锁定后,系统会显示当前的栈回溯(stack trace),并且默认情况下系统将保持锁定状态。或者,也可以将内核配置为触发 panic;为此提供了 sysctl “kernel.softlockup_panic”、内核参数 “softlockup_panic”(详见“内核命令行参数”)以及编译选项 “BOOTPARAM_SOFTLOCKUP_PANIC”。

“硬锁定”(hardlockup)被定义为这样一种 bug:它导致 CPU 在内核模式下循环数秒(详见下文“实现”部分),而不允许其他中断运行。与软锁定类似,检测到硬锁定后会显示当前的栈回溯,并且系统将保持锁定状态,除非更改默认行为。可以通过 sysctl ‘hardlockup_panic’、编译时开关 “BOOTPARAM_HARDLOCKUP_PANIC” 以及内核参数 “nmi_watchdog”(详见“内核命令行参数”)来更改此行为。

panic 选项可以与 panic_timeout 结合使用(该超时时间通过名称容易让人混淆的 “kernel.panic” sysctl 来设置),从而使系统在指定时间后自动重启。

配置

内核提供了一个开关,允许管理员配置此周期。“watchdog_thresh”参数(默认值为 10 秒)用于控制阈值。对于特定环境而言,合适的数值需要在对锁定的快速响应与检测开销之间进行权衡。

实现

软锁定和硬锁定检测器都是围绕 hrtimer(高精度定时器)构建的。此外,软锁定检测器会定期调度一个作业,而硬锁定检测器可能会在支持的架构上使用 Perf/NMI 事件。

频率与心跳

检测器的核心是一个 hrtimer。它具有多种用途

  • 为软锁定检测器调度看门狗作业

  • 为硬锁定检测器增加中断计数器(心跳)

  • 检测软锁定

  • 在 Buddy(伙伴)模式下检测硬锁定

该 hrtimer 的周期为 2*watchdog_thresh/5,默认情况下为 4 秒。在硬锁定检测器介入之前,hrtimer 有两次或三次机会生成中断(心跳)。

软锁定检测器

看门狗作业由 hrtimer 调度,并在一个 stop 调度线程中运行。每次调度时,它都会更新一个时间戳。如果该时间戳在 2*watchdog_thresh 秒(软锁定阈值)内未更新,“软锁定检测器”(编写在 hrtimer 回调函数中)会将有用的调试信息转储到系统日志中,之后如果收到指示,它将调用 panic,或者恢复执行其他内核代码。

硬锁定检测器 (NMI/Perf)

在支持 NMI(不可屏蔽中断)perf 事件的架构上,每隔 “watchdog_thresh” 秒就会生成一个周期性的 NMI。

如果系统中的任何 CPU 在 “watchdog_thresh” 时间窗口内没有收到任何 hrtimer 中断(心跳),“硬锁定检测器”(NMI perf 事件的处理程序)将生成内核警告或调用 panic。

检测开销 (NMI)

检测锁定所需的时间可能会有所不同,具体取决于锁定发生时相对于 NMI 检查窗口的时间点。以下示例假设 watchdog_thresh 为 10。

  • 最佳情况:锁定发生在第一个心跳到期之前不久。检测器将在下一次检查时几乎立即注意到丢失的 hrtimer 中断。

    Time 100.0: cpu 1 heartbeat
    Time 100.1: hardlockup_check, cpu1 stores its state
    Time 103.9: Hard Lockup on cpu1
    Time 104.0: cpu 1 heartbeat never comes
    Time 110.1: hardlockup_check, cpu1 checks the state again, should be the same, declares lockup
    
    Time to detection: ~6 seconds
    
  • 最坏情况:锁定发生在紧随 NMI 检查之后发生的有效中断(心跳)后不久。下一次 NMI 检查发现中断计数已更改(由于那一次心跳),便假定 CPU 运行正常,并重置基准。锁定直到随后的检查时才被检测到。

    Time 100.0: hardlockup_check, cpu1 stores its state
    Time 100.1: cpu 1 heartbeat
    Time 100.2: Hard Lockup on cpu1
    Time 110.0: hardlockup_check, cpu1 stores its state (misses lockup as state changed)
    Time 120.0: hardlockup_check, cpu1 checks the state again, should be the same, declares lockup
    
    Time to detection: ~20 seconds
    

硬锁定检测器 (Buddy)

在 NMI perf 事件不可用(或被禁用)的架构或配置上,内核可能会使用 “buddy”(伙伴)硬锁定检测器。该机制需要 SMP(对称多处理)支持。

在此模式下,每个 CPU 会被分配一个“伙伴”CPU 进行监视。监视 CPU 运行它自己的 hrtimer(与用于软锁定检测的定时器相同),并检查伙伴 CPU 的 hrtimer 中断计数是否有所增加。

为了确保及时性并避免误报,伙伴系统会在每个 hrtimer 间隔(2*watchdog_thresh/5,默认情况下为 4 秒)执行检查。它使用 3 作为丢失中断的阈值。如果伙伴的中断计数在连续 3 次检查中都没有改变,则假定伙伴 CPU 发生了硬锁定(中断被禁用)。随后,监视 CPU 将触发硬锁定响应(警告或 panic)。

检测开销 (Buddy)

在默认检查间隔为 4 秒的情况下(watchdog_thresh = 10)

  • 最佳情况:锁定发生在检查前不久。

    在大约 8 秒内检测到(0 秒到第 1 次检查 + 4 秒到第 2 次 + 4 秒到第 3 次)。

  • 最坏情况:锁定发生在检查后不久。

    在大约 12 秒内检测到(4 秒到第 1 次检查 + 4 秒到第 2 次 + 4 秒到第 3 次)。

Buddy 检测器的局限性

  1. 全 CPU 锁定:如果所有 CPU 同时锁定,buddy 检测器将无法检测到该情况,因为监视 CPU 也被冻结了。

  2. 栈回溯:与 NMI 检测器不同,buddy 检测器无法直接中断被锁定的 CPU 以获取栈回溯。它依赖于体系结构特定的机制(如 NMI 回溯支持)来尝试检索被锁定 CPU 的状态。如果缺少此类支持,日志可能只会显示发生了锁定,而无法提供被锁定 CPU 的栈信息。

看门狗核心排除

默认情况下,看门狗在所有在线核心上运行。但是,在配置了 NO_HZ_FULL 的内核上,默认情况下看门狗仅在 housekeeping(守护)核心上运行,而不在 “nohz_full” 启动参数指定的核心上运行。如果我们允许看门狗默认在 “nohz_full” 核心上运行,我们就必须运行定时器滴答(timer ticks)来激活调度器,这会阻止 “nohz_full” 功能保护这些核心上的用户代码免受内核干扰。当然,在 nohz_full 核心上默认禁用它意味着,当这些核心确实进入内核时,默认情况下我们将无法检测它们是否发生锁定。然而,允许看门狗继续在 housekeeping(非无滴答)核心上运行,意味着我们将继续在这些核心上正确检测锁定。

无论哪种情况,都可以通过 kernel.watchdog_cpumask sysctl 来调整排除运行看门狗的核心集合。对于 nohz_full 核心,这对于调试内核似乎挂起在 nohz_full 核心上的情况非常有用。