嵌入式面试真题第 14 题:防范业务逻辑死锁的系统健康监督与看门狗设计
在这里插入图片描述问题
在多任务嵌入式系统、RTOS 固件、边缘网关、工业控制器、车载控制单元、消费电子设备或其他长期无人值守的软件系统中,通常会配置硬件看门狗,以便系统发生严重异常时自动复位。
传统实现往往由一个独立 Watchdog Task、Idle Hook、主循环或高优先级定时任务周期性喂狗。只要这个喂狗节点还能获得 CPU 时间,硬件看门狗就不会超时。然而,系统可能已经出现以下“进程还活着、业务却不再工作”的假活状态:
- • 高优先级任务持续运行,低优先级关键任务长期饥饿。
- • 任务不断重试或相互唤醒,但业务状态不再前进,形成活锁。
- • 消息队列、缓冲区或对象池耗尽,生产者和消费者互相等待。
- • 状态机卡在某个中间状态,周期函数仍在调用,但事务永远无法完成。
- • 外设、总线、文件系统、网络栈或协处理器请求悬挂,任务仍在等待或轮询。
- • 某个 CPU 核、调度 tick、关键中断或时间基准失效,而其他核仍能继续喂狗。
- • 看门狗任务本身优先级过高、依赖过少,反而绕过了真正的业务健康状态。
面对这种问题,如何把看门狗从“某个线程周期运行就喂狗”的简单定时器,重新设计成一套通用、可量化、可诊断、可扩展的系统健康监督机制?如何定义喂狗条件、采集健康证据、监控锁和队列、处理启动与低功耗阶段、保存复位现场,并参考已有开源组件构建可落地方案?
回答
结论:硬件看门狗只能作为最终执行器,不能直接代表系统健康。正确方案是在业务任务与硬件看门狗之间增加一个独立的 Health Supervisor,由所有关键执行单元提交可验证的健康证据;只有当任务执行、业务进度、时限约束、资源锁、队列流动、调度能力、时间基准和关键依赖均满足当前运行模式的健康契约时,Supervisor 才允许喂硬件看门狗。
喂狗条件必须从:
“看门狗任务本轮获得了 CPU”
升级为:
“系统在规定时间窗内完成了所有必要工作,并且没有出现不可接受的停滞、资源占用或依赖失效”
这套设计的核心不是简单增加若干 heartbeat bit,而是建立四层监督链路:
- 1. 执行层健康:关键任务、关键中断、每个 CPU 核和调度器是否仍有执行机会。
- 2. 业务层健康:事务、状态机、队列和数据流是否持续产生可观察的进展。
- 3. 资源层健康:Mutex、Semaphore、队列、内存池、总线和外设是否在限定时间内被释放或完成。
- 4. 恢复层健康:异常能否先局部恢复、再子系统重启、最终由硬件看门狗复位,并在复位前留下足够诊断证据。
总体架构
业务任务不直接操作硬件 Watchdog 寄存器,也不应该各自独立喂同一个硬件看门狗。它们只负责提交自身职责范围内的健康证据。Health Supervisor 读取这些证据、执行规则评估,并作为唯一喂狗授权点。
硬件看门狗应被视为无法依赖常规软件路径时的最终恢复手段。软件 Supervisor、日志系统、任务调度器甚至整个内核都可能失效,因此监督链应尽量具备分层和独立性:软件任务看门狗负责多任务聚合,硬件看门狗负责调度器或系统级失效,必要时再由独立 PMIC、外部 MCU 或安全岛监控主处理器。
问题应如何抽象
这道题表面上是“两个任务互相等待 Mutex,而看门狗任务仍在喂狗”,但更通用的抽象是:
系统存在至少一个仍可执行的路径,因此传统 watchdog heartbeat 持续更新;与此同时,决定系统服务能力的关键不变量已经失效,或者关键状态长时间没有推进。
这类故障可统一称为 业务假活、逻辑停滞或 liveness failure。它不同于直接崩溃:程序计数器仍在变化,部分线程仍在调度,甚至日志仍有输出,但系统已无法完成其主要职责。
常见故障类型
与依赖故障时间与平台故障Mutex循环等待Semaphore永久等待条件变量丢失唤醒同步RPC相互调用高优先级忙循环低优先级关键任务饥饿优先级反转某CPU核局部失效队列永久满队列永久空Buffer不再推进对象池耗尽状态不转移事务无完成重试风暴活锁总线事务悬挂外设中断丢失文件系统或网络假死协处理器无响应Tick停止中断长期关闭时钟源跳变低功耗恢复异常
因此,单纯要求“每个任务每秒置位一次”仍然不够。一个任务完全可以在错误循环里持续置位;一个状态机可以反复进入同一状态;一个消息处理线程可以一直被调度,却没有成功消费任何有效消息。健康证据必须能区分以下三件事:
| | |
| 任务是否获得过 CPU、ISR 是否发生、调度 tick 是否工作 | |
| | |
| | |
健康模型:从 Heartbeat 到 Health Contract
推荐把每个关键参与者定义成一个受监督实体 health participant。参与者不是必须等同于线程,它可以是:
- • 一个共享服务,如文件系统、存储服务、网络管理器或内存池。
每个参与者注册一份 Health Contract,至少包含:
participant_id 参与者唯一标识
criticality 关键等级:critical / important / optional
period 预期执行周期或检查周期
progress_deadline 最长允许无业务进展时间
startup_grace 启动阶段宽限时间
failure_threshold 连续多少次异常才判定失败
recovery_policy 局部恢复、子系统重启或系统复位
required_in_modes 哪些运行模式下必须健康
运行过程中,参与者提交不同类型的证据:
execution_heartbeat 本任务或回调确实执行过
progress_epoch 完成了一个有意义的业务阶段
deadline_complete 指定周期或事务在期限内完成
resource_snapshot 当前锁、队列、内存和 I/O 状态
error_state 可恢复错误、永久错误或降级状态
为什么必须区分执行心跳与进度心跳
假设音频任务每 10ms 醒来一次,并执行以下错误循环:
while (true) {
retry_same_failed_transaction();
heartbeat++;
}
heartbeat持续变化,只能证明线程获得了 CPU,不能证明音频帧被成功处理。正确做法是同时维护:
exec_counter++ 每次调度周期都更新
frame_commit_counter++ 只有一帧真正提交到下游后才更新
last_good_frame_time 记录最后一次成功输出时间
Supervisor 可以据此区分:
喂狗门控:所有关键健康条件的合取
硬件喂狗不应由任意一个参与者直接触发,而应由 Feed Gate 统一决策。最简单的逻辑是关键条件的 AND:
feed_allowed =
all_required_participants_alive
&& all_required_progress_deadlines_met
&& no_fatal_lock_timeout
&& scheduler_and_timebase_healthy
&& no_unrecoverable_resource_stall
&& supervisor_snapshot_consistent
&& current_mode_policy_satisfied
实际系统需要允许可选功能、降级运行和启动阶段宽限,因此不一定是机械的全局 AND。更合理的是按关键度和运行模式计算:
critical participant failed -> 立即阻止喂狗或进入短暂诊断阶段
important participant failed -> 先局部恢复,恢复失败后阻止喂狗
optional participant failed -> 隔离或降级,不一定系统复位
喂狗决策流程
Feed Gate 还应避免“旧证据重复使用”。每次喂狗必须消费一个新的监督周期或新的 health epoch,防止某个参与者早先提交过一次健康标志后永久有效。
推荐使用单调计数器而不是单纯布尔位:
last_seen_exec_counter[i]
last_seen_progress_counter[i]
current_exec_counter[i]
current_progress_counter[i]
Supervisor 每轮比较新旧值,再更新时间戳。这样可以避免任务只在启动时置一次 bit,随后一直被误判为健康。
关键任务应提交哪些健康证据
不同任务不能用完全相同的 heartbeat 语义。健康点必须放在“任务确实完成核心职责”的位置,而不是任务循环入口、无条件定时回调或错误处理循环中。
业务进度指标的设计原则
- 1. 必须对应可验证完成点:在事务提交、帧输出、状态迁移或请求完成后更新。
- 2. 必须单调或可比较:优先使用递增计数、序号、epoch 或最后完成时间。
- 3. 不能依赖正在被监控的锁:否则 Supervisor 为读取健康状态也可能被同一死锁阻塞。
- 4. 不能由无关任务代写:健康证据应能定位责任实体。
- 5. 不能无限宽限:长期等待外部依赖也必须有业务级超时、降级或隔离策略。
- 6. 要允许合法空闲:任务没有输入时,不应强迫 progress counter 变化,而应证明“空闲是被允许的”。
合法空闲可以通过以下条件表达:
input_queue_empty
&& no_inflight_transaction
&& upstream_marked_idle
&& task_execution_heartbeat_recent
只有在没有待办工作时,缺少业务进度才可视为正常;一旦存在 pending work,就必须在 deadline 内看到完成数、消费指针或状态 epoch 推进。
锁超时与死锁监控
题目中的 Mutex 循环等待应由两类机制共同覆盖:
- 1. 开发期预防与检测:固定锁顺序、静态分析、运行时 lock dependency 检查、超时断言和故障注入。
- 2. 产品运行期监督与恢复:记录锁 owner、持有时长、等待者和调用点,超过阈值后禁止喂狗并保存现场。
锁元数据
建议为关键锁增加轻量包装层,至少记录:
lock_id
owner_task_id
acquire_timestamp
owner_pc_or_callsite
waiter_bitmap_or_wait_count
max_hold_time
acquire_count
contention_count
timeout_count
锁获取和释放路径更新原子元数据:
Wait-for Graph
若资源允许,可构造任务与锁的等待图:
图中出现有向环,意味着存在循环等待条件。小型 MCU 不一定要实时运行完整图算法,但可以采用以下折中:
- • 为锁定义全局层级
lock_rank,运行时断言只能按 rank 递增获取。 - • 记录每个任务“当前持有锁集合”和“正在等待锁”。
- • 仅在 Supervisor 发现超时后离线构造等待链。
- • 在 Debug/Fault-Injection 构建中启用完整依赖图,在 Release 构建中保留轻量持有时长监控。
Linux 的 lockdep 是典型参考:它记录锁类别之间的获取顺序,并检测可能形成循环依赖的锁顺序。该机制主要用于开发和验证,不能替代产品中的恢复策略,但其“把锁获取顺序表示成依赖图”的思想非常适合嵌入式关键锁审计。
锁超时不是简单地统一设为一个值
锁的合理阈值应根据临界区最坏执行时间、可被抢占时间和平台抖动确定:
T_lock_limit >= T_critical_section_wc
+ T_preemption_wc
+ T_interrupt_jitter
+ T_margin
不同锁应有不同阈值:
| |
| |
| |
| |
| 应避免长时间持有,更新过程可拆成 copy-then-swap |
| 优先改为双缓冲、消息传递或 ownership transfer |
不要把所有锁阈值都设置得大于硬件看门狗超时,否则锁监控失去意义。
队列、缓冲区与对象池的进度监控
很多逻辑停滞不是 Mutex 死锁,而是流控闭环失效。例如:
- • 消息被生产但从未完成确认,in-flight 数持续增长。
因此 Supervisor 不应只检查线程心跳,还要检查队列和资源的“流动性”。推荐记录:
enqueue_count
dequeue_count
complete_count
drop_count
queue_depth
queue_high_watermark
oldest_item_age
inflight_count
pool_free_count
allocation_fail_count
队列健康判定示例
if queue_depth > 0:
require dequeue_count changes within T_consume
require oldest_item_age < T_item_deadline
if queue_depth == capacity:
require producer_backpressure is active
require depth leaves full state within T_full_limit
if inflight_count > 0:
require complete_count changes within T_complete
队列水位本身不是故障。队列长期满且出队计数不变,才是高价值故障信号;队列长期空也不一定有问题,除非上游明确存在待生产工作或数据源应持续输出。
数据流监控图
状态机与事务推进监控
状态机通常不会彻底停止执行,而是卡在某个等待状态、异常分支或重复重试路径中。仅监控任务是否周期运行无法发现这类问题。
建议每个关键状态机维护:
current_state
state_enter_timestamp
transition_counter
last_successful_transition
transaction_id
transaction_stage
retry_count
last_error
Supervisor 根据状态定义最大驻留时间:
需要避免一个常见错误:在每次状态机函数调用时更新 heartbeat。正确做法是在合法状态转换、事务阶段提交或确认完成时更新 transition_counter和 progress_epoch。
Supervisor 自身必须避免成为死锁参与者
Supervisor 的健康检查路径应遵循“只读、有限时、无业务依赖”的设计原则。它不应为了检查系统健康而获取正在被检查的业务 Mutex,也不应同步调用可能阻塞的驱动、文件系统、网络或日志接口。
推荐的数据采集方式
- 1. 原子变量:计数器、时间戳、状态枚举和 bitmask 使用原子读写。
- 2. 版本化快照:写侧先更新 sequence 为奇数,写完后变成偶数;读侧检查前后 sequence 一致。
- 3. 双缓冲快照:业务任务更新非活动页,再原子切换索引。
- 4. 单向事件记录:任务向无阻塞 ring buffer 追加事件,Supervisor 只读。
- 5. 调试寄存器或 TCB 只读访问:读取任务状态、栈水位和 PC/LR,不等待业务锁。
Supervisor 不应该做:
- take(business_mutex, WAIT_FOREVER)
- sync_read(file_system)
- blocking_send(log_queue)
- allocate_from_shared_heap_without_limit
- call_into_untrusted_driver()
若健康数据本身无法在限定时间内读取,应将“无法获得一致快照”视为异常,而不是继续等待。
Snapshot 一致性示例
/**
* @brief Lightweight health snapshot published by one participant.
*/
typedefstruct
{
atomic_uint_fast32_t seq;
atomic_uint_fast32_t exec_counter;
atomic_uint_fast32_t progress_counter;
atomic_uint_fast32_t state;
atomic_uint_fast64_t last_progress_us;
atomic_uint_fast32_t flags;
} health_snapshot_t;
写侧可以采用 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 停止,其他核仍推进 |
| |
| 任务仍有进展,但 deadline miss 持续增加 |
| 高优先级任务等待低优先级 owner,锁持有时长异常 |
ESP-IDF 的 Task Watchdog 通过监控各 CPU 的 Idle Task 和可订阅任务来发现长时间不让出 CPU 的任务;Zephyr Task Watchdog 则提供多个软件 watchdog channel,并可选用硬件 watchdog 作为 fallback。这些机制说明“一个硬件 WDT 直接对应一个喂狗线程”不足以覆盖多任务系统,应先在软件层聚合多个受监督实体。
软件任务看门狗与硬件看门狗的分层
建议采用至少两层:
软件层负责
- • 每个参与者不同的周期、deadline 和故障阈值。
硬件层负责
外部监控层负责
在高可靠系统中,外部 PMIC 或独立 MCU 可监控主处理器的周期脉冲、挑战应答、通信序号和电源状态。外部监控不应只接收固定 GPIO 翻转,否则主 CPU 的错误循环仍可能继续翻转。更强的做法是 challenge-response:外部监控周期发送变化的 challenge,主系统必须经过受控路径计算并返回正确 response,且返回时序必须落在窗口内。
Window Watchdog 与双阶段超时
普通 watchdog 只检查“是否太晚喂狗”,无法发现错误循环过于频繁地喂狗。Window Watchdog 同时限制最早和最晚喂狗时间:
T_open <= T_feed <= T_close
若系统在窗口打开前就喂狗,说明喂狗路径可能进入异常忙循环;若超过窗口关闭时间仍未喂狗,则说明系统停止推进或调度异常。
flowchart LR A[上次喂狗] --> B[禁止喂狗窗口] B --> C[允许喂狗窗口] C --> D[超时复位点] B -.过早喂狗.-> E[异常 / 复位] C -.正常喂狗.-> A D --> F[硬件复位]
Pretimeout / Early Warning
若硬件支持预超时中断、双阶段 watchdog 或 early warning,可在最终复位前执行最小诊断:
- 2. 保存 reset reason、Supervisor failure bitmap 和最近 health epoch。
- 3. 记录当前任务、每核 PC/LR、栈指针和关键寄存器。
- 6. 将固定大小的 crash record 写入 retention RAM、备份寄存器、FRAM 或预留 Flash 区。
- 7. 返回中断并让硬件 watchdog 执行最终复位。
Linux watchdog API 中的 pretimeout 机制就是这一思想:在最终 timeout 之前先触发通知,以便记录 panic、core dump 或其他诊断信息。实现时必须确保 pretimeout handler 不依赖复杂锁、动态内存和可能阻塞的存储路径。
恢复策略:从局部恢复到系统复位
发现异常后不应一律立即硬复位,也不能无限尝试局部恢复。推荐建立有界恢复阶梯:
flowchart TD A[检测到健康异常] --> B[隔离新请求 / 标记 Not Ready] B --> C[取消或超时当前事务] C --> D[复位单个外设 / 通信链路] D --> E[重启单个 Worker 或子系统] E --> F[重建依赖子树] F --> G{恢复成功且通过健康观察窗?} G -->|是| H[恢复服务] G -->|否| I[保存快照并停止喂狗] I --> J[硬件复位]典型恢复级别
Erlang/OTP Supervisor 的 one_for_one、one_for_all、rest_for_one和最大重启强度机制可作为恢复策略设计参考。嵌入式系统同样需要明确:是只重启故障任务、重启一组相互依赖任务,还是重启整个系统;同时要限制单位时间内重启次数,避免设备进入无限 reset loop。
防止复位风暴
Watchdog 设计必须考虑“复位后仍立即触发同一问题”。推荐保存并评估:
reset_reason
watchdog_failure_code
boot_attempt_counter
last_boot_duration
consecutive_watchdog_resets
firmware_slot
configuration_generation
若在短时间内连续多次 watchdog reset,应切换到安全策略:
复位计数应存放在 retention RAM、备份寄存器或具有耐久性设计的持久化区域,不能每次高频擦写同一 Flash 页。
超时参数如何量化
看门狗超时不能凭经验随意设置。至少要区分:
T_supervisor_period Supervisor 评估周期
T_participant_deadline 关键参与者业务期限
T_detection 从故障发生到被判定的时间
T_snapshot 保存最小故障快照的时间
T_recovery 允许局部恢复的最大时间
T_feed_jitter 调度与时钟抖动
T_hw_wdt 硬件看门狗超时
基本约束可表示为:
T_detection <= T_supervisor_period * failure_threshold + T_feed_jitter
若允许在停止喂狗前保存现场并尝试一次短恢复,则应满足:
T_hw_wdt > T_detection + T_snapshot + T_recovery + T_margin
但 T_hw_wdt也不能过大,否则系统失去快速恢复能力。若硬件支持 pretimeout,可拆分为:
T_pretimeout = T_detection + T_recovery_budget
T_reset = T_pretimeout + T_snapshot_budget + T_hard_margin
示例
假设:
Supervisor 周期 100 ms
关键控制任务最长无进展时间 300 ms
连续失败阈值 2 次
快照保存预算 50 ms
局部外设恢复预算 200 ms
调度与时钟安全余量 150 ms
则最坏检测时间约为:
T_detection <= 100 ms * 2 + 100 ms 抖动 = 300 ms
硬件 WDT 可先估算为:
T_hw_wdt > 300 ms + 50 ms + 200 ms + 150 ms = 700 ms
实际可取硬件支持的 800ms 或 1s 档位,并通过压力测试验证。若 Flash 写入、低功耗唤醒或无线校准存在合法长延迟,应使用运行模式 profile,而不是把全局 WDT 永久放宽到数十秒。
运行模式与动态健康策略
系统在启动、正常运行、升级、工厂测试、深度睡眠和故障恢复阶段,对任务进度的要求不同。不能用一套固定 heartbeat 规则覆盖所有阶段。
stateDiagram-v2 [*] --> Boot Boot --> Startup: 内核与基础驱动就绪 Startup --> Operational: 关键服务完成初始化 Operational --> Maintenance: OTA / 校准 / 大块存储操作 Operational --> LowPower: 进入低功耗 LowPower --> Resume: 唤醒 Resume --> Operational: 健康重新确认 Maintenance --> Operational: 维护完成 Startup --> Recovery: 初始化失败 Operational --> Recovery: 健康异常 Recovery --> Operational: 局部恢复成功 Recovery --> Fatal: 超过恢复预算 Fatal --> [*]
各模式监督重点
有界 Health Lease
长 Flash 擦除、证书生成、无线校准等操作可能超过普通 deadline。不要简单 watchdog_disable()或无限延长 timeout。可使用有界 lease:
health_lease_begin(participant, operation, max_duration)
health_lease_progress(participant, stage)
health_lease_end(participant, result)
Supervisor 只允许白名单操作申请 lease,并验证:
多核系统的特殊设计
在 SMP 或 AMP 系统中,单个 Supervisor 任务可能只运行在一个核上,而另一个核已失效。若硬件 watchdog 只由健康核喂养,就会掩盖局部核故障。
推荐方案
- 1. 每个核都有独立
per_core_epoch,由该核上的定时器、Idle Task 或调度钩子更新。 - 3. Supervisor 评估所有核的 epoch,而不是只检查自身。
- 4. 如果 SoC 支持每核 watchdog、NMI 或 cross-core interrupt,应组合使用。
- 5. 对 AMP 子系统使用 mailbox sequence、challenge-response 或共享内存 generation counter。
- 6. 某核失效后先尝试局部核复位;若共享资源一致性无法保证,则升级为 SoC 复位。
flowchart TB C0[Core 0 Tasks] --> E0[Core 0 Epoch] C1[Core 1 Tasks] --> E1[Core 1 Epoch] I0[Core 0 ISR/Tick] --> E0 I1[Core 1 ISR/Tick] --> E1 E0 --> S[Supervisor] E1 --> S S --> G{所有必需 Core 推进?} G -->|是| W[Feed HW WDT] G -->|否| R[抓取跨核现场 / 复位]需要注意 cache coherency 和内存屏障。跨核健康计数器应使用原子操作或明确的共享内存同步,不能依赖普通 volatile推断可见性。
外设、总线和外部依赖监控
业务任务可能没有死锁,而是等待永远不完成的 I/O。每个外部请求都应具备:
request_id
submit_timestamp
deadline
completion_timestamp
cancel_or_reset_path
retry_budget
I/O 健康规则
- • 任何同步 I/O 都必须有有限 timeout。
- • 丢失中断时可由 timeout 路径轮询状态并复位外设。
- • 外部云服务不可达通常应触发降级或重连,不应直接导致整机 watchdog reset。
- • 对于本地关键执行器、传感器或安全协处理器,长期不可用可升级为系统复位或安全态。
sequenceDiagram participant App as 业务任务 participant IO as I/O Manager participant Dev as 外设 / 总线 participant Sup as Supervisor App->>IO: submit(request_id, deadline) IO->>Dev: start transaction alt 正常完成 Dev-->>IO: completion IRQ IO-->>App: result IO->>Sup: complete_count++ else 中断丢失或设备挂起 Sup->>Sup: oldest_inflight_age > deadline Sup->>IO: cancel / reset request IO->>Dev: reset peripheral or bus alt 恢复成功 IO->>Sup: recovery_epoch++ else 恢复失败 Sup->>Sup: 阻止喂狗并保存现场 end end
复位前留痕设计
如果 Watchdog 最终一定会复位,复位前最有价值的工作不是打印大量日志,而是保存一条固定格式、可校验、可在下次启动读取的故障记录。
建议保存字段
magic / version / length / crc
reset_sequence
failure_timestamp
boot_id / firmware_version / build_id
health_failure_bitmap
first_failed_participant
last_feed_epoch
current_mode
per_core_pc_lr_sp
current_task_id
critical_task_state[]
lock_owner[] / hold_time[] / wait_lock[]
queue_depth[] / oldest_item_age[]
state_machine_state[] / transition_epoch[]
heap_free / min_heap_free / stack_high_watermark[]
last_n_events from lock-free trace ring
hardware_reset_reason
存储优先级
- 1. Retention RAM 或备份 SRAM:速度快、对软件路径依赖少。
- 2. RTC Backup Register:容量小,但适合保存故障码和计数。
- 3. FRAM/MRAM:适合频繁记录,但取决于硬件。
- 4. 预擦除 Flash 记录槽:必须避免在 pretimeout 时做擦除。
- 5. 外部安全 MCU:可独立记录主处理器心跳丢失原因。
不应在最后阶段做的事
- • 执行文件系统 mount、sync 或复杂事务。
- • 无限等待 DMA、Flash 或 UART 发送完成。
故障记录必须是 best-effort,并且不能反过来阻止最终复位。
参考开源组件与机制
下面列出的组件不应被机械拼装成同一套软件。它们分别提供了任务级 watchdog、硬件 WDT 接口、锁依赖检测、CPU lockup 检测、服务级健康探针和监督树等设计参考。
| | | |
| 多线程软件 watchdog channel,可选硬件 fallback | 每个任务独立通道、统一超时管理、软件层聚合后交给硬件层 | 默认仍偏执行存活,需要业务 progress 扩展 |
| Zephyr Hardware Watchdog API | | timeout window、硬件独立复位、统一驱动接口 | |
| | 任务订阅、每核 Idle 探针、任务与中断 watchdog 分层 | |
| RT-Thread Watchdog Device | | 设备抽象、timeout 设置、keepalive 和 start/stop | 文档中的 Idle Hook 喂狗示例只能证明系统有空闲时间,不能证明业务健康 |
| | 用户态健康检查通过后才 ping;nowayout;pretimeout | |
| | | |
| Linux soft/hard lockup detector | | | |
| FreeRTOS Event Groups / Task Notifications | 任务向 Supervisor 提交轻量事件和 bit | | |
| Kubernetes liveness/readiness/startup probes | | 区分“能活”“能服务”“启动完成”,失败阈值与周期分离 | |
| | 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。可借鉴点:
- • 软件层维护多个参与者,而不是所有任务直接碰硬件寄存器。
- • 软件监督器失效时,硬件 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 仍大量运行。
- • 关键业务任务退出或永远等待,系统仍有空闲时间。
因此,在 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 或计数器。它们适合构建第一版健康上报通道,但需注意:
- • Supervisor 每轮必须清除或比较 generation,防止旧 bit 重用。
- • ISR 上报应只提交轻量事件,不做复杂健康判断。
- • 如果任务数量超过 event bits,应使用数组、位图分组或计数器表。
- • Event Group 只负责通信,不负责定义“何时算业务健康”。
Kubernetes 健康探针的抽象价值
Kubernetes 区分 startup、readiness 和 liveness:
- • Startup:应用是否完成启动,在此之前不应用普通 liveness 规则误杀慢启动服务。
- • Readiness:当前能否接受业务;失败时可以摘除流量,但不一定立即重启。
- • Liveness:是否进入只能通过重启恢复的失效状态。
嵌入式系统可以采用同样分层:
startup_health 初始化是否按阶段完成
service_ready 当前是否能提供核心功能
system_live 是否仍具备自主恢复和继续推进能力
例如网络暂时断开可能使 service_ready=false,但不应立即触发整机复位;控制回路 deadlock 则可能直接使 system_live=false。
Erlang/OTP Supervisor 的抽象价值
OTP Supervisor 把故障恢复组织成监督树,并提供不同重启策略和最大重启强度。嵌入式项目可对应设计:
one_for_one -> 只复位单个驱动或 Worker
rest_for_one -> 重启故障模块及依赖它的后续模块
one_for_all -> 重启一组共享状态的协作任务
intensity -> 限制单位时间内重启次数,超过后升级整机复位
对于没有进程隔离的 MCU RTOS,直接删除和重建任务可能遗留锁、内存和驱动状态,因此任务级重启必须经过严格设计。若无法证明局部重启安全,应直接重启整个子系统或整机。
一个可落地的通用接口设计
下面是一个与具体 RTOS 解耦的简化接口。实际项目可把原子操作、时间函数、任务标识和锁监控接入 FreeRTOS、RT-Thread、Zephyr 或自研内核。
/**
* @brief Health participant criticality.
*/
typedefenum
{
HEALTH_CRITICAL,
HEALTH_IMPORTANT,
HEALTH_OPTIONAL,
} health_criticality_t;
/**
* @brief Runtime health state reported by a participant.
*/
typedefenum
{
HEALTH_STATE_STARTING,
HEALTH_STATE_READY,
HEALTH_STATE_IDLE,
HEALTH_STATE_BUSY,
HEALTH_STATE_DEGRADED,
HEALTH_STATE_FAILED,
} health_state_t;
/**
* @brief Static health contract of one supervised participant.
*/
typedefstruct
{
constchar *name;
health_criticality_t criticality;
uint32_t exec_deadline_ms;
uint32_t progress_deadline_ms;
uint32_t startup_grace_ms;
uint8_t failure_threshold;
uint32_t required_mode_mask;
} health_contract_t;
/**
* @brief Register one participant and return its handle.
*
* @param contract Static health contract copied or referenced by the manager.
* @return Non-negative participant handle on success; negative error code otherwise.
*/
inthealth_register(consthealth_contract_t *contract);
/**
* @brief Report that the participant has obtained execution time.
*
* @param handle Participant handle returned by health_register().
*/
voidhealth_report_execution(int handle);
/**
* @brief Report completion of a meaningful business progress point.
*
* @param handle Participant handle returned by health_register().
* @param progress_id Monotonic stage, transaction, frame, or generation identifier.
*/
voidhealth_report_progress(int handle, uint32_t progress_id);
/**
* @brief Publish the current business state without blocking.
*
* @param handle Participant handle returned by health_register().
* @param state Current participant health state.
*/
voidhealth_set_state(int handle, health_state_t state);
/**
* @brief Start a bounded long-operation lease.
*
* @param handle Participant handle returned by health_register().
* @param max_duration_ms Maximum duration accepted by the policy.
* @return 0 on success; negative error code otherwise.
*/
inthealth_lease_begin(int handle, uint32_t max_duration_ms);
/**
* @brief End a previously granted long-operation lease.
*
* @param handle Participant handle returned by health_register().
*/
voidhealth_lease_end(int handle);
参与者上报示例
for (;;)
{
health_report_execution(audio_health);
if (audio_input_available())
{
if (audio_process_one_frame() == 0)
{
frame_sequence++;
health_report_progress(audio_health, frame_sequence);
health_set_state(audio_health, HEALTH_STATE_READY);
}
else
{
health_set_state(audio_health, HEALTH_STATE_DEGRADED);
}
}
else
{
health_set_state(audio_health, HEALTH_STATE_IDLE);
}
task_wait_until_next_period();
}
关键点是:health_report_execution()与 health_report_progress()分开。前者证明任务被调度,后者只有在业务完成点更新。
Supervisor 主循环伪代码
for (;;)
{
constuint64_t now = monotonic_time_us();
health_result_t result;
health_collect_snapshot(&snapshot, now);
result = health_evaluate(&snapshot, current_mode, now);
if (result.feed_allowed)
{
hardware_watchdog_keepalive();
health_record_feed_epoch(now);
}
else
{
health_isolate_failed_services(&result);
if (!health_try_bounded_recovery(&result))
{
health_store_crash_record(&snapshot, &result);
health_stop_feeding_watchdog();
}
}
supervisor_wait_until_next_period();
}
这里的 health_try_bounded_recovery()必须有严格的时间预算。进入停止喂狗状态后,不应因为普通任务随后又更新了 heartbeat 就重新喂狗,除非系统明确完成一次受控恢复并通过健康观察窗。
Feed Epoch 与一致性协议
在多任务系统中,简单的 bitmask 方案容易出现竞态。例如 Supervisor 清 bit 的同时,任务又置 bit,可能丢失一轮心跳;或者任务启动时置位一次,Supervisor 未正确清除,导致后续一直健康。
推荐使用 epoch:
supervisor_epoch = N
participant_seen_epoch[i]
participant_progress_epoch[i]
一种协议是:Supervisor 每轮递增全局 epoch;参与者在完成健康点后把本地 seen_epoch更新为当前 epoch。Supervisor 只接受本轮或允许窗口内的 epoch。
更简单且更健壮的方案是直接比较单调 counter:
if current_exec_counter != previous_exec_counter:
execution_progressed = true
if current_progress_counter != previous_progress_counter:
business_progressed = true
对于低频任务,不要求每个 Supervisor 周期都变化,而是用最后变化时间与任务 contract 比较。
任务退出、挂起和动态创建
监督系统必须处理任务生命周期,否则合法停用会被误判为故障,异常退出又可能被忽略。
生命周期状态
UNREGISTERED
STARTING
ACTIVE
SUSPENDED_BY_POLICY
STOPPING
STOPPED
FAILED
规则建议:
- • 任务创建后进入 STARTING,并获得有限 startup grace。
- • 只有 Supervisor 或系统模式管理器可批准
SUSPENDED_BY_POLICY。 - • 任务退出钩子要将状态改为 STOPPED 或 FAILED。
- • 动态任务的 health handle 必须有 generation,防止旧 handle 指向新任务。
时间基准的可靠性
所有 deadline 判断都依赖时间。如果系统 tick 停止或时钟被错误重配,Supervisor 可能永远认为没有超时。因此应至少有两个层次的时间证据:
- • RTOS monotonic tick:用于普通软件 deadline。
- • 独立硬件 watchdog clock、RTC、低速独立振荡器或外部监控时钟:用于最终超时。
可选地交叉检查:
delta_rtos_tick
vs.
delta_independent_timer
若两者长期偏差超过阈值,应报告 timebase fault。进入低功耗前必须明确:硬件 WDT 是否暂停、是否继续计时、唤醒后剩余窗口是多少,以及软件基准是否发生跳变。
内存与栈健康
逻辑停滞也可能由内存耗尽、栈接近溢出或内存破坏引发。可把以下指标纳入健康快照:
heap_free
heap_min_ever_free
allocation_fail_count
pool_free_count
stack_high_watermark per critical task
stack_overflow_hook_count
memory_guard_error
但阈值应避免过度敏感:堆空间低不等于立即复位;若关键分配已失败、恢复路径也无法分配,才可能升级为系统故障。更推荐对关键路径使用静态内存、专用内存池或预分配恢复资源,使 Supervisor 和 crash recorder 不依赖普通堆。
安全性与故障注入考虑
Watchdog 通道本身也可能被错误代码或恶意输入滥用:
- • 任务可以伪造其他参与者的 heartbeat。
建议:
- 1. 只允许 Supervisor 所在特权域访问硬件 WDT。
- 2. Health API 校验 handle generation 和调用者身份。
- 3. Watchdog 启动后使用 hardware lock 或 nowayout 类配置,禁止普通软件关闭。
- 4. Health table 放在受 MPU/MMU 保护区域,业务任务只能写自身槽位。
- 5. Lease 类型、最大时长和次数由静态策略控制。
- 6. 远程维护命令只能切换到预定义 maintenance profile,不能无期限停用 watchdog。
- 7. 故障记录带 CRC、版本和单调序号,防止读取损坏数据后误诊。
测试与故障注入
只有能被主动触发并验证的 watchdog 方案才可信。测试不能只确认“正常运行时不会误复位”,还要确认每类异常都能在规定时间内被检测、留痕和恢复。
故障注入矩阵
| | |
| | |
| | |
| | |
| | |
| | 硬件或 interrupt watchdog 触发 |
| depth 满、dequeue 不变、oldest age 增长 | |
| | |
| | |
| | |
| | |
| | |
| | |
必测指标
fault_detection_latency
recovery_latency
watchdog_reset_latency
false_positive_rate
snapshot_success_rate
snapshot_write_time
maximum_supervisor_execution_time
health_update_overhead
lock_monitor_overhead
reset_storm_escape_success
压力测试组合
- • CPU 100% 负载 + 高频中断 + Flash 写入。
- • Tickless idle + 频繁睡眠唤醒。
- • 双核同时执行 cache-heavy 工作负载。
- • 文件系统损坏、网络断开、外设 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、静态分析、运行时依赖检查和故障注入,从源头降低死锁概率。
推荐实施步骤
第一阶段:建立最小闭环
- 1. 选出真正影响设备核心功能的 3~8 个 critical participant。
- 3. 建立唯一 Supervisor 和固定周期评估。
- 4. 为每个参与者分别记录 execution counter 与 progress counter。
- 5. 配置 startup、operational 两套 profile。
- 6. 失败时保存最小 failure bitmap 和 reset reason。
第二阶段:补齐资源与流控监控
- 1. 包装关键 Mutex,记录 owner 和 hold time。
- 2. 监控核心队列的 depth、oldest item age 和 complete counter。
- 3. 所有 I/O 请求增加 deadline 和取消/复位路径。
- 4. 加入 per-core epoch、Idle counter 和关键 ISR counter。
- 5. 使用 pretimeout 或 early warning 保存寄存器和任务快照。
第三阶段:加入恢复策略与工程化验证
- 2. 引入 restart budget,防止恢复风暴。
- 3. 实现 reset-loop 检测、安全模式和固件回滚。
- 4. 建立系统级 fault injection 测试。
- 5. 将检测时间、误复位率、快照成功率纳入发布指标。
- 6. 在 Debug 构建中启用更强的锁依赖和断言检查。
最终回答组织方式
面试或设计评审中,可以按以下顺序回答:
- 1. 先指出根因:独立喂狗任务只能证明自己被调度,不能证明系统业务健康。
- 2. 提出分布式健康监督:关键任务提交执行、进度、deadline、锁和队列证据,由 Supervisor 统一判定。
- 3. 说明喂狗门控:只有所有 critical health contract 满足,Supervisor 才喂硬件 WDT。
- 4. 说明死锁覆盖:记录 Mutex owner、持有时间、等待者和锁顺序;Supervisor 无锁读取,超时后禁止喂狗。
- 5. 扩展到通用故障:活锁、饥饿、队列堵塞、状态机停滞、I/O 悬挂、单核失效和时间基准故障。
- 6. 说明分层 watchdog:软件 Task WDT 聚合多参与者,硬件 WDT 兜底调度器和软件层失效,必要时外部监控再兜底。
- 7. 说明恢复与留痕:先有界局部恢复,再子系统重启,最终停止喂狗;pretimeout 保存任务、锁、队列、PC/LR 和最近事件。
- 8. 说明参数量化:根据监督周期、业务 deadline、快照时间、恢复预算和抖动反推硬件 timeout。
- 9. 说明工程落地:参考 Zephyr Task WDT、ESP-IDF TWDT/IWDT、Linux watchdog/lockdep、RT-Thread WDT、OTP Supervisor 等机制,并通过故障注入验证。
一句话概括:看门狗不应监督“某个喂狗线程是否活着”,而应监督“系统是否仍在完成它必须完成的工作”;硬件喂狗只是所有关键健康证据通过后的最终动作。
参考链接
- • Zephyr Task Watchdog[1]
- • Zephyr Task Watchdog APIs[2]
- • RT-Thread WATCHDOG Device[4]
- • Linux Watchdog driver API[5]
- • Linux Runtime locking correctness validator / lockdep[6]
- • Linux Softlockup and Hardlockup Detectors[7]
- • FreeRTOS Event Groups[8]
- • FreeRTOS Task Notifications[9]
- • Kubernetes Liveness, Readiness and Startup Probes[10]
- • Erlang/OTP Supervisor Behaviour[11]
引用链接
[1]Zephyr Task Watchdog: https://docs.zephyrproject.org/latest/services/task_wdt/index.html
[2]Zephyr Task Watchdog APIs: https://docs.zephyrproject.org/latest/doxygen/html/group__task__wdt__api.html
[3]ESP-IDF Watchdogs: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/wdts.html
[4]RT-Thread WATCHDOG Device: https://www.rt-thread.io/document/site/programming-manual/device/watchdog/watchdog/
[5]Linux Watchdog driver API: https://docs.kernel.org/watchdog/watchdog-api.html
[6]Linux Runtime locking correctness validator / lockdep: https://docs.kernel.org/locking/lockdep-design.html
[7]Linux Softlockup and Hardlockup Detectors: https://docs.kernel.org/admin-guide/lockup-watchdogs.html
[8]FreeRTOS Event Groups: https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/06-Event-groups
[9]FreeRTOS Task Notifications: https://freertos.org/RTOS_Task_Notification_As_Event_Group.html
[10]Kubernetes Liveness, Readiness and Startup Probes: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
[11]Erlang/OTP Supervisor Behaviour: https://www.erlang.org/doc/system/sup_princ.html