嵌入式面试真题第 14 题:防范业务逻辑死锁的系统健康监督与看门狗设计
问题
在多任务嵌入式系统、RTOS 固件、边缘网关、工业控制器、车载控制单元、消费电子设备或其他长期无人值守的软件系统中,通常会配置硬件看门狗,以便系统发生严重异常时自动复位。
传统实现往往由一个独立 Watchdog Task、Idle Hook、主循环或高优先级定时任务周期性喂狗。只要这个喂狗节点还能获得 CPU 时间,硬件看门狗就不会超时。然而,系统可能已经出现以下“进程还活着、业务却不再工作”的假活状态:
- 两个或多个任务形成循环等待,业务路径死锁。
- 高优先级任务持续运行,低优先级关键任务长期饥饿。
- 任务不断重试或相互唤醒,但业务状态不再前进,形成活锁。
- 消息队列、缓冲区或对象池耗尽,生产者和消费者互相等待。
- 状态机卡在某个中间状态,周期函数仍在调用,但事务永远无法完成。
- 外设、总线、文件系统、网络栈或协处理器请求悬挂,任务仍在等待或轮询。
- 某个 CPU 核、调度 tick、关键中断或时间基准失效,而其他核仍能继续喂狗。
- 看门狗任务本身优先级过高、依赖过少,反而绕过了真正的业务健康状态。
面对这种问题,如何把看门狗从“某个线程周期运行就喂狗”的简单定时器,重新设计成一套通用、可量化、可诊断、可扩展的系统健康监督机制?如何定义喂狗条件、采集健康证据、监控锁和队列、处理启动与低功耗阶段、保存复位现场,并参考已有开源组件构建可落地方案?
回答
结论:硬件看门狗只能作为最终执行器,不能直接代表系统健康。正确方案是在业务任务与硬件看门狗之间增加一个独立的 Health Supervisor,由所有关键执行单元提交可验证的健康证据;只有当任务执行、业务进度、时限约束、资源锁、队列流动、调度能力、时间基准和关键依赖均满足当前运行模式的健康契约时,Supervisor 才允许喂硬件看门狗。
喂狗条件必须从:
1 | “看门狗任务本轮获得了 CPU” |
升级为:
1 | “系统在规定时间窗内完成了所有必要工作,并且没有出现不可接受的停滞、资源占用或依赖失效” |
这套设计的核心不是简单增加若干 heartbeat bit,而是建立四层监督链路:
- 执行层健康:关键任务、关键中断、每个 CPU 核和调度器是否仍有执行机会。
- 业务层健康:事务、状态机、队列和数据流是否持续产生可观察的进展。
- 资源层健康:Mutex、Semaphore、队列、内存池、总线和外设是否在限定时间内被释放或完成。
- 恢复层健康:异常能否先局部恢复、再子系统重启、最终由硬件看门狗复位,并在复位前留下足够诊断证据。
总体架构
1 | flowchart LR |
业务任务不直接操作硬件 Watchdog 寄存器,也不应该各自独立喂同一个硬件看门狗。它们只负责提交自身职责范围内的健康证据。Health Supervisor 读取这些证据、执行规则评估,并作为唯一喂狗授权点。
硬件看门狗应被视为无法依赖常规软件路径时的最终恢复手段。软件 Supervisor、日志系统、任务调度器甚至整个内核都可能失效,因此监督链应尽量具备分层和独立性:软件任务看门狗负责多任务聚合,硬件看门狗负责调度器或系统级失效,必要时再由独立 PMIC、外部 MCU 或安全岛监控主处理器。
问题应如何抽象
这道题表面上是“两个任务互相等待 Mutex,而看门狗任务仍在喂狗”,但更通用的抽象是:
系统存在至少一个仍可执行的路径,因此传统 watchdog heartbeat 持续更新;与此同时,决定系统服务能力的关键不变量已经失效,或者关键状态长时间没有推进。
这类故障可统一称为 业务假活、逻辑停滞或 liveness failure。它不同于直接崩溃:程序计数器仍在变化,部分线程仍在调度,甚至日志仍有输出,但系统已无法完成其主要职责。
常见故障类型
1 | mindmap |
因此,单纯要求“每个任务每秒置位一次”仍然不够。一个任务完全可以在错误循环里持续置位;一个状态机可以反复进入同一状态;一个消息处理线程可以一直被调度,却没有成功消费任何有效消息。健康证据必须能区分以下三件事:
| 层次 | 需要回答的问题 | 仅靠普通 heartbeat 能否覆盖 |
|---|---|---|
| Execution | 任务是否获得过 CPU、ISR 是否发生、调度 tick 是否工作 | 部分可以 |
| Progress | 任务是否完成了有意义的业务步骤 | 不能 |
| Correctness | 完成结果是否满足协议、资源和状态约束 | 不能 |
健康模型:从 Heartbeat 到 Health Contract
推荐把每个关键参与者定义成一个受监督实体 health participant。参与者不是必须等同于线程,它可以是:
- 一个 RTOS 任务。
- 一个周期控制回路。
- 一个协议会话或连接。
- 一个状态机实例。
- 一个 DMA/ISR 数据通路。
- 一个 CPU 核的调度探针。
- 一个关键外设或协处理器。
- 一个共享服务,如文件系统、存储服务、网络管理器或内存池。
每个参与者注册一份 Health Contract,至少包含:
1 | participant_id 参与者唯一标识 |
运行过程中,参与者提交不同类型的证据:
1 | execution_heartbeat 本任务或回调确实执行过 |
为什么必须区分执行心跳与进度心跳
假设音频任务每 10ms 醒来一次,并执行以下错误循环:
1 | while (true) { |
heartbeat 持续变化,只能证明线程获得了 CPU,不能证明音频帧被成功处理。正确做法是同时维护:
1 | exec_counter++ 每次调度周期都更新 |
Supervisor 可以据此区分:
| 观测结果 | 可能含义 | 推荐判断 |
|---|---|---|
| exec 不变,progress 不变 | 未调度、阻塞、死锁、线程退出 | 故障 |
| exec 变化,progress 不变 | 活锁、重复失败、依赖阻塞、空转 | 超过业务期限后故障 |
| exec 变化,progress 变化 | 正常推进 | 健康候选 |
| exec 不变,progress 变化 | 证据采集设计错误、其他上下文代替更新 | 需要审计 |
喂狗门控:所有关键健康条件的合取
硬件喂狗不应由任意一个参与者直接触发,而应由 Feed Gate 统一决策。最简单的逻辑是关键条件的 AND:
1 | feed_allowed = |
实际系统需要允许可选功能、降级运行和启动阶段宽限,因此不一定是机械的全局 AND。更合理的是按关键度和运行模式计算:
1 | critical participant failed -> 立即阻止喂狗或进入短暂诊断阶段 |
喂狗决策流程
1 | flowchart TD |
Feed Gate 还应避免“旧证据重复使用”。每次喂狗必须消费一个新的监督周期或新的 health epoch,防止某个参与者早先提交过一次健康标志后永久有效。
推荐使用单调计数器而不是单纯布尔位:
1 | last_seen_exec_counter[i] |
Supervisor 每轮比较新旧值,再更新时间戳。这样可以避免任务只在启动时置一次 bit,随后一直被误判为健康。
关键任务应提交哪些健康证据
不同任务不能用完全相同的 heartbeat 语义。健康点必须放在“任务确实完成核心职责”的位置,而不是任务循环入口、无条件定时回调或错误处理循环中。
| 任务类型 | 不推荐的健康点 | 推荐的健康点 |
|---|---|---|
| 传感器采集 | 任务被唤醒时 | 完成一次采样、校验并发布有效数据后 |
| 控制回路 | 周期函数入口 | 控制量计算完成并成功写入执行器后 |
| 音频处理 | 每次循环开始 | 音频帧成功处理并提交 DMA 后 |
| 蓝牙/网络 | 收到任意事件 | 事件队列持续消费、连接状态机推进或数据确认完成后 |
| 存储任务 | 开始处理请求 | 请求完成、提交或明确返回错误后 |
| UI 渲染 | 刷新定时器触发 | 一帧渲染完成并提交显示控制器后 |
| OTA 升级 | 下载线程仍在运行 | 分块下载、校验、写入和最终提交阶段分别推进后 |
| 文件系统服务 | 服务线程被调度 | 请求队列完成数增加且无长时间挂起请求 |
业务进度指标的设计原则
- 必须对应可验证完成点:在事务提交、帧输出、状态迁移或请求完成后更新。
- 必须单调或可比较:优先使用递增计数、序号、epoch 或最后完成时间。
- 不能依赖正在被监控的锁:否则 Supervisor 为读取健康状态也可能被同一死锁阻塞。
- 不能由无关任务代写:健康证据应能定位责任实体。
- 不能无限宽限:长期等待外部依赖也必须有业务级超时、降级或隔离策略。
- 要允许合法空闲:任务没有输入时,不应强迫 progress counter 变化,而应证明“空闲是被允许的”。
合法空闲可以通过以下条件表达:
1 | input_queue_empty |
只有在没有待办工作时,缺少业务进度才可视为正常;一旦存在 pending work,就必须在 deadline 内看到完成数、消费指针或状态 epoch 推进。
锁超时与死锁监控
题目中的 Mutex 循环等待应由两类机制共同覆盖:
- 开发期预防与检测:固定锁顺序、静态分析、运行时 lock dependency 检查、超时断言和故障注入。
- 产品运行期监督与恢复:记录锁 owner、持有时长、等待者和调用点,超过阈值后禁止喂狗并保存现场。
锁元数据
建议为关键锁增加轻量包装层,至少记录:
1 | lock_id |
锁获取和释放路径更新原子元数据:
1 | sequenceDiagram |
Wait-for Graph
若资源允许,可构造任务与锁的等待图:
1 | flowchart LR |
图中出现有向环,意味着存在循环等待条件。小型 MCU 不一定要实时运行完整图算法,但可以采用以下折中:
- 仅监控少量关键锁。
- 为锁定义全局层级
lock_rank,运行时断言只能按 rank 递增获取。 - 记录每个任务“当前持有锁集合”和“正在等待锁”。
- 仅在 Supervisor 发现超时后离线构造等待链。
- 在 Debug/Fault-Injection 构建中启用完整依赖图,在 Release 构建中保留轻量持有时长监控。
Linux 的 lockdep 是典型参考:它记录锁类别之间的获取顺序,并检测可能形成循环依赖的锁顺序。该机制主要用于开发和验证,不能替代产品中的恢复策略,但其“把锁获取顺序表示成依赖图”的思想非常适合嵌入式关键锁审计。
锁超时不是简单地统一设为一个值
锁的合理阈值应根据临界区最坏执行时间、可被抢占时间和平台抖动确定:
1 | T_lock_limit >= T_critical_section_wc |
不同锁应有不同阈值:
| 锁类型 | 典型阈值策略 |
|---|---|
| 寄存器或短链表保护锁 | 微秒至低毫秒级,超时通常是严重缺陷 |
| Flash 擦写或文件系统事务锁 | 可到数十或数百毫秒,但应有阶段进度 |
| 网络连接管理锁 | 不应跨阻塞 I/O;若必须存在,应有明确上限 |
| 全局配置锁 | 应避免长时间持有,更新过程可拆成 copy-then-swap |
| 图形或音频大对象锁 | 优先改为双缓冲、消息传递或 ownership transfer |
不要把所有锁阈值都设置得大于硬件看门狗超时,否则锁监控失去意义。
队列、缓冲区与对象池的进度监控
很多逻辑停滞不是 Mutex 死锁,而是流控闭环失效。例如:
- 生产者等待队列空位,消费者等待另一个资源。
- 消费者仍在运行,但每次取到的都是无效消息。
- 队列持续满,任务不断超时重试。
- 消息被生产但从未完成确认,in-flight 数持续增长。
- 内存池耗尽,释放路径被某个错误状态跳过。
因此 Supervisor 不应只检查线程心跳,还要检查队列和资源的“流动性”。推荐记录:
1 | enqueue_count |
队列健康判定示例
1 | if queue_depth > 0: |
队列水位本身不是故障。队列长期满且出队计数不变,才是高价值故障信号;队列长期空也不一定有问题,除非上游明确存在待生产工作或数据源应持续输出。
数据流监控图
1 | flowchart LR |
状态机与事务推进监控
状态机通常不会彻底停止执行,而是卡在某个等待状态、异常分支或重复重试路径中。仅监控任务是否周期运行无法发现这类问题。
建议每个关键状态机维护:
1 | current_state |
Supervisor 根据状态定义最大驻留时间:
| 状态 | 是否允许长期驻留 | 监控方式 |
|---|---|---|
| IDLE | 是 | 需要证明无待办事务 |
| WAIT_INPUT | 视场景 | 检查输入源是否有 pending 数据 |
| CONNECTING | 否 | 最大连接时限 + 重试预算 |
| WAIT_RESPONSE | 否 | 请求 deadline + 取消机制 |
| COMMITTING | 否 | 阶段进度 + 存储写入时限 |
| ERROR_RETRY | 否 | 最大重试次数 + 退避 + 升级恢复 |
| DEGRADED | 可以 | 必须明确降级能力和退出条件 |
1 | stateDiagram-v2 |
需要避免一个常见错误:在每次状态机函数调用时更新 heartbeat。正确做法是在合法状态转换、事务阶段提交或确认完成时更新 transition_counter 和 progress_epoch。
Supervisor 自身必须避免成为死锁参与者
Supervisor 的健康检查路径应遵循“只读、有限时、无业务依赖”的设计原则。它不应为了检查系统健康而获取正在被检查的业务 Mutex,也不应同步调用可能阻塞的驱动、文件系统、网络或日志接口。
推荐的数据采集方式
- 原子变量:计数器、时间戳、状态枚举和 bitmask 使用原子读写。
- 版本化快照:写侧先更新 sequence 为奇数,写完后变成偶数;读侧检查前后 sequence 一致。
- 双缓冲快照:业务任务更新非活动页,再原子切换索引。
- 单向事件记录:任务向无阻塞 ring buffer 追加事件,Supervisor 只读。
- 调试寄存器或 TCB 只读访问:读取任务状态、栈水位和 PC/LR,不等待业务锁。
1 | Supervisor 不应该做: |
若健康数据本身无法在限定时间内读取,应将“无法获得一致快照”视为异常,而不是继续等待。
Snapshot 一致性示例
1 | /** |
写侧可以采用 sequence-lock 风格协议;读侧最多重试有限次数。若持续读不到一致快照,Supervisor 记录 snapshot_unstable,不得无限自旋。
Supervisor 的优先级与调度设计
Supervisor 不一定要设为系统最高优先级。优先级过高可能掩盖低优先级关键任务长期得不到调度的问题,也可能在系统过载时持续抢占业务任务。
更合理的设计是:
- Supervisor 具有足够优先级,能在规定时间内运行并评估健康。
- 每个 CPU 核保留独立执行探针,检测局部调度停滞。
- 硬件看门狗时钟尽量独立于系统主时钟和调度 tick。
- 监控 Idle Task、调度 tick、关键周期任务和中断延迟,而不是只监控 Supervisor 本身。
- 监督周期要明显小于硬件 WDT timeout,留出诊断和抖动余量。
可检测的调度异常
| 异常 | 典型证据 |
|---|---|
| 高优先级任务忙循环 | Idle counter 不再增加;低优先级任务 exec counter 停止 |
| 中断长期关闭 | Tick/关键 ISR counter 停止;硬件独立 WDT 最终超时 |
| 单核局部卡死 | 该核 per-core counter 停止,其他核仍推进 |
| 调度器异常 | 多个互不相关任务同时停止,但某些 ISR 仍运行 |
| CPU 过载 | 任务仍有进展,但 deadline miss 持续增加 |
| 优先级反转 | 高优先级任务等待低优先级 owner,锁持有时长异常 |
ESP-IDF 的 Task Watchdog 通过监控各 CPU 的 Idle Task 和可订阅任务来发现长时间不让出 CPU 的任务;Zephyr Task Watchdog 则提供多个软件 watchdog channel,并可选用硬件 watchdog 作为 fallback。这些机制说明“一个硬件 WDT 直接对应一个喂狗线程”不足以覆盖多任务系统,应先在软件层聚合多个受监督实体。
软件任务看门狗与硬件看门狗的分层
建议采用至少两层:
1 | flowchart TD |
软件层负责
- 多参与者注册、动态启停和模式切换。
- 每个参与者不同的周期、deadline 和故障阈值。
- 业务进度、锁、队列和资源规则。
- 局部恢复、降级、子系统重启和故障记录。
- 在硬件 WDT 到期前生成诊断快照。
硬件层负责
- Supervisor 未运行。
- 内核或调度器失效。
- 中断被长期关闭。
- 软件层内存损坏或控制流跑飞。
- CPU 核或时钟域异常。
- 软件无法主动完成复位。
外部监控层负责
在高可靠系统中,外部 PMIC 或独立 MCU 可监控主处理器的周期脉冲、挑战应答、通信序号和电源状态。外部监控不应只接收固定 GPIO 翻转,否则主 CPU 的错误循环仍可能继续翻转。更强的做法是 challenge-response:外部监控周期发送变化的 challenge,主系统必须经过受控路径计算并返回正确 response,且返回时序必须落在窗口内。
Window Watchdog 与双阶段超时
普通 watchdog 只检查“是否太晚喂狗”,无法发现错误循环过于频繁地喂狗。Window Watchdog 同时限制最早和最晚喂狗时间:
1 | T_open <= T_feed <= T_close |
若系统在窗口打开前就喂狗,说明喂狗路径可能进入异常忙循环;若超过窗口关闭时间仍未喂狗,则说明系统停止推进或调度异常。
1 | flowchart LR |
Pretimeout / Early Warning
若硬件支持预超时中断、双阶段 watchdog 或 early warning,可在最终复位前执行最小诊断:
- 冻结普通日志写入,避免再次阻塞。
- 保存 reset reason、Supervisor failure bitmap 和最近 health epoch。
- 记录当前任务、每核 PC/LR、栈指针和关键寄存器。
- 保存关键锁 owner、等待者和持有时长。
- 保存队列水位、最老消息年龄和状态机阶段。
- 将固定大小的 crash record 写入 retention RAM、备份寄存器、FRAM 或预留 Flash 区。
- 返回中断并让硬件 watchdog 执行最终复位。
Linux watchdog API 中的 pretimeout 机制就是这一思想:在最终 timeout 之前先触发通知,以便记录 panic、core dump 或其他诊断信息。实现时必须确保 pretimeout handler 不依赖复杂锁、动态内存和可能阻塞的存储路径。
恢复策略:从局部恢复到系统复位
发现异常后不应一律立即硬复位,也不能无限尝试局部恢复。推荐建立有界恢复阶梯:
1 | flowchart TD |
典型恢复级别
| 级别 | 动作 | 适用场景 | 风险 |
|---|---|---|---|
| L0 | 清除单个错误标志、重试一次 | 瞬态错误 | 可能掩盖持续故障 |
| L1 | 取消事务、重置队列或连接 | 单事务卡住 | 需要保证资源回收完整 |
| L2 | 复位外设、总线或协处理器 | I/O 状态机异常 | 可能影响共享用户 |
| L3 | 重启任务组或业务子系统 | 局部服务失效 | RTOS 任务重建必须安全 |
| L4 | 软复位主系统 | 内核仍可控 | 可能无法处理调度器故障 |
| L5 | 停止喂硬件 WDT,执行硬复位 | 全局不可恢复或软件不可信 | 需要持久化现场 |
| L6 | 外部监控断电重启 | 主 SoC 或电源域异常 | 恢复代价最大 |
Erlang/OTP Supervisor 的 one_for_one、one_for_all、rest_for_one 和最大重启强度机制可作为恢复策略设计参考。嵌入式系统同样需要明确:是只重启故障任务、重启一组相互依赖任务,还是重启整个系统;同时要限制单位时间内重启次数,避免设备进入无限 reset loop。
防止复位风暴
Watchdog 设计必须考虑“复位后仍立即触发同一问题”。推荐保存并评估:
1 | reset_reason |
若在短时间内连续多次 watchdog reset,应切换到安全策略:
- 回滚到上一个固件槽。
- 跳过非关键功能初始化。
- 使用最小安全配置。
- 禁止自动执行导致故障的任务。
- 进入诊断模式并等待维护。
- 对外报告稳定的故障码,而不是持续重启。
复位计数应存放在 retention RAM、备份寄存器或具有耐久性设计的持久化区域,不能每次高频擦写同一 Flash 页。
超时参数如何量化
看门狗超时不能凭经验随意设置。至少要区分:
1 | T_supervisor_period Supervisor 评估周期 |
基本约束可表示为:
1 | T_detection <= T_supervisor_period * failure_threshold + T_feed_jitter |
若允许在停止喂狗前保存现场并尝试一次短恢复,则应满足:
1 | T_hw_wdt > T_detection + T_snapshot + T_recovery + T_margin |
但 T_hw_wdt 也不能过大,否则系统失去快速恢复能力。若硬件支持 pretimeout,可拆分为:
1 | T_pretimeout = T_detection + T_recovery_budget |
示例
假设:
1 | Supervisor 周期 100 ms |
则最坏检测时间约为:
1 | T_detection <= 100 ms * 2 + 100 ms 抖动 = 300 ms |
硬件 WDT 可先估算为:
1 | T_hw_wdt > 300 ms + 50 ms + 200 ms + 150 ms = 700 ms |
实际可取硬件支持的 800ms 或 1s 档位,并通过压力测试验证。若 Flash 写入、低功耗唤醒或无线校准存在合法长延迟,应使用运行模式 profile,而不是把全局 WDT 永久放宽到数十秒。
运行模式与动态健康策略
系统在启动、正常运行、升级、工厂测试、深度睡眠和故障恢复阶段,对任务进度的要求不同。不能用一套固定 heartbeat 规则覆盖所有阶段。
1 | stateDiagram-v2 |
各模式监督重点
| 模式 | 必须健康的对象 | 可暂时豁免的对象 | 特殊要求 |
|---|---|---|---|
| Boot | 时钟、内存、启动链、基础 WDT 接管 | 业务任务 | Bootloader 与应用 WDT 无缝交接 |
| Startup | 初始化状态机和关键驱动 | 尚未创建的业务任务 | 每个初始化阶段有单独 deadline |
| Operational | 全部关键业务参与者 | 被明确关闭的可选功能 | 使用严格业务进度检查 |
| Maintenance | 升级、存储、校准任务 | 暂停的普通业务任务 | 需要有界 lease,不得无限延长 |
| LowPower | 低功耗控制器和唤醒源 | 被挂起任务 | 硬件 WDT 需支持暂停或低功耗时钟 |
| Resume | 时钟恢复、外设恢复、任务恢复 | 尚未恢复的可选服务 | 恢复后必须重新建立 baseline |
| Recovery | Supervisor、诊断、恢复执行器 | 已隔离故障模块 | 总恢复时间必须受限 |
有界 Health Lease
长 Flash 擦除、证书生成、无线校准等操作可能超过普通 deadline。不要简单 watchdog_disable() 或无限延长 timeout。可使用有界 lease:
1 | health_lease_begin(participant, operation, max_duration) |
Supervisor 只允许白名单操作申请 lease,并验证:
- 最大持续时间不超过预配置上限。
- 操作期间仍有阶段进度。
- 其他关键任务仍健康。
- lease 不能嵌套失控或反复续期。
- 超时后仍进入正常故障处理。
多核系统的特殊设计
在 SMP 或 AMP 系统中,单个 Supervisor 任务可能只运行在一个核上,而另一个核已失效。若硬件 watchdog 只由健康核喂养,就会掩盖局部核故障。
推荐方案
- 每个核都有独立
per_core_epoch,由该核上的定时器、Idle Task 或调度钩子更新。 - 关键任务记录实际运行核和最近执行时间。
- Supervisor 评估所有核的 epoch,而不是只检查自身。
- 如果 SoC 支持每核 watchdog、NMI 或 cross-core interrupt,应组合使用。
- 对 AMP 子系统使用 mailbox sequence、challenge-response 或共享内存 generation counter。
- 某核失效后先尝试局部核复位;若共享资源一致性无法保证,则升级为 SoC 复位。
1 | flowchart TB |
需要注意 cache coherency 和内存屏障。跨核健康计数器应使用原子操作或明确的共享内存同步,不能依赖普通 volatile 推断可见性。
外设、总线和外部依赖监控
业务任务可能没有死锁,而是等待永远不完成的 I/O。每个外部请求都应具备:
1 | request_id |
I/O 健康规则
- 任何同步 I/O 都必须有有限 timeout。
- 异步请求必须记录 in-flight 年龄。
- 丢失中断时可由 timeout 路径轮询状态并复位外设。
- 总线复位不能在持有全局业务锁时执行。
- 共享总线恢复必须通知所有客户端重新同步。
- 外部云服务不可达通常应触发降级或重连,不应直接导致整机 watchdog reset。
- 对于本地关键执行器、传感器或安全协处理器,长期不可用可升级为系统复位或安全态。
1 | sequenceDiagram |
复位前留痕设计
如果 Watchdog 最终一定会复位,复位前最有价值的工作不是打印大量日志,而是保存一条固定格式、可校验、可在下次启动读取的故障记录。
建议保存字段
1 | magic / version / length / crc |
存储优先级
- Retention RAM 或备份 SRAM:速度快、对软件路径依赖少。
- RTC Backup Register:容量小,但适合保存故障码和计数。
- FRAM/MRAM:适合频繁记录,但取决于硬件。
- 预擦除 Flash 记录槽:必须避免在 pretimeout 时做擦除。
- 外部安全 MCU:可独立记录主处理器心跳丢失原因。
不应在最后阶段做的事
- 动态分配大块内存。
- 获取可能被故障任务持有的锁。
- 执行文件系统 mount、sync 或复杂事务。
- 通过已经异常的网络发送长报告。
- 在看门狗中断里打印海量日志。
- 无限等待 DMA、Flash 或 UART 发送完成。
故障记录必须是 best-effort,并且不能反过来阻止最终复位。
参考开源组件与机制
下面列出的组件不应被机械拼装成同一套软件。它们分别提供了任务级 watchdog、硬件 WDT 接口、锁依赖检测、CPU lockup 检测、服务级健康探针和监督树等设计参考。
| 组件或机制 | 直接用途 | 可借鉴的核心原理 | 局限 |
|---|---|---|---|
| Zephyr Task Watchdog | 多线程软件 watchdog channel,可选硬件 fallback | 每个任务独立通道、统一超时管理、软件层聚合后交给硬件层 | 默认仍偏执行存活,需要业务 progress 扩展 |
| Zephyr Hardware Watchdog API | 驱动硬件 WDT、窗口和回调 | timeout window、硬件独立复位、统一驱动接口 | 不理解业务语义 |
| ESP-IDF TWDT / IWDT | 监控任务、Idle Task 和中断长时间阻塞 | 任务订阅、每核 Idle 探针、任务与中断 watchdog 分层 | 主要检测不让出 CPU,不等同业务完成 |
| RT-Thread Watchdog Device | 统一硬件 watchdog 设备接口 | 设备抽象、timeout 设置、keepalive 和 start/stop | 文档中的 Idle Hook 喂狗示例只能证明系统有空闲时间,不能证明业务健康 |
Linux /dev/watchdog |
用户态 daemon 驱动硬件 watchdog | 用户态健康检查通过后才 ping;nowayout;pretimeout | 通用 API 不替应用定义健康条件 |
| Linux lockdep | 开发期锁依赖验证 | 锁类别、获取顺序、依赖图、循环检测 | 主要用于验证,不负责产品恢复 |
| Linux soft/hard lockup detector | 检测内核任务不调度或中断不响应 | 分开监控调度活性和中断活性 | 面向 Linux 内核,不覆盖业务状态机 |
| FreeRTOS Event Groups / Task Notifications | 任务向 Supervisor 提交轻量事件和 bit | 低开销事件聚合、从 ISR 延迟提交、任务通知 | bit 置位必须防止旧值重复和无条件置位 |
| Kubernetes liveness/readiness/startup probes | 服务健康与重启控制 | 区分“能活”“能服务”“启动完成”,失败阈值与周期分离 | 面向容器,但健康语义可迁移到嵌入式模式管理 |
| Erlang/OTP Supervisor | 监督进程与分层重启 | one-for-one、one-for-all、rest-for-one、重启强度限制 | 进程隔离能力强于多数 MCU RTOS,需要做适配 |
Zephyr Task Watchdog
Zephyr 的 Task Watchdog 为多个任务提供独立 channel。每个 channel 配置 reload period,任务通过 task_wdt_feed(channel_id) 更新自身通道;Task WDT 还可以把硬件 watchdog 作为 fallback。可借鉴点:
- 软件层维护多个参与者,而不是所有任务直接碰硬件寄存器。
- 每个参与者有独立 timeout。
- 软件监督器失效时,硬件 WDT 仍可执行最终复位。
- 通道数量在配置时明确限制,适合资源受限系统。
要覆盖业务假活,应在 channel feed 前加入本题所述的 progress、lock、queue 和 deadline 判断,而不是在任务循环中无条件调用 feed。
ESP-IDF Task Watchdog 与 Interrupt Watchdog
ESP-IDF 将 Task Watchdog 和 Interrupt Watchdog 分开:Task WDT 主要检测任务长期不让出 CPU,可订阅 Idle Task、普通 task 或 user;Interrupt WDT 关注 ISR 或调度 tick 长时间被阻塞。可借鉴点:
- 调度活性和中断活性是不同故障域,应分别监控。
- 多核系统需要检查各 CPU 的 Idle Task。
- 可订阅“user”而不仅是任务,便于监控周期函数或功能模块。
- timeout 后先输出 backtrace,再决定 panic 或复位。
但即便任务持续 yield,也可能出现业务状态不推进,因此还要在上层增加 Health Contract。
RT-Thread Watchdog Device
RT-Thread 提供统一 Watchdog Device 接口,包括设置 timeout、查询剩余时间、keepalive、start 和 stop。文档示例常在 Idle Hook 中喂狗,这能检测“系统完全没有进入空闲任务”的部分 CPU 饥饿问题,但不能覆盖以下情况:
- 死锁任务都处于阻塞态,Idle Task 仍大量运行。
- 关键业务任务退出或永远等待,系统仍有空闲时间。
- 状态机活锁但持续让出 CPU。
- 某个非关键任务正常,关键任务失效。
因此,在 RT-Thread 项目中应保留设备驱动层,但把 RT_DEVICE_CTRL_WDT_KEEPALIVE 的调用移动到统一 Supervisor,并在调用前完成业务健康聚合。
Linux Watchdog API
Linux /dev/watchdog 模型通常由用户态 daemon 定期 ping 硬件 WDT。官方文档明确给出一个更高级的思路:daemon 可以先检查 HTTP 服务或其他条件,确认系统正常后才写 watchdog。可借鉴点:
- 喂狗 daemon 是策略执行者,而不是只负责 sleep + keepalive。
nowayout防止 daemon 异常退出时意外关闭 watchdog。- pretimeout 在最终复位前提供现场保存机会。
- reset reason 和 boot status 应在启动后读取。
Linux lockdep 与 lockup detectors
lockdep 通过锁获取顺序构建依赖关系,可检测复杂循环锁依赖。softlockup detector 关注内核线程长时间不被调度,hardlockup detector 关注 CPU 长时间不响应中断。两者共同说明:
- 死锁预防需要依赖图或锁顺序验证。
- 调度卡死与中断卡死需要不同监控源。
- 检测后应保存当前栈回溯和锁状态。
- 单一 heartbeat 不能覆盖所有 lockup 类型。
FreeRTOS Event Groups 与 Task Notifications
FreeRTOS Event Groups 可以聚合多个任务的事件 bit,Task Notifications 可作为更轻量的 event flag 或计数器。它们适合构建第一版健康上报通道,但需注意:
- 不要让任务在循环入口无条件设置健康 bit。
- Supervisor 每轮必须清除或比较 generation,防止旧 bit 重用。
- ISR 上报应只提交轻量事件,不做复杂健康判断。
- 如果任务数量超过 event bits,应使用数组、位图分组或计数器表。
- Event Group 只负责通信,不负责定义“何时算业务健康”。
Kubernetes 健康探针的抽象价值
Kubernetes 区分 startup、readiness 和 liveness:
- Startup:应用是否完成启动,在此之前不应用普通 liveness 规则误杀慢启动服务。
- Readiness:当前能否接受业务;失败时可以摘除流量,但不一定立即重启。
- Liveness:是否进入只能通过重启恢复的失效状态。
嵌入式系统可以采用同样分层:
1 | startup_health 初始化是否按阶段完成 |
例如网络暂时断开可能使 service_ready=false,但不应立即触发整机复位;控制回路 deadlock 则可能直接使 system_live=false。
Erlang/OTP Supervisor 的抽象价值
OTP Supervisor 把故障恢复组织成监督树,并提供不同重启策略和最大重启强度。嵌入式项目可对应设计:
1 | one_for_one -> 只复位单个驱动或 Worker |
对于没有进程隔离的 MCU RTOS,直接删除和重建任务可能遗留锁、内存和驱动状态,因此任务级重启必须经过严格设计。若无法证明局部重启安全,应直接重启整个子系统或整机。
一个可落地的通用接口设计
下面是一个与具体 RTOS 解耦的简化接口。实际项目可把原子操作、时间函数、任务标识和锁监控接入 FreeRTOS、RT-Thread、Zephyr 或自研内核。
1 | /** |
参与者上报示例
1 | for (;;) |
关键点是:health_report_execution() 与 health_report_progress() 分开。前者证明任务被调度,后者只有在业务完成点更新。
Supervisor 主循环伪代码
1 | for (;;) |
这里的 health_try_bounded_recovery() 必须有严格的时间预算。进入停止喂狗状态后,不应因为普通任务随后又更新了 heartbeat 就重新喂狗,除非系统明确完成一次受控恢复并通过健康观察窗。
Feed Epoch 与一致性协议
在多任务系统中,简单的 bitmask 方案容易出现竞态。例如 Supervisor 清 bit 的同时,任务又置 bit,可能丢失一轮心跳;或者任务启动时置位一次,Supervisor 未正确清除,导致后续一直健康。
推荐使用 epoch:
1 | supervisor_epoch = N |
一种协议是:Supervisor 每轮递增全局 epoch;参与者在完成健康点后把本地 seen_epoch 更新为当前 epoch。Supervisor 只接受本轮或允许窗口内的 epoch。
更简单且更健壮的方案是直接比较单调 counter:
1 | if current_exec_counter != previous_exec_counter: |
对于低频任务,不要求每个 Supervisor 周期都变化,而是用最后变化时间与任务 contract 比较。
任务退出、挂起和动态创建
监督系统必须处理任务生命周期,否则合法停用会被误判为故障,异常退出又可能被忽略。
生命周期状态
1 | UNREGISTERED |
规则建议:
- 任务创建后进入 STARTING,并获得有限 startup grace。
- 完成初始化后必须显式进入 ACTIVE。
- 只有 Supervisor 或系统模式管理器可批准
SUSPENDED_BY_POLICY。 - 业务任务不能自行永久关闭自身监督。
- 任务退出钩子要将状态改为 STOPPED 或 FAILED。
- 关键任务异常退出立即阻止喂狗或触发恢复。
- 动态任务的 health handle 必须有 generation,防止旧 handle 指向新任务。
时间基准的可靠性
所有 deadline 判断都依赖时间。如果系统 tick 停止或时钟被错误重配,Supervisor 可能永远认为没有超时。因此应至少有两个层次的时间证据:
- RTOS monotonic tick:用于普通软件 deadline。
- 独立硬件 watchdog clock、RTC、低速独立振荡器或外部监控时钟:用于最终超时。
可选地交叉检查:
1 | delta_rtos_tick |
若两者长期偏差超过阈值,应报告 timebase fault。进入低功耗前必须明确:硬件 WDT 是否暂停、是否继续计时、唤醒后剩余窗口是多少,以及软件基准是否发生跳变。
内存与栈健康
逻辑停滞也可能由内存耗尽、栈接近溢出或内存破坏引发。可把以下指标纳入健康快照:
1 | heap_free |
但阈值应避免过度敏感:堆空间低不等于立即复位;若关键分配已失败、恢复路径也无法分配,才可能升级为系统故障。更推荐对关键路径使用静态内存、专用内存池或预分配恢复资源,使 Supervisor 和 crash recorder 不依赖普通堆。
安全性与故障注入考虑
Watchdog 通道本身也可能被错误代码或恶意输入滥用:
- 任意模块都能直接写硬件 WDT 寄存器。
- 任务可以伪造其他参与者的 heartbeat。
- 越界写破坏 health table。
- 故障代码反复申请长 lease。
- 诊断接口可远程关闭 watchdog。
建议:
- 只允许 Supervisor 所在特权域访问硬件 WDT。
- Health API 校验 handle generation 和调用者身份。
- Watchdog 启动后使用 hardware lock 或 nowayout 类配置,禁止普通软件关闭。
- Health table 放在受 MPU/MMU 保护区域,业务任务只能写自身槽位。
- Lease 类型、最大时长和次数由静态策略控制。
- 远程维护命令只能切换到预定义 maintenance profile,不能无期限停用 watchdog。
- 故障记录带 CRC、版本和单调序号,防止读取损坏数据后误诊。
测试与故障注入
只有能被主动触发并验证的 watchdog 方案才可信。测试不能只确认“正常运行时不会误复位”,还要确认每类异常都能在规定时间内被检测、留痕和恢复。
故障注入矩阵
| 注入故障 | 预期观测 | 预期动作 |
|---|---|---|
| 任务永久阻塞 | exec counter 停止 | 超过 deadline 后阻止喂狗 |
| 任务错误循环且持续 heartbeat | exec 变化、progress 不变 | 业务 deadline 到期后复位或恢复 |
| 两锁反向获取 | lock hold 超时、等待环 | 保存 owner/waiter,停止喂狗 |
| 高优先级忙循环 | Idle counter 与低优先级任务停滞 | Task/CPU watchdog 触发 |
| 长时间关闭中断 | Tick/ISR 停止 | 硬件或 interrupt watchdog 触发 |
| 队列永久满 | depth 满、dequeue 不变、oldest age 增长 | 隔离生产者,恢复失败后复位 |
| 外设不产生完成中断 | in-flight age 超时 | 取消事务并复位外设 |
| 单核停滞 | 对应 per-core epoch 不变 | 抓取跨核现场并复位 |
| Supervisor 自身挂起 | 无后续硬件 feed | 硬件 WDT 直接复位 |
| pretimeout 保存路径故障 | crash record 不完整 | 最终复位仍必须发生 |
| 连续启动失败 | reset counter 增长 | 进入安全模式或回滚槽位 |
| 合法长 Flash 操作 | lease 有阶段进度 | 不误复位,超 lease 则失败 |
必测指标
1 | fault_detection_latency |
压力测试组合
- CPU 100% 负载 + 高频中断 + Flash 写入。
- 低内存 + 队列拥塞 + 多任务优先级竞争。
- Tickless idle + 频繁睡眠唤醒。
- 双核同时执行 cache-heavy 工作负载。
- 日志后端阻塞或 UART 拔除。
- 文件系统损坏、网络断开、外设 NACK 和 DMA 丢中断。
- 在 Supervisor 评估期间随机改变任务状态,验证快照一致性。
常见错误设计
错误一:在 Idle Hook 无条件喂狗
Idle Hook 只能证明系统存在空闲 CPU 时间。若关键任务都死锁并进入阻塞态,Idle Task 反而会运行得更多,因此看门狗永远不会超时。
错误二:由最高优先级 Watchdog Task 无条件喂狗
它只能证明该任务未被阻塞。高优先级本身还可能掩盖低优先级关键任务的饥饿。
错误三:每个任务独立直接喂硬件 WDT
只要任何一个任务成功喂狗,其他任务失效就可能被掩盖;同时无法统一评估业务依赖和故障优先级。
错误四:每个任务只置一个布尔 heartbeat bit
旧 bit 可能被重复使用,任务也可能在错误循环中持续置位。应使用 generation、counter、deadline 和业务 progress。
错误五:Supervisor 为检查状态而获取业务锁
Supervisor 可能加入死锁环,导致无法保存现场。应使用原子元数据或无锁快照。
错误六:发现异常后无限尝试恢复
恢复循环自身可能成为活锁并持续喂狗。必须限制恢复次数和总时间。
错误七:为避免误复位把 WDT timeout 调得极大
这只会延迟故障恢复。应使用模式化 deadline、startup grace 和有界 lease,而不是永久放宽全局超时。
错误八:复位前执行复杂日志和文件系统操作
故障时这些子系统可能正是被卡住的部分。应写固定大小、无阻塞、预分配的 crash record。
错误九:把外部服务不可达等同于整机不健康
云端断网、服务器维护或网络弱信号通常应降级、重连或标记 not ready,而不是直接整机复位。Watchdog 应关注本地系统是否仍能正确执行恢复策略。
错误十:只做运行期复位,不做开发期锁验证
Watchdog 能恢复服务,但不能消除缺陷。应结合固定锁顺序、超时 API、静态分析、运行时依赖检查和故障注入,从源头降低死锁概率。
推荐实施步骤
第一阶段:建立最小闭环
- 选出真正影响设备核心功能的 3~8 个 critical participant。
- 禁止普通任务直接喂硬件 WDT。
- 建立唯一 Supervisor 和固定周期评估。
- 为每个参与者分别记录 execution counter 与 progress counter。
- 配置 startup、operational 两套 profile。
- 失败时保存最小 failure bitmap 和 reset reason。
第二阶段:补齐资源与流控监控
- 包装关键 Mutex,记录 owner 和 hold time。
- 监控核心队列的 depth、oldest item age 和 complete counter。
- 所有 I/O 请求增加 deadline 和取消/复位路径。
- 加入 per-core epoch、Idle counter 和关键 ISR counter。
- 使用 pretimeout 或 early warning 保存寄存器和任务快照。
第三阶段:加入恢复策略与工程化验证
- 定义 L0~L5 恢复阶梯和升级条件。
- 引入 restart budget,防止恢复风暴。
- 实现 reset-loop 检测、安全模式和固件回滚。
- 建立系统级 fault injection 测试。
- 将检测时间、误复位率、快照成功率纳入发布指标。
- 在 Debug 构建中启用更强的锁依赖和断言检查。
最终回答组织方式
面试或设计评审中,可以按以下顺序回答:
- 先指出根因:独立喂狗任务只能证明自己被调度,不能证明系统业务健康。
- 提出分布式健康监督:关键任务提交执行、进度、deadline、锁和队列证据,由 Supervisor 统一判定。
- 说明喂狗门控:只有所有 critical health contract 满足,Supervisor 才喂硬件 WDT。
- 说明死锁覆盖:记录 Mutex owner、持有时间、等待者和锁顺序;Supervisor 无锁读取,超时后禁止喂狗。
- 扩展到通用故障:活锁、饥饿、队列堵塞、状态机停滞、I/O 悬挂、单核失效和时间基准故障。
- 说明分层 watchdog:软件 Task WDT 聚合多参与者,硬件 WDT 兜底调度器和软件层失效,必要时外部监控再兜底。
- 说明恢复与留痕:先有界局部恢复,再子系统重启,最终停止喂狗;pretimeout 保存任务、锁、队列、PC/LR 和最近事件。
- 说明参数量化:根据监督周期、业务 deadline、快照时间、恢复预算和抖动反推硬件 timeout。
- 说明工程落地:参考 Zephyr Task WDT、ESP-IDF TWDT/IWDT、Linux watchdog/lockdep、RT-Thread WDT、OTP Supervisor 等机制,并通过故障注入验证。
一句话概括:看门狗不应监督“某个喂狗线程是否活着”,而应监督“系统是否仍在完成它必须完成的工作”;硬件喂狗只是所有关键健康证据通过后的最终动作。
参考链接
- Zephyr Task Watchdog
- Zephyr Task Watchdog APIs
- ESP-IDF Watchdogs
- RT-Thread WATCHDOG Device
- Linux Watchdog driver API
- Linux Runtime locking correctness validator / lockdep
- Linux Softlockup and Hardlockup Detectors
- FreeRTOS Event Groups
- FreeRTOS Task Notifications
- Kubernetes Liveness, Readiness and Startup Probes
- Erlang/OTP Supervisor Behaviour










