挂起持锁RTOS任务的风险:死锁与安全暂停协议

先诊断调度操作,再调整暂停设计

工作任务持有设备互斥锁时,另一个任务调用vTaskSuspend(worker)。诊断任务随后申请同一把锁,却一直等待。首先需要的是有顺序的证据:成功获取锁、拥有者被挂起、其他任务尝试获取,以及之后没有出现原拥有者释放。本文集中分析原生FreeRTOS Kernel V11.1.0的挂起、恢复语义和竞态时间线,示例采用单核前提,不重复完整关闭或OTA静止协议。

挂起阻止任务执行,不会展开调用栈或释放应用资源。官方挂起接口说明,被挂起任务无论优先级多高都不会获得处理时间。因此优先级继承不能让显式挂起的互斥锁拥有者运行释放路径。诊断时应区分可运行但优先级低的拥有者,与已经退出正常调度的拥有者。

任务先检查工作,中断在任务挂起前发布并恢复,任务随后挂起,却没有保留的唤醒事件。
原创概念图。箭头表示所有权或生命周期依赖,不代表实测时序。

任务挂起不带嵌套计数

假设控制者A为校准挂起任务,控制者B又为调试挂起同一任务。当A恢复它,任务即可重新参与调度,即使B仍认为自己的暂停请求有效。vTaskSuspend()次数不累积,多次挂起只需一次vTaskResume()即可恢复,依据见V11.1.0任务接口。

因此,叫作pause_acquire()的包装函数,不能简单把每次申请映射为挂起、每次释放映射为恢复,就宣称支持引用计数。如果多个客户需要共同暂停条件,应用必须明确仲裁。诊断日志应记录每次挂起和恢复的调用者及原因,不要只有工作任务句柄。某个客户恢复后任务意外重启,往往是语义不匹配,不一定是内存损坏。

重建提前恢复的竞态

第二类故障不需要互斥锁。时刻A,工作任务检查共享标记,认为没有工作;时刻B,中断发布工作并尝试恢复它;时刻C,工作任务才调用vTaskSuspend(NULL)。恢复先于挂起发生,没有替未来等待保留令牌,于是任务可能在已经有工作时仍停在挂起状态。

固定版本实现会检查目标是否真正挂起,再执行恢复。这与能够保留事件、直到接收者消费的合适信号量或通知方案不同。任务接口也明确警告:中断可能先到达时,不应使用xTaskResumeFromISR()实现中断同步。测试构建应使用受控暂停点,把事件准确放在检查与挂起之间,比随机加入延时更容易证明原因。

状态查询不能关闭这个窗口

在恢复或挂起之前增加eTaskGetState(),不会让两个操作成为原子动作。文档说明返回的是瞬时状态,在调用者检查前就可能改变。一个控制者看到任务挂起后,另一上下文可能先恢复它;看到任务可运行,也不能证明它下一步不会立刻挂起。

状态查询适合诊断观察,应与事件顺序关联,不能把“看到挂起”当作“设备事务已经完成”之类业务安全条件。还要区分显式挂起,与等待队列、互斥锁或通知造成的阻塞。没有执行的任务也可能只是正在正确的同步对象上等待。检查该内核的状态报告逻辑和等待原因,不要仅凭调试器列表名称猜测。

任务挂起与调度器暂停完全不同

vTaskSuspendAll()暂停调度,但不关闭中断,当前任务仍继续运行。xTaskResumeAll()恢复调度,却不会恢复之前被vTaskSuspend()显式挂起的任务。接口禁止在调度暂停期间调用可能引起上下文切换的API。这些规则与暂停某一个工作任务不同,名字相似不能互相替换。

在单核诊断场景中,控制者若先暂停调度,再等待另一任务持有的锁,就阻止了自己需要的拥有者继续推进。中断仍可能改变共享硬件状态,所以暂停调度也不是通用中断互斥机制。没有检查端口实现和并发保证前,不能推广到多核。记录中应明确区分任务挂起、调度器暂停、中断屏蔽和普通阻塞调用。

遵守ISR恢复边界

在允许调用内核的中断中,应使用xTaskResumeFromISR()而非vTaskResume()。它返回BaseType_t,表示恢复任务是否应引发上下文切换,再按所选端口的中断退出让出机制处理。这个返回值不是“业务消息已送达”的确认。具体中断优先级限制属于处理器端口,超过允许API调用优先级的ISR,不能仅凭名称带FromISR就使用它。

实现中的待就绪路径还表明,恢复与调度器暂停可能相互影响。因此时序记录应区分恢复请求、进入就绪和真正执行。唤醒之后何时运行,仍取决于调度。中断时间戳不能直接当作工作任务恢复设备操作的时刻,否则会把调度等待误算成外设延迟。

验证机制,再选择最小修复

建议测试矩阵覆盖持锁时挂起、两次挂起一次恢复、自挂起之前提前恢复、状态查询竞态,以及合法优先级的ISR恢复。另加调度暂停案例,但不要为测试引入被禁止的阻塞调用。记录任务身份、调用者、锁拥有者、调度状态和有序时间戳。本文没有执行这些目标测试。

如果目的是传递事件,应选择能保留事件的合适原语,并验证等待谓词。如果目的是业务暂停,则请求工作任务到达资源安全点后协作等待,把应用协议与调度语义分开。绝不能通过外部替另一个任务释放互斥锁来掩盖问题。验收目标是每次停滞都有可复现的顺序解释,最终机制也确实符合事件传递、暂停所有权或调度控制的真实需要。

类似文章