microbus 核心实现:分发快照让回调再入队也不失控
声明: microbus 是一个开源项目,作者为 Peter Vanderhell。本文是阅读该项目源码和文档后整理的学习笔记,用于理解嵌入式事件总线在裸机上的确定性实现方式。本文作者不是该项目的开发者,未参与该项目的任何代码贡献。 文中所有工程细节均来自对开源代码的分析,不代表本文作者的设计决策。
项目仓库:github.com/Vanderhell/microbus
这篇文章讲 microbus 源码里的三个实现思想:状态归调用者、分发确定性优先、队列用快照保证有界。不重复入门用法,直接看机制。
核心思想
microbus 的全部设计围绕一个目标:在单上下文裸机上提供确定性的发布订阅。三个落点:
- 状态归调用者:
mbus_t由调用者分配,库内没有全局变量,可多实例、可放栈上 - 分发确定性:遍历订阅表按固定顺序,分发期间锁定订阅表
- 队列有界性:dispatch 入口快照队列计数,单次分发绝不失控
订阅表与投递期锁定
订阅表就是 mbus_t 里的 subs[16] 数组,每个槽是 {topic, handler, ctx, active}。分发时 mbus_deliver 从下标 0 升序扫到 MBUS_MAX_SUBSCRIBERS,话题匹配或命中 MBUS_TOPIC_ANY 就调用回调。
确定性从哪来:mbus_deliver 进出时维护 delivery_depth 计数。深度大于 0 期间,subscribe、unsubscribe、unsubscribe_topic 直接返回 MBUS_ERR_BUSY。效果是分发期间订阅表不可变,遍历顺序和结果都可预测。代价是回调里不能改订阅,只能等分发结束再改。
环形队列的单调计数
队列是 queue[8] 加 q_head、q_tail,两个 32 位计数器只增不减,用 (head & (DEPTH-1)) 取模定位槽位,所以深度必须是 2 的幂。满判定是 head - tail >= DEPTH,空判定是 head == tail。
单调计数的好处:不需要单独的 count 字段;32 位计数要入队 2^32 次才回绕;单生产者单消费者下 head 和 tail 各自只被一方写,配合临界区宏就能构成 ISR 与主循环的通道。
dispatch 入口快照
队列存在的意义是解耦产生事件和处理事件。mbus_dispatch 的实现思想是快照:
- 入口取
eligible = 当前队列计数 - 循环最多处理 eligible 个:pop 到局部事件,分发,
eligible-- - 回调里再入队的事件本回合不处理,留到下次 dispatch
没有快照的版本,回调再入队会让循环追着队尾跑,可能无限执行。快照把单次 dispatch 的执行时间变成有界:最多 eligible 次回调,回调里入队再多也只增长 head。
ABI 校验与统计计数
mbus_impl_init校验bus_size == sizeof(*bus):库和消费者配置不一致时编译出不同大小的结构,运行期直接拒绝,避免踩坏调用者内存publish_count、deliver_count、drop_count三个计数器,drop 记录队列满丢事件,丢事件率可观测MBUS_ISR_SAFE_QUEUE开启时 push、pop、计数都用临界区宏包裹,构成 SPSC 模式
设计取舍与边界
- 拷贝换安全:payload 拷进事件对象,调用方缓冲可复用,代价是 40 B 事件对象加一次 memcpy
- 确定性换灵活:分发期锁定订阅表,回调内改订阅被拒绝
- 有界换吞吐:单次 dispatch 只处理入口快照,回调密集入队时事件延迟到下次
- 边界:默认单上下文,多线程访问要外部加锁;话题空间 uint8 只有 256 个