嵌入式面试真题第 14 题:防范业务逻辑死锁的系统健康监督与看门狗设计

在这里插入图片描述

问题

在多任务嵌入式系统、RTOS 固件、边缘网关、工业控制器、车载控制单元、消费电子设备或其他长期无人值守的软件系统中,通常会配置硬件看门狗,以便系统发生严重异常时自动复位。

传统实现往往由一个独立 Watchdog Task、Idle Hook、主循环或高优先级定时任务周期性喂狗。只要这个喂狗节点还能获得 CPU 时间,硬件看门狗就不会超时。然而,系统可能已经出现以下“进程还活着、业务却不再工作”的假活状态:

  • 两个或多个任务形成循环等待,业务路径死锁。
  • 高优先级任务持续运行,低优先级关键任务长期饥饿。
  • 任务不断重试或相互唤醒,但业务状态不再前进,形成活锁。
  • 消息队列、缓冲区或对象池耗尽,生产者和消费者互相等待。
  • 状态机卡在某个中间状态,周期函数仍在调用,但事务永远无法完成。
  • 外设、总线、文件系统、网络栈或协处理器请求悬挂,任务仍在等待或轮询。
  • 某个 CPU 核、调度 tick、关键中断或时间基准失效,而其他核仍能继续喂狗。
  • 看门狗任务本身优先级过高、依赖过少,反而绕过了真正的业务健康状态。

面对这种问题,如何把看门狗从“某个线程周期运行就喂狗”的简单定时器,重新设计成一套通用、可量化、可诊断、可扩展的系统健康监督机制?如何定义喂狗条件、采集健康证据、监控锁和队列、处理启动与低功耗阶段、保存复位现场,并参考已有开源组件构建可落地方案?

回答

结论:硬件看门狗只能作为最终执行器,不能直接代表系统健康。正确方案是在业务任务与硬件看门狗之间增加一个独立的 Health Supervisor,由所有关键执行单元提交可验证的健康证据;只有当任务执行、业务进度、时限约束、资源锁、队列流动、调度能力、时间基准和关键依赖均满足当前运行模式的健康契约时,Supervisor 才允许喂硬件看门狗。

喂狗条件必须从:

1
“看门狗任务本轮获得了 CPU”

升级为:

1
“系统在规定时间窗内完成了所有必要工作,并且没有出现不可接受的停滞、资源占用或依赖失效”

这套设计的核心不是简单增加若干 heartbeat bit,而是建立四层监督链路:

  1. 执行层健康:关键任务、关键中断、每个 CPU 核和调度器是否仍有执行机会。
  2. 业务层健康:事务、状态机、队列和数据流是否持续产生可观察的进展。
  3. 资源层健康:Mutex、Semaphore、队列、内存池、总线和外设是否在限定时间内被释放或完成。
  4. 恢复层健康:异常能否先局部恢复、再子系统重启、最终由硬件看门狗复位,并在复位前留下足够诊断证据。

总体架构

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
flowchart LR
subgraph Business[业务与执行单元]
T1[关键任务 A]
T2[关键任务 B]
T3[状态机 / Worker]
ISR[关键 ISR / Tick / DMA]
CORE[每核调度探针]
end

subgraph Evidence[健康证据层]
HB[执行心跳与调度时间戳]
PG[业务进度计数 / Epoch]
DL[Deadline / 周期完成记录]
LK[锁 Owner / 持有时长 / 等待图]
Q[队列水位 / 入队出队速率]
RS[资源与依赖状态]
end

subgraph Supervisor[Health Supervisor]
SNAP[无锁快照采集]
RULE[Health Policy Engine]
MODE[启动 / 运行 / 升级 / 低功耗配置]
GATE{Feed Gate}
ESC[恢复与升级策略]
DUMP[故障快照与持久化]
end

subgraph Watchdog[复位执行链]
SW[软件任务看门狗]
HW[硬件看门狗]
EXT[外部监控 MCU / PMIC WDT]
end

T1 --> HB
T2 --> HB
T3 --> PG
ISR --> DL
CORE --> HB
T1 --> LK
T2 --> LK
T3 --> Q
T1 --> RS
T2 --> RS

HB --> SNAP
PG --> SNAP
DL --> SNAP
LK --> SNAP
Q --> SNAP
RS --> SNAP
SNAP --> RULE
MODE --> RULE
RULE --> GATE
GATE -->|全部满足| SW
SW --> HW
HW -.可选级联.-> EXT
GATE -->|不满足| ESC
ESC --> DUMP
ESC -->|停止喂狗| HW

业务任务不直接操作硬件 Watchdog 寄存器,也不应该各自独立喂同一个硬件看门狗。它们只负责提交自身职责范围内的健康证据。Health Supervisor 读取这些证据、执行规则评估,并作为唯一喂狗授权点。

硬件看门狗应被视为无法依赖常规软件路径时的最终恢复手段。软件 Supervisor、日志系统、任务调度器甚至整个内核都可能失效,因此监督链应尽量具备分层和独立性:软件任务看门狗负责多任务聚合,硬件看门狗负责调度器或系统级失效,必要时再由独立 PMIC、外部 MCU 或安全岛监控主处理器。

问题应如何抽象

这道题表面上是“两个任务互相等待 Mutex,而看门狗任务仍在喂狗”,但更通用的抽象是:

系统存在至少一个仍可执行的路径,因此传统 watchdog heartbeat 持续更新;与此同时,决定系统服务能力的关键不变量已经失效,或者关键状态长时间没有推进。

这类故障可统一称为 业务假活、逻辑停滞或 liveness failure。它不同于直接崩溃:程序计数器仍在变化,部分线程仍在调度,甚至日志仍有输出,但系统已无法完成其主要职责。

常见故障类型

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
mindmap
root((业务假活与逻辑停滞))
等待型故障
Mutex 循环等待
Semaphore 永久等待
条件变量丢失唤醒
同步 RPC 相互调用
调度型故障
高优先级忙循环
低优先级关键任务饥饿
优先级反转
某 CPU 核局部失效
流控型故障
队列永久满
队列永久空
Buffer 不再推进
对象池耗尽
状态机故障
状态不转移
事务无完成
重试风暴
活锁
I/O 与依赖故障
总线事务悬挂
外设中断丢失
文件系统或网络假死
协处理器无响应
时间与平台故障
Tick 停止
中断长期关闭
时钟源跳变
低功耗恢复异常

因此,单纯要求“每个任务每秒置位一次”仍然不够。一个任务完全可以在错误循环里持续置位;一个状态机可以反复进入同一状态;一个消息处理线程可以一直被调度,却没有成功消费任何有效消息。健康证据必须能区分以下三件事:

层次 需要回答的问题 仅靠普通 heartbeat 能否覆盖
Execution 任务是否获得过 CPU、ISR 是否发生、调度 tick 是否工作 部分可以
Progress 任务是否完成了有意义的业务步骤 不能
Correctness 完成结果是否满足协议、资源和状态约束 不能

健康模型:从 Heartbeat 到 Health Contract

推荐把每个关键参与者定义成一个受监督实体 health participant。参与者不是必须等同于线程,它可以是:

  • 一个 RTOS 任务。
  • 一个周期控制回路。
  • 一个协议会话或连接。
  • 一个状态机实例。
  • 一个 DMA/ISR 数据通路。
  • 一个 CPU 核的调度探针。
  • 一个关键外设或协处理器。
  • 一个共享服务,如文件系统、存储服务、网络管理器或内存池。

每个参与者注册一份 Health Contract,至少包含:

1
2
3
4
5
6
7
8
participant_id       参与者唯一标识
criticality 关键等级:critical / important / optional
period 预期执行周期或检查周期
progress_deadline 最长允许无业务进展时间
startup_grace 启动阶段宽限时间
failure_threshold 连续多少次异常才判定失败
recovery_policy 局部恢复、子系统重启或系统复位
required_in_modes 哪些运行模式下必须健康

运行过程中,参与者提交不同类型的证据:

1
2
3
4
5
execution_heartbeat  本任务或回调确实执行过
progress_epoch 完成了一个有意义的业务阶段
deadline_complete 指定周期或事务在期限内完成
resource_snapshot 当前锁、队列、内存和 I/O 状态
error_state 可恢复错误、永久错误或降级状态

为什么必须区分执行心跳与进度心跳

假设音频任务每 10ms 醒来一次,并执行以下错误循环:

1
2
3
4
while (true) {
retry_same_failed_transaction();
heartbeat++;
}

heartbeat 持续变化,只能证明线程获得了 CPU,不能证明音频帧被成功处理。正确做法是同时维护:

1
2
3
exec_counter++           每次调度周期都更新
frame_commit_counter++ 只有一帧真正提交到下游后才更新
last_good_frame_time 记录最后一次成功输出时间

Supervisor 可以据此区分:

观测结果 可能含义 推荐判断
exec 不变,progress 不变 未调度、阻塞、死锁、线程退出 故障
exec 变化,progress 不变 活锁、重复失败、依赖阻塞、空转 超过业务期限后故障
exec 变化,progress 变化 正常推进 健康候选
exec 不变,progress 变化 证据采集设计错误、其他上下文代替更新 需要审计

喂狗门控:所有关键健康条件的合取

硬件喂狗不应由任意一个参与者直接触发,而应由 Feed Gate 统一决策。最简单的逻辑是关键条件的 AND:

1
2
3
4
5
6
7
8
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。更合理的是按关键度和运行模式计算:

1
2
3
critical participant failed  -> 立即阻止喂狗或进入短暂诊断阶段
important participant failed -> 先局部恢复,恢复失败后阻止喂狗
optional participant failed -> 隔离或降级,不一定系统复位

喂狗决策流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
flowchart TD
A[Supervisor 周期到达] --> B[读取所有健康快照]
B --> C{快照完整且时间基准正常?}
C -->|否| F[进入失败处理]
C -->|是| D{关键任务执行与业务进度满足?}
D -->|否| F
D -->|是| E{锁 / 队列 / I/O / 资源约束满足?}
E -->|否| F
E -->|是| M{当前模式允许喂狗?}
M -->|否| F
M -->|是| G[喂软件层或硬件看门狗]
G --> H[记录本轮 Feed Epoch]

F --> I{故障是否可局部恢复?}
I -->|是| J[执行有界恢复并重新评估]
I -->|否| K[保存故障快照]
J --> L{恢复成功?}
L -->|是| H
L -->|否| K
K --> N[停止喂狗 / 主动复位 / 触发外部监控]

Feed Gate 还应避免“旧证据重复使用”。每次喂狗必须消费一个新的监督周期或新的 health epoch,防止某个参与者早先提交过一次健康标志后永久有效。

推荐使用单调计数器而不是单纯布尔位:

1
2
3
4
last_seen_exec_counter[i]
last_seen_progress_counter[i]
current_exec_counter[i]
current_progress_counter[i]

Supervisor 每轮比较新旧值,再更新时间戳。这样可以避免任务只在启动时置一次 bit,随后一直被误判为健康。

关键任务应提交哪些健康证据

不同任务不能用完全相同的 heartbeat 语义。健康点必须放在“任务确实完成核心职责”的位置,而不是任务循环入口、无条件定时回调或错误处理循环中。

任务类型 不推荐的健康点 推荐的健康点
传感器采集 任务被唤醒时 完成一次采样、校验并发布有效数据后
控制回路 周期函数入口 控制量计算完成并成功写入执行器后
音频处理 每次循环开始 音频帧成功处理并提交 DMA 后
蓝牙/网络 收到任意事件 事件队列持续消费、连接状态机推进或数据确认完成后
存储任务 开始处理请求 请求完成、提交或明确返回错误后
UI 渲染 刷新定时器触发 一帧渲染完成并提交显示控制器后
OTA 升级 下载线程仍在运行 分块下载、校验、写入和最终提交阶段分别推进后
文件系统服务 服务线程被调度 请求队列完成数增加且无长时间挂起请求

业务进度指标的设计原则

  1. 必须对应可验证完成点:在事务提交、帧输出、状态迁移或请求完成后更新。
  2. 必须单调或可比较:优先使用递增计数、序号、epoch 或最后完成时间。
  3. 不能依赖正在被监控的锁:否则 Supervisor 为读取健康状态也可能被同一死锁阻塞。
  4. 不能由无关任务代写:健康证据应能定位责任实体。
  5. 不能无限宽限:长期等待外部依赖也必须有业务级超时、降级或隔离策略。
  6. 要允许合法空闲:任务没有输入时,不应强迫 progress counter 变化,而应证明“空闲是被允许的”。

合法空闲可以通过以下条件表达:

1
2
3
4
input_queue_empty
&& no_inflight_transaction
&& upstream_marked_idle
&& task_execution_heartbeat_recent

只有在没有待办工作时,缺少业务进度才可视为正常;一旦存在 pending work,就必须在 deadline 内看到完成数、消费指针或状态 epoch 推进。

锁超时与死锁监控

题目中的 Mutex 循环等待应由两类机制共同覆盖:

  1. 开发期预防与检测:固定锁顺序、静态分析、运行时 lock dependency 检查、超时断言和故障注入。
  2. 产品运行期监督与恢复:记录锁 owner、持有时长、等待者和调用点,超过阈值后禁止喂狗并保存现场。

锁元数据

建议为关键锁增加轻量包装层,至少记录:

1
2
3
4
5
6
7
8
9
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

锁获取和释放路径更新原子元数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sequenceDiagram
participant T as 业务任务
participant L as Monitored Mutex
participant M as Lock Metadata
participant S as Supervisor

T->>L: lock(timeout)
L->>M: 记录 waiter / 请求时间
alt 获取成功
L-->>T: success
T->>M: 设置 owner 与 acquire_time
T->>T: 执行业务临界区
T->>L: unlock()
L->>M: 清除 owner / 更新最大持有时长
else 等待超时
L-->>T: timeout
T->>M: timeout_count++
end

S->>M: 无阻塞读取快照
S->>S: 计算持有时长和等待关系

Wait-for Graph

若资源允许,可构造任务与锁的等待图:

1
2
3
4
5
flowchart LR
TA[Task A] -->|等待| LB[Mutex B]
LB -->|持有者| TB[Task B]
TB -->|等待| LA[Mutex A]
LA -->|持有者| TA

图中出现有向环,意味着存在循环等待条件。小型 MCU 不一定要实时运行完整图算法,但可以采用以下折中:

  • 仅监控少量关键锁。
  • 为锁定义全局层级 lock_rank,运行时断言只能按 rank 递增获取。
  • 记录每个任务“当前持有锁集合”和“正在等待锁”。
  • 仅在 Supervisor 发现超时后离线构造等待链。
  • 在 Debug/Fault-Injection 构建中启用完整依赖图,在 Release 构建中保留轻量持有时长监控。

Linux 的 lockdep 是典型参考:它记录锁类别之间的获取顺序,并检测可能形成循环依赖的锁顺序。该机制主要用于开发和验证,不能替代产品中的恢复策略,但其“把锁获取顺序表示成依赖图”的思想非常适合嵌入式关键锁审计。

锁超时不是简单地统一设为一个值

锁的合理阈值应根据临界区最坏执行时间、可被抢占时间和平台抖动确定:

1
2
3
4
T_lock_limit >= T_critical_section_wc
+ T_preemption_wc
+ T_interrupt_jitter
+ T_margin

不同锁应有不同阈值:

锁类型 典型阈值策略
寄存器或短链表保护锁 微秒至低毫秒级,超时通常是严重缺陷
Flash 擦写或文件系统事务锁 可到数十或数百毫秒,但应有阶段进度
网络连接管理锁 不应跨阻塞 I/O;若必须存在,应有明确上限
全局配置锁 应避免长时间持有,更新过程可拆成 copy-then-swap
图形或音频大对象锁 优先改为双缓冲、消息传递或 ownership transfer

不要把所有锁阈值都设置得大于硬件看门狗超时,否则锁监控失去意义。

队列、缓冲区与对象池的进度监控

很多逻辑停滞不是 Mutex 死锁,而是流控闭环失效。例如:

  • 生产者等待队列空位,消费者等待另一个资源。
  • 消费者仍在运行,但每次取到的都是无效消息。
  • 队列持续满,任务不断超时重试。
  • 消息被生产但从未完成确认,in-flight 数持续增长。
  • 内存池耗尽,释放路径被某个错误状态跳过。

因此 Supervisor 不应只检查线程心跳,还要检查队列和资源的“流动性”。推荐记录:

1
2
3
4
5
6
7
8
9
10
enqueue_count
dequeue_count
complete_count
drop_count
queue_depth
queue_high_watermark
oldest_item_age
inflight_count
pool_free_count
allocation_fail_count

队列健康判定示例

1
2
3
4
5
6
7
8
9
10
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

队列水位本身不是故障。队列长期满且出队计数不变,才是高价值故障信号;队列长期空也不一定有问题,除非上游明确存在待生产工作或数据源应持续输出。

数据流监控图

1
2
3
4
5
6
7
8
9
10
flowchart LR
P[Producer] -->|enqueue_count| Q[(Queue)]
Q -->|dequeue_count| C[Consumer]
C -->|complete_count| O[Output / Commit]
Q --> M[depth / oldest age]
P --> M2[drop / timeout]
C --> M3[inflight / error]
M --> S[Supervisor]
M2 --> S
M3 --> S

状态机与事务推进监控

状态机通常不会彻底停止执行,而是卡在某个等待状态、异常分支或重复重试路径中。仅监控任务是否周期运行无法发现这类问题。

建议每个关键状态机维护:

1
2
3
4
5
6
7
8
current_state
state_enter_timestamp
transition_counter
last_successful_transition
transaction_id
transaction_stage
retry_count
last_error

Supervisor 根据状态定义最大驻留时间:

状态 是否允许长期驻留 监控方式
IDLE 需要证明无待办事务
WAIT_INPUT 视场景 检查输入源是否有 pending 数据
CONNECTING 最大连接时限 + 重试预算
WAIT_RESPONSE 请求 deadline + 取消机制
COMMITTING 阶段进度 + 存储写入时限
ERROR_RETRY 最大重试次数 + 退避 + 升级恢复
DEGRADED 可以 必须明确降级能力和退出条件
1
2
3
4
5
6
7
8
9
10
11
12
stateDiagram-v2
[*] --> Startup
Startup --> Ready: 初始化完成
Ready --> Processing: 收到工作
Processing --> Ready: 事务完成 / progress++
Processing --> WaitingIO: 提交外部请求
WaitingIO --> Processing: I/O 完成
WaitingIO --> Recovering: 超过 I/O deadline
Processing --> Recovering: 状态驻留超时
Recovering --> Ready: 局部恢复成功
Recovering --> Fatal: 重试预算耗尽
Fatal --> [*]: 停止喂狗或主动复位

需要避免一个常见错误:在每次状态机函数调用时更新 heartbeat。正确做法是在合法状态转换、事务阶段提交或确认完成时更新 transition_counterprogress_epoch

Supervisor 自身必须避免成为死锁参与者

Supervisor 的健康检查路径应遵循“只读、有限时、无业务依赖”的设计原则。它不应为了检查系统健康而获取正在被检查的业务 Mutex,也不应同步调用可能阻塞的驱动、文件系统、网络或日志接口。

推荐的数据采集方式

  1. 原子变量:计数器、时间戳、状态枚举和 bitmask 使用原子读写。
  2. 版本化快照:写侧先更新 sequence 为奇数,写完后变成偶数;读侧检查前后 sequence 一致。
  3. 双缓冲快照:业务任务更新非活动页,再原子切换索引。
  4. 单向事件记录:任务向无阻塞 ring buffer 追加事件,Supervisor 只读。
  5. 调试寄存器或 TCB 只读访问:读取任务状态、栈水位和 PC/LR,不等待业务锁。
1
2
3
4
5
6
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 一致性示例

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @brief Lightweight health snapshot published by one participant.
*/
typedef struct
{
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 停止,其他核仍推进
调度器异常 多个互不相关任务同时停止,但某些 ISR 仍运行
CPU 过载 任务仍有进展,但 deadline miss 持续增加
优先级反转 高优先级任务等待低优先级 owner,锁持有时长异常

ESP-IDF 的 Task Watchdog 通过监控各 CPU 的 Idle Task 和可订阅任务来发现长时间不让出 CPU 的任务;Zephyr Task Watchdog 则提供多个软件 watchdog channel,并可选用硬件 watchdog 作为 fallback。这些机制说明“一个硬件 WDT 直接对应一个喂狗线程”不足以覆盖多任务系统,应先在软件层聚合多个受监督实体。

软件任务看门狗与硬件看门狗的分层

建议采用至少两层:

1
2
3
4
5
6
flowchart TD
A[关键任务 / 状态机 / ISR] --> B[软件健康通道]
B --> C[Health Supervisor]
C --> D[软件 Task Watchdog]
D --> E[硬件 Watchdog]
E -.可选.-> F[外部 PMIC / Safety MCU]

软件层负责

  • 多参与者注册、动态启停和模式切换。
  • 每个参与者不同的周期、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
2
3
4
5
6
7
flowchart LR
A[上次喂狗] --> B[禁止喂狗窗口]
B --> C[允许喂狗窗口]
C --> D[超时复位点]
B -.过早喂狗.-> E[异常 / 复位]
C -.正常喂狗.-> A
D --> F[硬件复位]

Pretimeout / Early Warning

若硬件支持预超时中断、双阶段 watchdog 或 early warning,可在最终复位前执行最小诊断:

  1. 冻结普通日志写入,避免再次阻塞。
  2. 保存 reset reason、Supervisor failure bitmap 和最近 health epoch。
  3. 记录当前任务、每核 PC/LR、栈指针和关键寄存器。
  4. 保存关键锁 owner、等待者和持有时长。
  5. 保存队列水位、最老消息年龄和状态机阶段。
  6. 将固定大小的 crash record 写入 retention RAM、备份寄存器、FRAM 或预留 Flash 区。
  7. 返回中断并让硬件 watchdog 执行最终复位。

Linux watchdog API 中的 pretimeout 机制就是这一思想:在最终 timeout 之前先触发通知,以便记录 panic、core dump 或其他诊断信息。实现时必须确保 pretimeout handler 不依赖复杂锁、动态内存和可能阻塞的存储路径。

恢复策略:从局部恢复到系统复位

发现异常后不应一律立即硬复位,也不能无限尝试局部恢复。推荐建立有界恢复阶梯:

1
2
3
4
5
6
7
8
9
10
flowchart TD
A[检测到健康异常] --> B[隔离新请求 / 标记 Not Ready]
B --> C[取消或超时当前事务]
C --> D[复位单个外设 / 通信链路]
D --> E[重启单个 Worker 或子系统]
E --> F[重建依赖子树]
F --> G{恢复成功且通过健康观察窗?}
G -->|是| H[恢复服务]
G -->|否| I[保存快照并停止喂狗]
I --> J[硬件复位]

典型恢复级别

级别 动作 适用场景 风险
L0 清除单个错误标志、重试一次 瞬态错误 可能掩盖持续故障
L1 取消事务、重置队列或连接 单事务卡住 需要保证资源回收完整
L2 复位外设、总线或协处理器 I/O 状态机异常 可能影响共享用户
L3 重启任务组或业务子系统 局部服务失效 RTOS 任务重建必须安全
L4 软复位主系统 内核仍可控 可能无法处理调度器故障
L5 停止喂硬件 WDT,执行硬复位 全局不可恢复或软件不可信 需要持久化现场
L6 外部监控断电重启 主 SoC 或电源域异常 恢复代价最大

Erlang/OTP Supervisor 的 one_for_oneone_for_allrest_for_one 和最大重启强度机制可作为恢复策略设计参考。嵌入式系统同样需要明确:是只重启故障任务、重启一组相互依赖任务,还是重启整个系统;同时要限制单位时间内重启次数,避免设备进入无限 reset loop。

防止复位风暴

Watchdog 设计必须考虑“复位后仍立即触发同一问题”。推荐保存并评估:

1
2
3
4
5
6
7
reset_reason
watchdog_failure_code
boot_attempt_counter
last_boot_duration
consecutive_watchdog_resets
firmware_slot
configuration_generation

若在短时间内连续多次 watchdog reset,应切换到安全策略:

  • 回滚到上一个固件槽。
  • 跳过非关键功能初始化。
  • 使用最小安全配置。
  • 禁止自动执行导致故障的任务。
  • 进入诊断模式并等待维护。
  • 对外报告稳定的故障码,而不是持续重启。

复位计数应存放在 retention RAM、备份寄存器或具有耐久性设计的持久化区域,不能每次高频擦写同一 Flash 页。

超时参数如何量化

看门狗超时不能凭经验随意设置。至少要区分:

1
2
3
4
5
6
7
T_supervisor_period   Supervisor 评估周期
T_participant_deadline 关键参与者业务期限
T_detection 从故障发生到被判定的时间
T_snapshot 保存最小故障快照的时间
T_recovery 允许局部恢复的最大时间
T_feed_jitter 调度与时钟抖动
T_hw_wdt 硬件看门狗超时

基本约束可表示为:

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
2
T_pretimeout = T_detection + T_recovery_budget
T_reset = T_pretimeout + T_snapshot_budget + T_hard_margin

示例

假设:

1
2
3
4
5
6
Supervisor 周期                 100 ms
关键控制任务最长无进展时间 300 ms
连续失败阈值 2 次
快照保存预算 50 ms
局部外设恢复预算 200 ms
调度与时钟安全余量 150 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
2
3
4
5
6
7
8
9
10
11
12
13
14
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 --> [*]

各模式监督重点

模式 必须健康的对象 可暂时豁免的对象 特殊要求
Boot 时钟、内存、启动链、基础 WDT 接管 业务任务 Bootloader 与应用 WDT 无缝交接
Startup 初始化状态机和关键驱动 尚未创建的业务任务 每个初始化阶段有单独 deadline
Operational 全部关键业务参与者 被明确关闭的可选功能 使用严格业务进度检查
Maintenance 升级、存储、校准任务 暂停的普通业务任务 需要有界 lease,不得无限延长
LowPower 低功耗控制器和唤醒源 被挂起任务 硬件 WDT 需支持暂停或低功耗时钟
Resume 时钟恢复、外设恢复、任务恢复 尚未恢复的可选服务 恢复后必须重新建立 baseline
Recovery Supervisor、诊断、恢复执行器 已隔离故障模块 总恢复时间必须受限

有界 Health Lease

长 Flash 擦除、证书生成、无线校准等操作可能超过普通 deadline。不要简单 watchdog_disable() 或无限延长 timeout。可使用有界 lease:

1
2
3
health_lease_begin(participant, operation, max_duration)
health_lease_progress(participant, stage)
health_lease_end(participant, result)

Supervisor 只允许白名单操作申请 lease,并验证:

  • 最大持续时间不超过预配置上限。
  • 操作期间仍有阶段进度。
  • 其他关键任务仍健康。
  • lease 不能嵌套失控或反复续期。
  • 超时后仍进入正常故障处理。

多核系统的特殊设计

在 SMP 或 AMP 系统中,单个 Supervisor 任务可能只运行在一个核上,而另一个核已失效。若硬件 watchdog 只由健康核喂养,就会掩盖局部核故障。

推荐方案

  1. 每个核都有独立 per_core_epoch,由该核上的定时器、Idle Task 或调度钩子更新。
  2. 关键任务记录实际运行核和最近执行时间。
  3. Supervisor 评估所有核的 epoch,而不是只检查自身。
  4. 如果 SoC 支持每核 watchdog、NMI 或 cross-core interrupt,应组合使用。
  5. 对 AMP 子系统使用 mailbox sequence、challenge-response 或共享内存 generation counter。
  6. 某核失效后先尝试局部核复位;若共享资源一致性无法保证,则升级为 SoC 复位。
1
2
3
4
5
6
7
8
9
10
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。每个外部请求都应具备:

1
2
3
4
5
6
request_id
submit_timestamp
deadline
completion_timestamp
cancel_or_reset_path
retry_budget

I/O 健康规则

  • 任何同步 I/O 都必须有有限 timeout。
  • 异步请求必须记录 in-flight 年龄。
  • 丢失中断时可由 timeout 路径轮询状态并复位外设。
  • 总线复位不能在持有全局业务锁时执行。
  • 共享总线恢复必须通知所有客户端重新同步。
  • 外部云服务不可达通常应触发降级或重连,不应直接导致整机 watchdog reset。
  • 对于本地关键执行器、传感器或安全协处理器,长期不可用可升级为系统复位或安全态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 最终一定会复位,复位前最有价值的工作不是打印大量日志,而是保存一条固定格式、可校验、可在下次启动读取的故障记录。

建议保存字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 检测、服务级健康探针和监督树等设计参考。

组件或机制 直接用途 可借鉴的核心原理 局限
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
2
3
startup_health   初始化是否按阶段完成
service_ready 当前是否能提供核心功能
system_live 是否仍具备自主恢复和继续推进能力

例如网络暂时断开可能使 service_ready=false,但不应立即触发整机复位;控制回路 deadlock 则可能直接使 system_live=false

Erlang/OTP Supervisor 的抽象价值

OTP Supervisor 把故障恢复组织成监督树,并提供不同重启策略和最大重启强度。嵌入式项目可对应设计:

1
2
3
4
one_for_one   -> 只复位单个驱动或 Worker
rest_for_one -> 重启故障模块及依赖它的后续模块
one_for_all -> 重启一组共享状态的协作任务
intensity -> 限制单位时间内重启次数,超过后升级整机复位

对于没有进程隔离的 MCU RTOS,直接删除和重建任务可能遗留锁、内存和驱动状态,因此任务级重启必须经过严格设计。若无法证明局部重启安全,应直接重启整个子系统或整机。

一个可落地的通用接口设计

下面是一个与具体 RTOS 解耦的简化接口。实际项目可把原子操作、时间函数、任务标识和锁监控接入 FreeRTOS、RT-Thread、Zephyr 或自研内核。

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/**
* @brief Health participant criticality.
*/
typedef enum
{
HEALTH_CRITICAL,
HEALTH_IMPORTANT,
HEALTH_OPTIONAL,
} health_criticality_t;

/**
* @brief Runtime health state reported by a participant.
*/
typedef enum
{
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.
*/
typedef struct
{
const char *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.
*/
int health_register(const health_contract_t *contract);

/**
* @brief Report that the participant has obtained execution time.
*
* @param handle Participant handle returned by health_register().
*/
void health_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.
*/
void health_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.
*/
void health_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.
*/
int health_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().
*/
void health_lease_end(int handle);

参与者上报示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 主循环伪代码

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
for (;;)
{
const uint64_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:

1
2
3
supervisor_epoch = N
participant_seen_epoch[i]
participant_progress_epoch[i]

一种协议是:Supervisor 每轮递增全局 epoch;参与者在完成健康点后把本地 seen_epoch 更新为当前 epoch。Supervisor 只接受本轮或允许窗口内的 epoch。

更简单且更健壮的方案是直接比较单调 counter:

1
2
3
4
5
if current_exec_counter != previous_exec_counter:
execution_progressed = true

if current_progress_counter != previous_progress_counter:
business_progressed = true

对于低频任务,不要求每个 Supervisor 周期都变化,而是用最后变化时间与任务 contract 比较。

任务退出、挂起和动态创建

监督系统必须处理任务生命周期,否则合法停用会被误判为故障,异常退出又可能被忽略。

生命周期状态

1
2
3
4
5
6
7
UNREGISTERED
STARTING
ACTIVE
SUSPENDED_BY_POLICY
STOPPING
STOPPED
FAILED

规则建议:

  • 任务创建后进入 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
2
3
delta_rtos_tick
vs.
delta_independent_timer

若两者长期偏差超过阈值,应报告 timebase fault。进入低功耗前必须明确:硬件 WDT 是否暂停、是否继续计时、唤醒后剩余窗口是多少,以及软件基准是否发生跳变。

内存与栈健康

逻辑停滞也可能由内存耗尽、栈接近溢出或内存破坏引发。可把以下指标纳入健康快照:

1
2
3
4
5
6
7
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 通道本身也可能被错误代码或恶意输入滥用:

  • 任意模块都能直接写硬件 WDT 寄存器。
  • 任务可以伪造其他参与者的 heartbeat。
  • 越界写破坏 health table。
  • 故障代码反复申请长 lease。
  • 诊断接口可远程关闭 watchdog。

建议:

  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 方案才可信。测试不能只确认“正常运行时不会误复位”,还要确认每类异常都能在规定时间内被检测、留痕和恢复。

故障注入矩阵

注入故障 预期观测 预期动作
任务永久阻塞 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
2
3
4
5
6
7
8
9
10
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 工作负载。
  • 日志后端阻塞或 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、静态分析、运行时依赖检查和故障注入,从源头降低死锁概率。

推荐实施步骤

第一阶段:建立最小闭环

  1. 选出真正影响设备核心功能的 3~8 个 critical participant。
  2. 禁止普通任务直接喂硬件 WDT。
  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 保存寄存器和任务快照。

第三阶段:加入恢复策略与工程化验证

  1. 定义 L0~L5 恢复阶梯和升级条件。
  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 等机制,并通过故障注入验证。

一句话概括:看门狗不应监督“某个喂狗线程是否活着”,而应监督“系统是否仍在完成它必须完成的工作”;硬件喂狗只是所有关键健康证据通过后的最终动作。

参考链接