deleter 空间回收:书签只延后删除与五名额占用

作者:

在

结论:user.preserve 书签只把目录往后排、并不使其成为不可删除状态;get_preserved_segments() 在解析段号之前先取「倒序前五个带标记目录」,若带标记的 boot/crash 或非段目录占了名额,较早的真实书签段就不会进入延后集合。容量阈值用严格小于:可用字节 < 5 GiB 或可用比例 < 10%;两次 statvfs 读取失败时各自返回高于阈值的默认值,因此缺测本身不触发删除。以下为固定快照 c8fb906 的静态阅读加假目录/临时文件系统探针,未删除真实路段、未填盘、未做车端测试。

函数与数据依赖

回收逻辑集中在 system/loggerd/deleter.py,它与容量配置、目录枚举和上传器存在共享依赖。下表的语义以源码事实为准。

函数 源码语义
has_preserve_xattr() 查询某目录 user.preserve == b'1';走进程内 xattr_cache,跨进程变更可能不可见
get_preserved_segments() 从名称排序列表尾部向前找最多五个带标记目录,解析末个 -- 后的段号,将该段及前两段加入延后集合
get_available_bytes() / get_available_percent() 各自独立调用 statvfs();OSError 时按调用方默认值返回,两个读数不是原子快照
listdir_by_creation() 只列根目录下被判定为目录的项目,再按路段名称排序;函数名不表示按 inode 创建时间
deleter_step() 空间低于任一阈值后枚举、分组排序、检查 .lock,尝试 shutil.rmtree();成功即返回,单次最多删一个目录
deleter_thread() / main() 空间不足时每 0.1 秒重试,正常每 30 秒;主入口不另行处理步骤异常

优先级与保留语义

deleter_step() 使用 sorted(dirs, key=lambda d: (d in DELETE_LAST, d in preserved_dirs))。由于 False < True 且排序稳定,实际分组为「普通未保留、普通保留、boot/crash 未保留、boot/crash 保留」,同组内维持 listdir_by_creation() 的名称顺序。

关键边界是:user.preserve 只延后删除。当普通路段删完而空间仍不足时,书签段、boot 与 crash 仍可被删。这里也不检查 user.upload 或远端对象存储回执,因此「待上传」与「已书签」都不是不可删除状态。

另一个易被忽略的点:get_preserved_segments() 的 enumerate(filter(has_preserve_xattr, reversed(dirs))) 在解析路段格式之前就计算五个名额。假目录探针用两个非段目录占位,确认较早的真实书签段不进入集合——这不是实测删除事故。

每个候选先用 os.listdir(delete_path) 检查任意 .lock,有锁即跳过。但 uploader.main() 启动时又会清理日志根下所有锁文件,因此锁的存在并非跨进程可靠 lease。若 loggerd 仍在写而锁被误清,静态看 deleter 可能把该目录视为可删;实际触发取决于进程调度和空间状态。shutil.rmtree() 仅在无锁分支执行,成功后返回 (True, path),不能证明释放字节数达到目标,也没有按本轮删除内容重新测量容量。

缺测、异常与循环

阈值分别使用严格小于:可用字节 < 5 GiB,或可用比例 < 10%。两个 statvfs 读取失败时各自默认高于阈值,所以缺测本身不触发删除;若仅一次失败,另一次仍可能触发。由 f_blocks == 0 引起的除零没有被 config.py 的 OSError 捕获。

场景 实际行为与边界
空间不足但无目录可删 返回 (True, None),外层每 0.1 秒再尝试,不转入较长间隔
rmtree() 抛 OSError 在循环内捕获并转向下一目录;但书签 xattr 过滤与每项 os.listdir(delete_path) 位于该 try 之外
listdir() 抛 FileNotFoundError 直接穿透 deleter_step();枚举后目录被并发删除或 xattr 读取异常时,错误可穿透 deleter_thread()
目录符号链接 listdir_by_creation() 会把它列为候选,但 shutil.rmtree() 拒绝直接删除该链接,异常被记录后继续删普通段;链接及目标内容未动

本机临时目录探针只证明函数级行为,未复现真实跨进程调度、Pipe 或断电。目标车端的 Python、文件系统与并发行为仍需另测,不能仅据路径字符串认定候选为受信任普通目录。

现有测试与插件化验收

test_deleter.py 用替换的 statvfs 和临时路段覆盖阈值、普通/书签/boot/crash 删除顺序及锁跳过;未覆盖缺测、f_blocks=0、异常目录、跨进程 xattr 陈旧、被其他进程并发删除、活动锁被清理或删除失败后的持续重试。

四项原函数假目录探针可用 python3 -m unittest probes.test_deleter_preserve_failure_contract -q 复现书签名额、锁跳过、删除失败续试与列目录异常;两项临时文件系统探针执行原 listdir_by_creation()、书签分组与 deleter_step(),使用真实 os.listdir()/shutil.rmtree() 仅操作自动清理的临时目录:书签段及 boot 确实排在普通段之后,目录符号链接本机删不掉但不会阻断后续普通段删除。容量与 xattr 来源仍是假依赖,不覆盖运行中的 loggerd、真实日志根、断电或目标板文件系统。

面向插件化,应由存储服务发放段级写入 lease,并把「段已关闭、远端已确认、书签保留期限、空间紧急覆盖」做成明确状态;删除前重新验证目录身份与 lease,异常统一记录并维持监督健康。容量插件须报告挂载身份、可用字节/比例、采样时间与缺测,不把默认值伪装成真实容量。

常见问题

带 user.preserve 的书签段会被删除吗?

会。user.preserve 只延后删除;当普通路段删完、空间仍不足时,书签段、boot 与 crash 仍可被删。系统也不检查 user.upload 或远端回执。

为什么较早的书签段可能没被保护?

因为 get_preserved_segments() 在解析段号之前先取倒序前五个带标记目录;若 boot、crash 或其他非段目录占用了名额,它们随后因无合法段号被跳过,而较早的真实书签段不再进入延后集合。

删除阈值是多少,缺测会怎样?

阈值是严格小于:可用字节 < 5 GiB 或可用比例 < 10%。两次 statvfs 失败时各自返回高于阈值的默认值,缺测本身不触发删除;仅一次失败时另一次仍可能触发。

锁能防止删到正在写入的段吗?

不能保证。deleter 只检查目录内是否出现 .lock,而 uploader.main() 启动会清理日志根下所有锁文件,因此锁不是跨进程可靠 lease。

延伸阅读

参考资料

  • 项目 02 —— openpilot-research/DELETER_PRESERVE_ORDER_FAILURE_CONTRACT_DEEP_DIVE.md(目录级回收的优先级、书签名额占用与失败边界)
  • 项目 02 —— openpilot-research/LOG_STORAGE_CAPACITY_FAILOPEN_CONTRACT_DEEP_DIVE.md(容量测量与缺测默认值,作为本文的补充阅读)

说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

更新记录

首次发布 2026-10-11 最近核对:2026-10-11

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注