结论:msgq 共享内存队列的「单一发布者」是当前写入者冲突检测,不是权限认证或优先级仲裁——第二个 PubSocket.connect() 会先改写共享 write_uid 并重置读者槽位,旧发布者要到下次 send() 才收到 EADDRINUSE。读者槽位固定 15 个,超出时清空并逐出原有读者,会导致读者重置与帧丢失;环形缓冲回绕覆盖慢读者时,msgq_reset_reader() 把读指针放到当前写指针,已跳过的历史帧不可补取,合并读取也不报告被跳过的消息数。以下为对固定源码归档的静态追踪,不等同对目标系统的并发、权限或最坏时延测试。
进程间链路
cereal.messaging.pub_sock() 与 sub_sock() 按 services.py 选择队列大小,最终调用 msgq_new_queue()。它在 Linux /dev/shm/msgq_、macOS /tmp/msgq_ 下用主题名建 mmap 文件,可用 OPENPILOT_PREFIX 改前缀;msgq.h 的默认容量为 1 MiB,读者槽位固定 15 个。services.py 对 CAN、视频编码等使用更大容量。
共享内存文件创建模式为 0664,实际访问还受目录权限、umask 和进程用户影响;源代码中的这个模式本身不等于插件级授权策略。发布者经 PubMaster.send → PubSocket.send → 共享内存环形队列,消费者经 SubSocket.receive → SubMaster.update_msgs 到达应用。
函数级索引
下表按组件列出运行语义与容易误解的边界。
| 函数 / 组件 | 运行语义 | 容易误解的边界 |
|---|---|---|
msgq_new_queue() |
打开并映射主题文件,设置头部与环形数据区指针 | 主题名/前缀区分队列,不负责验证发布者身份或消息 schema |
msgq_init_publisher() |
生成本地 UID 并写共享 write_uid,同时重置读者计数和槽位 |
新发布者连接时替换共享发布者标识;不是拒绝第二个发布者接入 |
msgq_msg_send() |
旧本地 UID 与共享 UID 不一致时返回 EADDRINUSE;否则写长度/载荷、推进写指针并唤醒读者 |
冲突在旧发布者下次发送时显露;容量要求至少容纳三条当前尺寸消息 |
msgq_init_subscriber() |
分配读者槽;超出 15 个槽位时清空并逐出原有读者,再让读者重新连接 | 读者槽位不是可无限增长的服务注册表,满槽会造成读者重置和帧丢失 |
msgq_msg_recv() |
读者被逐出/缓冲无效时重置到当前写指针;read_conflate 时跳过中间消息,只拷贝最新 |
重置不回放旧帧,合并也不报告被跳过的消息数量 |
msgq_poll() |
检查可读状态并休眠等待唤醒或超时 | poll 就绪不保证随后一定能读到消息;读取端有重试路径 |
msgq_all_readers_updated() |
检查现存有效读者指针是否追上写指针,且至少有一名读者 | 它不是业务处理完成或落盘确认;失效槽位不参与「未读」判断 |
SubMaster.update() |
poll 指定主题,其他主题非阻塞读取;每次调用推进本地 frame | 没有跨主题原子快照,某主题当前循环可沿用旧值 |
SubMaster.all_checks() |
汇总 alive、平均频率与消息 valid |
只查 sm.valid[主题] 不等于执行全部健康检查 |
覆盖、逐出与健康位
msgq_msg_send() 写入前检查消息尺寸与队列容量;回绕或写入覆盖慢读者位置时标记该读者无效。msgq_msg_ready() 和接收函数看到失效读者后调用 msgq_reset_reader(),将读指针放到当前写指针。这保证不会把覆盖的字节当新消息,但意味着已跳过的历史帧不可补取。
| 消费者行为 | 语义 | 边界 |
|---|---|---|
SubMaster 合并读取 |
对所有主题使用 conflate=True,即使队列未满也只保留最新一条 |
需完整帧序列的记录器或分析工具不能复制这种消费模式 |
alive 计算 |
正频率主题按本地单调时钟和服务频率的十倍周期另行计算 | 零频/on-demand 服务初始被视为 alive/valid |
update_msgs() |
仅收到新消息时更新 data、valid、接收时间和 logMonoTime |
没收到时 updated=False,保留最后数据与最后 valid |
all_checks() |
才联合检查健康 | 调用方若只用最后 valid,旧数据可能继续被选中 |
PubSocket.connect() 调用 msgq_init_publisher() 改写共享 UID;PubSocket.send() 的 Python 包装把后续 EADDRINUSE 转成 MultiplePublishersError。因此「单一发布者」是当前写入者冲突检测,并非在第二个进程连接前核查授权、阻止冒名或保护旧发布者连续运行。
健康位与读取推进是两套机制:msgq_poll() 检查可读状态并休眠等待唤醒或超时,但 poll 就绪不保证随后一定能读到消息,读取端有重试路径;msgq_all_readers_updated() 只检查现存有效读者指针是否追上写指针且至少有一名读者,它不是业务处理完成或落盘确认,失效槽位也不参与「未读」判断。
因此消费侧不能只看最后 valid:某主题在当前循环可能沿用旧值,而 all_checks() 才联合检查 alive、平均频率与消息 valid。模拟模式还放宽了部分时序检查,不能以仿真健康位直接替代实板时序证据。
插件化与验证边界
以 msgq 承载插件候选仍可行,但必须在受信任进程/OS 层限制谁能创建权威主题发布者;候选各用自己的主题,固定仲裁器独占 carControl 等权威输出。消息接口应包含版本、来源、单调时间、明确 TTL、状态位与序号;仲裁器记录来源切换和超时降级。
队列大小/覆盖预算、十五读者上限、最坏消息尺寸、发布者重启时的读者复位,都应在目标硬件上压力测试。wait_for_readers_to_update() 只证明读取指针条件,不证明候选被理解、执行或持久记录;安全关键命令和审计日志需要独立的确认/落盘机制,不能把实时队列当事务通道。现有 msgq_tests.cc 和 test_messaging.py 覆盖一般/合并读取、慢读者和回绕等单元行为;本轮未运行,发布者竞争、读者槽位耗尽、进程 crash/restart、权限隔离和负载极限仍需专门故障注入与板上测量。
常见问题
「单一发布者」是一种安全认证吗?
不是。它是当前写入者冲突检测:第二个 PubSocket.connect() 会改写共享 UID 并重置读者槽位,旧发布者到下次 send() 才收到 EADDRINUSE;连接前不核查授权,也不保护旧发布者连续运行。
SubMaster 会保留完整帧序列吗?
不会。SubMaster 对所有主题使用 conflate=True,队列未满也只保留最新一条;需要完整序列的记录器或分析工具不能复制这种消费模式。
alive 和 valid 是一回事吗?
不是。update_msgs() 只在收到新消息时更新 valid,没收到时保留最后 valid;而 alive 按本地单调时钟与服务频率的十倍周期另行计算。调用方若只用最后 valid,旧数据可能继续被选中。
队列满了会补取历史帧吗?
不会。回绕覆盖或读者被逐出时,msgq_reset_reader() 把读指针放到当前写指针,已跳过的历史帧不可补取;合并读取也不报告被跳过的消息数量。
延伸阅读
- VisionIPC 缓冲区契约:图像主数据为什么不走 msgq —— 图像链路与消息队列的分工
- 系统底座:进程、共享内存与运行时原语 —— msgq 所在的底座层次
- 车载信号为什么会说谎:TTL、源时间与 500 ms 新鲜度阈值 —— 陈旧数据为何会继续被选中
- UDS 诊断工具为什么不能随手调:只读、会话与写 ECU 的分权设计 —— 发布权与安全关键命令的分权对照
参考资料
- 项目 02 ——
openpilot-research/MSGQ_TRANSPORT_BOUNDARIES_DEEP_DIVE.md(共享内存队列的发布者替换、覆盖、合并与读者复位边界) - 项目 02 ——
openpilot-research/SYSTEM_FOUNDATION_DEEP_DIVE.md(系统底座与图像主数据的独立通道,作为本文的补充阅读)
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复