结论:openpilot 的代码更新从「候选版本」走到「启动版本」要跨过三个独立标记:staging 目录、finalized 定稿、以及 .overlay_consistent 一致标记。它们各自的含义不同——一致标记只是「定稿流程走完」的本地记录,不是代码签名证明;UI 的「可安装」与「更新进程退出」也都不等于新代码已经激活。真正切换发生在启动脚本移动目录并 exec 重启的那一刻。以下为固定源码快照 c8fb906 的静态分析与假依赖探针结论。
生命周期与关键阶段
updated 被配置为非 PC 的驻车进程;main() 在 DisableUpdates 时退出,随后取得排他锁、初始化参数与等待器。初次循环或系统时间无效会等待 60 秒;之后检查远端,下载成功后长间隔等待,失败后较短间隔重试。需要强调:更新进程退出或 UI 显示「可安装」都不等于已经激活新代码。
人工请求走信号而非持久任务:UI 给 updater 发 SIGUSR1 表示只检查、SIGHUP 请求下载,请求由内存状态消费,不是持久队列。远端检查用 git ls-remote 获取分支 SHA,跳过旧 release2 分支,tizi/mici 对 release3 目标分支有设备映射。
| 阶段 | 关键入口 | 实际行为 |
|---|---|---|
| 人工请求 | WaitTimeHelper、SoftwareLayout._on_download_update() |
UI 发 SIGUSR1 只检查、SIGHUP 请求下载;由内存状态消费,非持久队列。 |
| overlay 初始化 | init_overlay() |
依据 .overlay_init 与 .git 修改时间决定复用或重建 staging;重建会删除整个 STAGING_ROOT。 |
| 远端检查 | Updater.check_for_update()、target_branch |
用 git ls-remote 获取分支 SHA,跳过旧 release2;设备型号映射 release3 目标分支。 |
| 下载与系统维护 | Updater.fetch_update()、handle_agnos_update() |
先清一致标记,再 fetch/强制 checkout/reset/clean 与递归子模块更新;AGNOS 目标版本不同时写目标 slot。 |
| finalized 定稿 | finalize_update()、set_consistent_flag() |
把 merged 拷到独立 finalized,重置主仓/子模块并尝试 Git/LFS 清理,最后写 .overlay_consistent。 |
| 状态发布 | Updater.set_params() |
写失败计数、分支、版本/说明、下载/安装可用标志与离线告警。 |
| 激活 | launch_chffrplus.sh |
检查标记与 mtime,把旧目录移作备份、把 finalized 移入运行目录,exec 重启脚本。 |
staging:overlay 的复用与重建
init_overlay() 依据 .overlay_init 与 .git 修改时间决定复用还是重建 staging。重建前会清 UpdateAvailable 和一致标记、卸载旧 merged,再创建 OverlayFS 并记录 GitDiff;重建会删除整个 STAGING_ROOT。fetch_update() 先清一致标记,再在 merged 中 fetch、强制 checkout/reset/clean 和递归子模块更新。假命令探针确认了这一阶段顺序,并提示破坏性 Git 命令不应在研究工作区执行。
三个标记分别证明什么
第一个标记是 staging 的 .overlay_init,表示本地 overlay 已初始化;启动脚本用它判断是否复用。第二个是 finalized 目录,表示 merged 已被复制成独立候选,且主仓与子模块被重置。第三个是 .overlay_consistent,由 finalize_update() 最后写入;隔离探针确认:git gc 抛 CalledProcessError 时 LFS prune 被跳过但仍置标记,LFS prune 自身抛错也仍置标记;主仓 reset 抛错则函数离开且不置标记。也就是说,一致标记不等于复制完整性、fsync、签名或断电恢复已通过。
| 标记或状态 | 计算依据 | 不保证什么 |
|---|---|---|
.overlay_init |
本地 overlay 是否初始化 | 不代表候选代码有效或可启动。 |
finalized + .overlay_consistent |
定稿流程是否走完 | 不校验 finalized 提交签名,清理失败仍可能置位。 |
UpdateAvailable |
finalized 一致标记 + 当前版本差异 + 目标分支一致 | 探针显示远端分支表为空时仍可能为真,语义是「可拉取」。 |
UpdaterFetchAvailable |
分支表非空后比较 merged 与远端 SHA/分支 | 表示「可拉取」,不是「可启动」。 |
激活与失败路径
启动脚本 launch_chffrplus.sh 在启动时检查当前目录 .overlay_init、Git 目录 mtime、finalized 一致标记,且不能已有 old_openpilot;然后把旧目录移到 staging 备份、把 finalized 移入运行目录并 exec 重启脚本。UI 的安装按钮只是写 DoReboot,不是就地切换。当启动脚本发现 old_openpilot 已存在时只打印「backup found, not updating」,源码中的 TODO 明确尚未实现自动恢复。
失败路径同样需要看清:main() 对 CalledProcessError 与其他异常记录错误并删除 .overlay_init,下一轮倾向重建 overlay;失败次数在进程内重新从 0 开始(源码有加载 Params 的 TODO)。成功检查但因计费网络跳过 fetch 时,循环仍把失败次数归零并更新检查时间,只有实际 fetch 成功才写 UpdaterLastFetchTime。因此不能把目录备份等同于经验证的断电回滚方案:若新版本能启动到 init_overlay(),重建 STAGING_ROOT 会清掉旧备份;若新版本在此之前无法启动,当前脚本没有自动回滚分支。
对自制硬件的约束
源文档给出一组建议:插件包、代码、模型、DBC 与独立安全固件需要同一个可验证兼容性集合,仅依赖 .overlay_consistent 或 Git 分支名不足以证明可上车;下载、完整性验证、激活、启动成功确认与回滚应是分离的受信任阶段,控制相关插件只能在驻车维护窗口替换且失败后保留可启动的旧版本。
此外,UI 的 UpdateAvailable、下载状态与安装按钮只是 Params 视图;硬件平台应另有启动槽健康、镜像签名、反回滚与 watchdog 证据,不能据页面状态宣称部署完成。设计断电、存储满、子模块缺失、镜像版本不匹配等注入测试,验证「旧版本仍能启动」,是当前源码没有提供的验证结果。
常见问题
UI 显示「可安装」是不是就能直接用了?
不是。UI 的安装按钮只写 DoReboot,真正切换由启动脚本移动目录并 exec 重启完成。更新进程退出或显示「可安装」都不等于新代码已激活。
.overlay_consistent 能证明代码签名吗?
不能。它由定稿流程最后写入,探针显示清理命令失败仍会置标记,主仓 reset 失败才不置位;源码没有单独校验 finalized 提交的签名。它不是代码签名证明。
升级失败会自动回滚到旧版本吗?
依源码没有自动回滚实现。发现 old_openpilot 时只打印「backup found, not updating」,源码 TODO 明确尚未实现;若新版本无法启动,启动脚本也没有自动回滚分支。
UpdateAvailable 和 UpdaterFetchAvailable 有何区别?
UpdateAvailable 依据 finalized 一致标记、当前版本差异与目标分支一致性计算;UpdaterFetchAvailable 在分支表非空后比较 merged 与远端 SHA/分支,语义是「可拉取」。两者都不是「可启动」的证明。
延伸阅读
- AGNOS 平台更新:A/B 分区与系统镜像怎么切 —— 代码树之外的系统层更新路径。
- Ed25519 签名加 96 字节事务记录:让 MCU 升级不怕中途断电 —— 与 Git 一致标记不同的签名与回滚思路。
- panda bootstub 的签名与刷写边界 —— 固件层的签名与刷写门槛。
- 软件更新页与开发者设置的参数链 —— 安装按钮到底是什么请求。
参考资料
- 项目 02 ——
openpilot-research/UPDATER_STAGING_ACTIVATION_DEEP_DIVE.md(updated 生命周期、overlay staging、三个标记、激活与失败路径)。 - 项目 02 ——
openpilot-research/同目录探针记录:定稿、远端分支与拉取阶段各四项假依赖隔离探针结论。
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复