• 欢迎访问少将全栈,学会感恩,乐于付出,珍惜缘份,成就彼此、推荐使用最新版火狐浏览器和Chrome浏览器访问本网站。
  • 吐槽,投稿,删稿,交个朋友
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏少将全栈吧

Microduck 的七守护进程设计:为什么一只 800g 的鸭子需要七个进程才安全

Build in Public admin 1小时前 6次浏览 已收录 扫描二维码

Microduck 的七守护进程设计:为什么一只 800g 的鸭子需要七个进程才安全

你大概会觉得,一只 25 厘米高、800 克重的机器人鸭子,跑个单进程够了。读几个传感器,算个控制律,发个舵机指令,一个 while 循环搞定。Microduck 的工程师不这么想。他们在 RK3566 主控上跑了整整七个守护进程,每个都有明确的职责边界和存活理由。这不是过度工程,而是用进程隔离换来的安全冗余。

七个进程,各管一摊

Microduck 的软件架构核心是七个 systemd 管理的守护进程:robotd、configd、updaterd、btd、padd、mediad、tofd。它们之间没有共享内存,没有消息总线,只有 Unix domain socket 上的 JSON-RPC 2.0 通信。

robotd 是唯一能碰电机的进程。15 个 XL330 舵机和 IMU 共享一条串行总线,50Hz 的控制循环独占它。其他进程想控制机器人,只能发”意图”——走多快、看哪里、站起来——robotd 内部的安全层决定这些意图能不能执行。这个设计把”什么指令能伤害机器人”这个问题的答案收窄到了一个进程里。

configd 管 WiFi、机器人名字、配对 PIN、手柄绑定。updaterd 管更新:签名校验、安装、健康检查、回滚。btd 是蓝牙通道,把手机 App 的 API 调用转发到对应的服务进程。padd 读手柄输入。mediad 管摄像头和音频,通过 WebRTC 推流。tofd 管头部的ToF深度传感器,一个 8×8 深度矩阵。

为什么不让 robotd 干所有事

最直觉的问题:为什么不把配置、蓝牙、更新都塞进 robotd?答案是一个场景:robotd 崩了。

一只鸭子摔了一下,控制循环里的某个 panic 被触发了,robotd 进了 crash-loop。这时候用户最需要做什么?更新固件修复 bug、重新配 WiFi、检查日志。如果这些功能都住在 robotd 里,鸭子就彻底失联了——你连修复的入口都没有。

所以 configd、updaterd、btd 三个进程被设计为不依赖 robotd。没有 systemd 的 Requires= 或 After= robotd,没有硬 IPC 依赖。所有对 robotd 的调用都是可选的、带超时的——socket 关了就关了,不 hang、不 panic、不拒绝服务。btd 照样能告诉手机”鸭子挂了,版本 X,上次更新失败”,照样能触发回滚。

这是架构文档里最强调的不变量:btd、configd 和 updaterd 必须在 robotd 死掉的情况下仍然完全可用。它们是恢复路径,而恢复路径在机器人坏掉时才真正需要。

Unix socket 而不是 TCP,更不是 D-Bus

进程间通信用 Unix domain socket 而非 localhost TCP,这个选择不是性能考虑,是安全考虑。

文件系统权限是免费的授权。一个 socket 设成 0660 模式加专用组,只有特定进程能连。TCP 端口对同一台机器上的所有进程和用户开放,要达到同等安全级别得自己写一套认证层。更关键的是 SO_PEERCRED——内核直接告诉你调用者的 uid/gid/pid。Microduck 用这个做两层控制:socket 的组决定谁能跟守护进程说话,allow_uids/allow_gids 决定谁能改机器人。只读调用故意不加门禁,支持工程师能检查一个自己无权修改的鸭子。

D-Bus 被排除了。BlueZ 已经把 D-Bus 拉进来了,系统里确实有。但同样的消息类型还要走蓝牙和 WebRTC,D-Bus 类型系统在那边用不了。一次定义、多种传输才是目标,JSON 让这件事几乎零成本。

控制面和数据面分开

Microduck 把进程间流量分成两类。控制面传命令、配置、状态、感知事件——几十字节,100Hz 以内。数据面传视频帧——640×480 RGB@30fps 大约 27MB/s。

数据面永远不跨 socket。robotd 不需要视频帧,它需要的是从视频里提取的特征:”球在 (x,y)”、”检测到人”。mediad 在摄像头旁边跑推理,只发布几十字节的特征数据。把帧传给 robotd 再让它做视觉会浪费大量内存带宽。

如果帧真的必须跨进程,最后一招是共享内存加 dmabuf 环形缓冲区,socket 只传”第 N 帧在偏移 X 准备好了”这种通知。架构文档把这种方案标注为”最后手段”,能用特征就不碰帧。

tofd 为什么独立于 mediad

ToF 深度传感器看起来跟摄像头关系密切,为什么不放进 mediad?架构文档给了具体理由。

VL53L5/8CX 传感器初始化时要通过 I²C 上传约 90KB 固件,耗时数秒。这个 I²C 总线还跟音频编解码器共享。大部分鸭子根本没装 ToF 传感器。一个重试循环去等固件上传,不该出现在管电机的进程里——这是 tofd 独立于 robotd 的理由。它也不属于 mediad——深度是总线上的传感器,不是媒体管线的一部分。tofd 在有摄像头之前就有用。它发布数据,不读任何人的数据,是七个进程里唯一的”只发不收”角色。

更新系统:签名、原子替换、健康门

updaterd 的更新流程是整个架构的安全底线。一次更新走六步:拉取 release tarball → 校验 minisign 签名 → 解压到 /opt/robot/daemon/releases// → 移动 current 符号链接 → 重启服务单元 → 问 robotd 健康吗。不健康就把旧版本放回去。

这个流程里有个微妙的设计:updaterd 和 btd 自己也住在 daemon artifact 里,所以”重启所有服务”会把 updaterd 自己杀掉。on_apply 重启 release 里的所有单元,除了 updaterd 和 btd。这两个通过 systemd transient timer 在回复发出 5 秒后延迟重启——足够让 IPC 回复到达客户端,又不会让旧二进制跑太久。

更新粒度选了应用级而非 A/B 镜像。RAUC 或 Mender 那种双分区方案对这个场景来说太重了——变的只有守护进程二进制和 ML 模型,操作系统是静态的。版本化目录加原子符号链接替换加健康门,不需要分区管理。

两个独立更新通道

daemon 和 model 走不同的更新通道,各自有版本线和 manifest。daemon 从 GitHub Releases 拉,model 从 Hugging Face Hub 拉。两者节奏不同——daemon 预期每 2-3 周一个 client-facing release,model 按自己的节奏走。

model 更新不需要重启整个 daemon。守护进程在运行时重新加载 ONNX 策略,不需要把 50Hz 控制循环停下来。这意味着换步态策略时鸭子是连续站着的,不是关机再开机。

这套架构对复刻者的启示

如果你在做类似的小型机器人,Microduck 的七进程架构有几个值得借鉴的点。

第一,把”能伤害机器人的代码”隔离到单一进程。其他所有功能——配置、更新、蓝牙——都不应该在控制循环崩溃时跟着死。第二,Unix socket 比 TCP 安全,SO_PEERCRED 是免费的身份验证。第三,感知和控制在进程层面分开,中间传特征不传帧。第四,更新系统不需要 A/B 分区,版本化目录加健康门就够了,但 updaterd 必须独立于被更新的东西。

这些不是教条。每个设计决策背后都有一个具体的故障场景:robotd 崩了怎么修、感知进程 OOM 怎么不影响步态、更新把二进制搞坏了怎么回滚。七进程看起来多,但每个进程的存在理由都能用一个”如果这个进程不存在会怎样”的问题来回答。


数据来源:pollen-robotics/microduck GitHub 仓库 docs/design/architecture.md(七守护进程架构设计、IPC 协议、状态所有权),docs/design/updater-design.md(更新系统 A/B 回滚、签名校验、健康门机制),README.md(系统总览),AGENTS.md(架构不变量)。

喜欢 (0)赏
[🍬谢谢你请我吃糖果🍬🍬~]
分享 (0)
关于作者:
少将,关注Web全栈开发、项目管理,持续不断的学习、努力成为一个更棒的开发,做最好的自己,让世界因你不同。