[toc]

drivers/watchdog Watchdog子系统(Watchdog Subsystem) 确保系统在软件故障时自动重启

历史与背景

这项技术是为了解决什么特定问题而诞生的?

Watchdog(看门狗)子系统的诞生是为了解决一个在计算系统中,尤其是高可靠性系统中,至关重要的问题:如何从致命的软件故障中自动恢复

软件系统可能会因为各种原因(如内核死锁、驱动程序中的无限循环、用户空间关键进程假死)而完全“卡死”(Hang),导致系统停止响应。在这种状态下,系统无法执行任何有效任务,也无法被正常地远程管理。对于无人值守的嵌入式设备或需要高可用性的服务器而言,这种状态是不可接受的。

Watchdog技术通过一个简单的“死人开关”(Dead Man’s Switch)机制来解决这个问题:

  1. 它提供一个硬件或软件定时器,一旦启动,就会开始倒计时。
  2. 系统中的监控软件(通常是一个用户空间的守护进程)必须周期性地“喂狗”(Feed the dog)或“踢狗”(Kick the dog),即重置这个定时器。
  3. 如果监控软件因为系统卡死而未能按时重置定时器,定时器就会超时。
  4. 超时后,Watchdog硬件会触发一个强制的、不可屏蔽的系统复位(Reset),使系统重启到一个已知的、干净的状态。

Watchdog框架的出现,则是为了将内核中五花八门的Watchdog硬件驱动统一起来,为用户空间提供一个标准的、可移植的交互接口。

它的发展经历了哪些重要的里程碑或版本迭代?

  • 早期的独立驱动:在统一框架出现之前,许多Watchdog硬件的驱动都是独立实现的,各有各的/proc接口或私有的ioctl命令,导致用户空间的监控程序难以做到通用。
  • Watchdog API v1的建立:这是最重要的里程碑。内核引入了一个标准的字符设备接口——/dev/watchdog,并定义了一套通用的ioctl命令(如WDIOC_KEEPALIVE, WDIOC_SETTIMEOUT)。这使得任何用户空间的守护进程(如watchdogd)都可以通过这个标准接口与任何遵循该框架的Watchdog驱动进行交互,实现了硬件的解耦。
  • “Pre-timeout”支持的引入:为了在硬重启之前提供更精细的故障诊断机会,框架增加了“预超时”功能。一些高级的Watchdog硬件可以在最终复位前的几秒钟,先触发一个不可屏蔽中断(NMI)或普通中断。内核可以捕获这个中断,执行一些紧急操作,例如打印内核调试信息(kdump)或通知高可用性软件准备故障转移。
  • “nowayout”特性的标准化:这是一个关键的安全特性。一旦通过模块参数或编译时选项开启,它会阻止任何程序(甚至是root)在Watchdog启动后将其关闭。这可以防止一个设计不佳的应用程序在退出时意外地关闭了Watchdog,从而使系统失去保护。

目前该技术的社区活跃度和主流应用情况如何?

Watchdog子系统是Linux内核中一个非常基础、稳定且对可靠性至关重要的部分。它被广泛应用于:

  • 嵌入式系统:工业控制器、路由器、物联网设备、汽车电子等所有需要无人值守且高可靠运行的场景。
  • 服务器:在高可用性(High-Availability)集群中,Watchdog是实现STONITH(Shoot The Other Node In The Head)/ fencing机制的关键,用于确保一个故障节点被可靠地重启,防止“脑裂”问题。
  • 通用系统:现代的系统管理器(如systemd)也集成了Watchdog功能,可以监控关键服务,并在服务卡死时自动重启系统。

核心原理与设计

它的核心工作原理是什么?

Watchdog框架是一个典型的三层模型,连接了硬件、内核和用户空间。

  1. 硬件层(Watchdog Timer, WDT)
    • 这是一个物理硬件定时器(或由内核模拟的软件定时器softdog)。
    • 它的核心是一个倒数计数器。
    • 当计数器减到0时,它会通过一个硬件信号线强制复位整个SoC或主板。
  2. 内核层(Watchdog Core and Driver)
    • Watchdog核心 (watchdog_core.c):实现了/dev/watchdog字符设备,并处理来自用户空间的open, write, ioctl等文件操作。
    • Watchdog硬件驱动:这是特定于硬件的驱动程序。它实现了一组标准的回调函数struct watchdog_ops,包括:
      • .start(): 启动硬件定时器。
      • .stop(): 停止硬件定时器。
      • .ping().keepalive(): 重置硬件定时器(“喂狗”)。
      • .set_timeout(): 设置超时时间。
    • 核心层的作用就是将用户空间的标准请求(如一次write操作)转换为对具体硬件驱动.ping()回调函数的调用。
  3. 用户空间层(Daemon)
    • 这是一个守护进程,例如watchdog包中的watchdogd,或者systemd
    • 它在启动时open("/dev/watchdog")。一旦打开成功,Watchdog硬件通常就会被驱动start()
    • 该进程进入一个主循环,周期性地(例如每隔几秒)向/dev/watchdog的文件描述符执行一次write操作或WDIOC_KEEPALIVE ioctl调用,从而触发内核去“喂狗”。
    • 这个守护进程通常还会执行一些系统健康检查,例如检查系统负载、内存使用情况、特定进程是否存在等。如果检查发现系统处于不健康状态,它可以故意停止“喂狗”,从而主动触发Watchdog重启系统。

它的主要优势体现在哪些方面?

  • 高可靠性:提供了一种从软件完全卡死状态下恢复的最终手段。
  • 解耦与标准化:统一的/dev/watchdog接口将用户空间监控程序与底层硬件驱动完全解耦。
  • 驱动开发简化:驱动开发者只需实现一小组定义良好的硬件操作回调,即可将他们的设备接入整个框架。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 无法解决硬件问题:Watchdog只能应对软件故障,无法修复硬件损坏。
  • 可能导致不必要的重启:如果系统负载过高,导致用户空间守护进程被调度器延迟,没能及时“喂狗”,就可能触发一次不必要的重启。因此,超时时间的设定需要仔细权衡。
  • 开发风险(nowayout:在开发阶段,如果nowayout被启用,而用户空间的守护进程因为配置错误等原因未能正常启动,系统将会陷入无限的重启循环(Boot Loop),给调试带来困难。
  • 诊断信息有限:一次简单的Watchdog重启本身不会告诉你系统为何卡死。需要配合pre-timeoutkdump等机制才能在重启前捕获到有用的调试信息。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

Watchdog是任何无法接受长时间服务中断或需要自主恢复的系统的标准解决方案。

  • 远程无人值守设备:部署在野外的气象站、通信基站、太空中的卫星等。
  • 高可用性集群:作为 fencing 设备,防止集群中的节点因通信中断而错误地认为其他节点已死,从而同时去抢占共享资源,导致数据损坏。
  • 生命支持或安全关键系统:在医疗设备或工业控制系统中,一个可预测的快速重启通常比一个未知的、持续的故障状态更安全。

是否有不推荐使用该技术的场景?为什么?

  • 典型的交互式桌面:对于普通桌面用户,一个意外的、无预警的重启可能会导致大量未保存的工作丢失,其体验通常比一个暂时卡死的应用程序更差。用户通常倾向于手动去杀死问题进程。
  • 需要长时间保持状态的非容错计算:例如,一个正在进行数天之久的科学计算任务,如果其软件没有设计检查点(Checkpoint)机制,一次Watchdog重启将意味着所有计算成果的丢失。在这种场景下,可能需要禁用Watchdog,但这本身也带来了系统可能永久卡死的风险。

对比分析

请将其 与 其他相似技术 进行详细对比。

Watchdog是一种独特的、 अंतिम的系统级恢复机制。其对比对象通常是其他层面的高可用或故障检测技术。

特性 Hardware Watchdog High-Availability (HA) Software Kernel Panic/Oops
检测对象 整个系统的无响应(软件完全停止“喂狗”)。 服务、应用程序或网络节点的无响应 内核自身检测到的非法操作(如访问无效内存)。
恢复粒度 整个系统(硬重启)。 单个服务(重启服务)或单个节点(触发fencing,可能也用到watchdog)。 内核(通常会导致系统停机或重启)。
触发方式 被动超时。因为“喂狗”动作没有发生。 主动探测。通过心跳检测、服务端口检查等主动发现问题。 主动触发。当CPU执行到错误指令时,由内核的异常处理代码主动触发。
解决的问题 系统完全死锁/假死,内核和应用都无法自救。 应用程序崩溃、服务无响应,但操作系统本身通常还活着。 内核代码遇到了无法恢复的内部错误
  • 关系:它们是互补而非竞争的关系。一个完整的高可用系统会同时使用这几种技术:
    • Kernel Oops/Panic处理内核自身的致命错误。
    • HA Software监控并恢复应用程序级别的故障。
    • 当HA软件发现某个节点彻底失联,或者它自己所在的节点完全卡死时,Watchdog作为最后的保障,会强制重启该节点,让其恢复到一个干净的状态,从而可以重新加入集群。

drivers/watchdog/watchdog_core.c

Watchdog核心初始化与延迟注册机制

本代码片段是Linux内核看门狗(Watchdog)子系统核心的初始化部分。其核心功能是建立看门狗框架的基础设施,并实现一个“延迟注册”机制。这个机制用于解决一个时序问题:某些看门狗硬件的驱动程序可能在内核启动的很早阶段就被探测(probe),甚至早于看门狗核心子系统自身的初始化。延迟注册确保了这些“早到”的驱动不会因核心未就绪而注册失败,而是被安全地放入一个队列中,待核心初始化完毕后再统一进行正式注册。

实现原理分析

此代码的实现巧妙地利用了内核的初始化调用顺序和同步机制来保证系统的健壮性。

  1. 两阶段初始化:

    • watchdog_dev_init(): 此函数(在此代码段中未显示其定义)是第一阶段。它负责创建所有具体看门狗设备都依赖的公共资源,例如注册/sys/class/watchdog设备类,以及通过alloc_chrdev_region申请用于/dev/watchdogX的主/次设备号池。
    • watchdog_deferred_registration(): 这是第二阶段。它处理那些在第一阶段完成前就已尝试注册的设备。
  2. 延迟注册机制:

    • 内核中存在一个全局链表wtd_deferred_reg_list和一个布尔标志wtd_deferred_reg_done(初始为false)。
    • 当一个具体的看门狗驱动调用watchdog_register_device()(未在此处显示)时,该函数会首先检查wtd_deferred_reg_done标志。
    • 如果标志为false,意味着看门狗核心尚未就绪。此时,注册函数不会报错,而是将该看门狗设备结构体(struct watchdog_device)添加到一个临时的延迟注册链表wtd_deferred_reg_list中。
    • 如果标志为true,则直接进行正常的设备注册流程。
  3. 同步与执行:

    • subsys_initcall_sync(watchdog_init): 这个宏是关键。subsys_initcall确保了watchdog_init在大多数设备驱动probe之前执行。后缀_sync则提供了一个更强的保证:它确保watchdog_init函数执行完毕后,内核才会继续下一阶段的初始化调用。
    • 当内核执行到watchdog_init时,它首先调用watchdog_dev_init完成基础设施的创建。
    • 随后,watchdog_deferred_registration被调用。它锁住互斥体,将wtd_deferred_reg_done标志设置为true,然后遍历wtd_deferred_reg_list链表,将所有等待的设备逐一取出并调用__watchdog_register_device进行正式注册。

通过这个机制,无论具体的看门狗驱动何时被探测,其注册请求都能被正确处理,从而避免了因初始化顺序依赖而导致的启动问题。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// watchdog_deferred_registration: 执行延迟注册的处理函数。
// 作用是将在核心初始化完成前就尝试注册的看门狗设备进行正式注册。
static int __init watchdog_deferred_registration(void)
{
// 加锁,以保护延迟注册列表和完成标志。
mutex_lock(&wtd_deferred_reg_mutex);
// 设置完成标志为true,此后新的注册请求将直接执行,不再进入延迟列表。
wtd_deferred_reg_done = true;
// 循环处理延迟注册列表,直到列表为空。
while (!list_empty(&wtd_deferred_reg_list)) {
struct watchdog_device *wdd;

// 获取列表中的第一个待处理的看门狗设备。
wdd = list_first_entry(&wtd_deferred_reg_list,
struct watchdog_device, deferred);
// 将该设备从延迟列表中移除。
list_del(&wdd->deferred);
// 调用内部的、真正的设备注册函数来完成注册。
__watchdog_register_device(wdd);
}
// 解锁。
mutex_unlock(&wtd_deferred_reg_mutex);
return 0;
}

// watchdog_init: 看门狗子系统的核心初始化函数。
static int __init watchdog_init(void)
{
int err;

// 首先,初始化看门狗的设备基础设施(如字符设备号、sysfs类等)。
err = watchdog_dev_init();
if (err < 0)
return err;

// 然后,处理所有在上述初始化完成前就已经被探测到的、等待注册的看门狗设备。
watchdog_deferred_registration();
return 0;
}

// watchdog_exit: 看门狗子系统的退出/清理函数。
static void __exit watchdog_exit(void)
{
// 以相反的顺序清理资源:首先清理设备基础设施。
watchdog_dev_exit();
// 然后销毁用于分配看门狗ID的IDA(ID Allocator)。
ida_destroy(&watchdog_ida);
}

// 将watchdog_init注册为同步的子系统初始化调用。
// _sync确保此函数执行完毕后,其他初始化才会继续,这对于延迟注册机制至关重要。
subsys_initcall_sync(watchdog_init);
// 将watchdog_exit注册为模块退出调用。
module_exit(watchdog_exit);

// 标准的模块作者、描述和许可证信息。
MODULE_AUTHOR("Alan Cox <alan@lxorguk.ukuu.org.uk>");
MODULE_AUTHOR("Wim Van Sebroeck <wim@iguana.be>");
MODULE_DESCRIPTION("WatchDog Timer Driver Core");
MODULE_LICENSE("GPL");

drivers/watchdog/watchdog_dev.c

Watchdog核心设备初始化:创建内核工作线程、设备类与设备号池

本代码片段的功能是初始化Linux Watchdog核心子系统中与设备模型和字符设备接口相关的部分。它在内核启动的早期阶段执行,为后续具体的硬件看门狗驱动程序(hardware watchdog drivers)注册和运行搭建必要的基础软件设施。这包括创建一个高优先级的内核工作线程、注册一个 “watchdog” 设备类,以及预留一块用于看门狗设备的字符设备号区域。

实现原理分析

此函数的实现集成了内核线程、设备模型和字符设备子系统的功能,以构建一个健壮的框架。

  1. 创建内核工作线程 (kthread_run_worker):

    • 函数首先创建一个名为 “watchdogd” 的内核工作线程(kworker)。这个线程专门用于异步处理看门狗相关的任务,例如处理预超时(pre-timeout)通知。将这些任务放在一个专用的工作线程中,可以避免阻塞调用者上下文,并允许以统一的方式处理来自不同硬件看门狗驱动的事件。
  2. 设置实时调度策略 (sched_set_fifo):

    • 这是一个至关重要的步骤。函数将 “watchdogd” 线程的调度策略设置为SCHED_FIFO(先进先出)。这是一种实时调度策略,赋予了该线程非常高的运行优先级。此举确保了当有看门狗相关的紧急任务(如响应预超时中断)需要执行时,”watchdogd” 线程能够立即抢占大多数其他普通内核线程或用户进程,从而保证了看门狗子系统对时间敏感事件的及时响应,这是维持系统稳定性的关键。
  3. 注册设备类 (class_register):

    • 通过调用class_register并传入watchdog_class(在别处定义),函数在sysfs中创建了/sys/class/watchdog/目录。这个目录充当了所有看门狗设备的逻辑容器。当一个具体的硬件看门狗驱动(如stm32-iwdg)注册其设备时,它会把自己归入这个类,从而在用户空间表现为/sys/class/watchdog/watchdog0这样的符号链接,提供了一个标准化的接口。
  4. 分配字符设备号 (alloc_chrdev_region):

    • 看门狗设备通过一个字符设备节点(如/dev/watchdog/dev/watchdog0)向用户空间提供主接口。alloc_chrdev_region函数向内核动态申请并预留了一段连续的字符设备号(主/次设备号对),最多可支持MAX_DOGS个看门狗设备。这为后续创建设备节点提供了必要的“门牌号”。
  5. 错误处理:

    • 函数包含了完善的错误处理逻辑。如果在初始化过程中的任何一步失败,它都会以相反的顺序撤销所有已经成功的操作(例如,如果分配设备号失败,它会注销设备类并销毁工作线程),以确保系统不会处于部分初始化或资源泄漏的状态。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// watchdog_dev_init: 初始化看门狗核心的设备部分。
// 描述:
// 为看门狗设备分配一段字符设备节点区域。
// 返回值: 成功则为0,否则为错误码。
int __init watchdog_dev_init(void)
{
int err;

// 创建一个名为 "watchdogd" 的内核工作线程。
// 0 表示工作线程可以在任何CPU上运行。
watchdog_kworker = kthread_run_worker(0, "watchdogd");
if (IS_ERR(watchdog_kworker)) {
pr_err("Failed to create watchdog kworker\n");
return PTR_ERR(watchdog_kworker);
}
// 将工作线程的调度策略设置为SCHED_FIFO(实时先进先出),赋予其高优先级。
sched_set_fifo(watchdog_kworker->task);

// 注册 "watchdog" 设备类,这将在sysfs中创建 /sys/class/watchdog/ 目录。
err = class_register(&watchdog_class);
if (err < 0) {
pr_err("couldn't register class\n");
goto err_register; // 如果失败,跳转到错误处理。
}

// 动态申请一段字符设备号区域,用于watchdog设备。
// &watchdog_devt: 用于存储分配到的起始设备号。
// 0: 起始次设备号。
// MAX_DOGS: 希望分配的设备数量。
// "watchdog": 与该区域关联的名称。
err = alloc_chrdev_region(&watchdog_devt, 0, MAX_DOGS, "watchdog");
if (err < 0) {
pr_err("watchdog: unable to allocate char dev region\n");
goto err_alloc; // 如果失败,跳转到错误处理。
}

return 0; // 初始化成功。

err_alloc:
// 错误处理路径:注销已经注册的设备类。
class_unregister(&watchdog_class);
err_register:
// 错误处理路径:销毁已经创建的内核工作线程。
kthread_destroy_worker(watchdog_kworker);
return err;
}