Zephyr工作队列:避免取消、重新排期与排空时的处理函数竞态
取消工作项,首先是在处理寿命
驱动取消延迟工作后释放上下文,随后处理函数访问旧指针而崩溃。关闭逻辑检查状态时,工作项可能已经从等待超时变成排队或正在执行。因此需要回答的不只是取消调用是否返回,而是谁能保证回收后不再出现生产者或处理函数引用。本文以Zephyr 4.2.0为基线,尤其不能直接照搬工作队列机制在2.6之前的示例。
工作项状态不是互斥的单一枚举:同一对象可能正在运行,同时再次排队,还存在延迟调度状态。4.2.0生命周期说明提醒,不应根据busy或pending的瞬时快照判断后续操作一定安全。查询一返回,状态就可能变化。应将API同步保证与应用关闭协议结合起来。

区分异步取消与同步等待
k_work_cancel()尝试取消,但不会等待正在运行的处理函数完成。延迟工作应使用对应的delayable接口,不能只把内部普通工作成员当成整个对象处理。如果回收内存需要等待处理函数结束,线程可以使用k_work_cancel_sync()或k_work_cancel_delayable_sync(),并提供struct k_work_sync对象。这些函数建立同步边界,不会任意打断处理函数执行。
具体调用上下文、返回值和同步对象存储要求应查固定版本接口定义。不能在ISR中进行同步等待,也不能形成工作队列等待自身的关系。等待处理函数退出时,不要持有它退出所需的互斥锁。启用内核一致性要求的架构,还可能限制同步对象所在内存,照搬栈上变量示例之前应核实配置。
按业务期限选择schedule或reschedule
已有延迟尚未到期时,k_work_schedule()保留原期限,k_work_reschedule()则替换期限。二者对应不同需求:从首个事件开始收集固定时长,和从最后一次输入后等待一段安静时间,并不是同一策略。持续输入可能让安静期策略无限推迟,因此若产品要求最大处理年龄,应另设上限。
这两个接口都不意味着处理函数会在精确时刻执行。超时到达只是使工作进入提交阶段,队列负载与线程调度仍影响真正执行时间。重新安排也不能撤销已运行处理函数的副作用。明确需求是事件合并、每事件一次操作,还是只处理最新状态。如果每个事件都必须保留,就另存事件记录,不要期待反复提交同一个工作项自动形成历史队列。
先关生产入口,再等待结束
可靠关闭从对象进入关闭中状态开始,并使用所有生产者共同遵守的同步机制。阻止计时器、中断和线程继续提交,再同步已经进入提交路径的生产者。之后由适当的控制线程同步取消有关普通及延迟工作,最后才回收外层对象和载荷缓冲区。
单独一个原子关闭标记不一定充分。生产者可能读到未关闭后被打断,等取消完成才继续提交。应采用完整的准入协议、生产者引用追踪或驱动中断同步,消除检查与提交之间的窗口。处理函数在关闭开始后也必须停止自重提。若处理函数退出要获取准入锁,控制者就不能持锁等待同步取消。核心不变量是:最终完成边界之后,不会再产生新引用。
只有拥有队列策略,才适合整体排空
k_work_queue_drain()等待队列排空,plug选项可以继续阻止外部提交。排空期间,队列自身线程重新提交工作有特殊规则。更容易遗漏的是,尚未到期的延迟工作不会因为排空而自动消失。文档说明排空不取消延迟调度;若在队列被堵住时期限到达,提交可能失败。
因此排空队列不能替代逐个取消所属延迟工作,也不适合默认用于共享系统工作队列,因为其他服务仍依赖它。只有阻塞行为或关闭所有权确有需要时,才创建私有队列,并计入线程和栈开销。还应记录被拒绝提交如何处理,解除堵塞后由谁重试或放弃,避免子系统悄悄停止。
围绕处理函数边界构造竞态
建议测试处理函数在读取上下文之前、持有应用资源时、准备再次提交之前设置受控暂停点,并在每个位置启动关闭。另做一个生产者已经通过准入检查、但尚未真正提交的场景。检查所有合法访问结束前,关闭逻辑不会释放对象;也检查结束后生产者不能让旧对象重新活动。
覆盖仅延迟、已排队、正在运行、运行中再次提交和已空闲等情况。让延迟期限在plug期间到达,再确认恢复策略与文档一致。日志记录API返回值、对象代次、入口状态、处理函数起止和最终回收。轻载运行一次正常不能证明这些窗口已覆盖。本文只做来源与接口审查,没有执行Zephyr固件或硬件测试。
验收检查
- 实现绑定Zephyr 4.2.0,或已按目标发布版重新核对。
- 回收前关闭并同步所有提交来源。
- 同步取消所在线程不会与处理函数互相等待。
- 延迟工作使用对应API,且期限策略清晰。
- drain、plug和拒绝提交都有所有者与恢复方式。
- 最后一次释放发生在既有访问完成、未来引用被阻止之后。