Lazy loaded image
Words 0Read Time 1 min
Invalid Date

核心判断

os.replace 的原子性 不是 Python 给的,是 POSIX rename(2) 系统调用给的。Python 只是个壳,真正的保证来自 内核 + 文件系统对目录项(dentry)更新的事务化处理
理解这件事的关键:"原子"不是指写入数据原子,而是指"目标路径所指向的 inode" 这个映射的切换是原子的

一、先拆基本面:文件名 ≠ 文件

在 Unix/Linux 里:
  • inode = 真正的文件实体(数据 + 元数据)
  • dentry / 目录项 = 一条 "名字 -> inode 编号" 的映射,存在父目录里
  • 一个 inode 可以被多个名字指向(硬链接),引用计数为 0 才真删
所以 os.replace(tmp, target) 干的事不是"把 tmp 的数据搬到 target",而是:
修改父目录里那条 target -> ??? 的映射,让它从指向旧 inode,变成指向 tmp 对应的 inode,然后释放旧 inode。
整个过程没有任何数据复制。这就是为什么它快,也是为什么它能原子。

二、原子性的真正来源:POSIX rename(2) 契约

POSIX 标准明确规定:
If the link named by the new argument exists, it shall be removed and old renamed to new. In this case, a link named new shall remain visible to other processes throughout the renaming operation and refer either to the file referred to by new or old before the operation began.
翻译成人话:任何时刻,从任何进程看 target 这个路径,要么是旧 inode,要么是新 inode,绝不会出现"路径不存在"或"路径存在但指向半截内容"的中间态
Python 文档对 os.replace 的承诺也是基于这个:
  • 在 POSIX 上,直接 wrap rename(2)
  • 在 Windows 上,Python 3.3+ 用 MoveFileExW + MOVEFILE_REPLACE_EXISTING,行为对齐成"原子替换"(早期 Windows 的 rename 在目标存在时会失败,这才有了 replace 这个独立 API)

三、内核里到底发生了什么

以 ext4 为例,rename 在内核里的关键流程:
关键点有三个:
  1. 整个目录项变更被打包成一个 journal 事务
ext4/xfs/btrfs 都把 rename 当作单一元数据事务。事务要么完整提交,要么完整回滚 —— 这是日志文件系统的最基本保证。
  1. 父目录 inode 锁串行化了并发读者
其他进程读 target 时要走 dentry 查找,会被 VFS 锁挡住,看到的只能是事务前或事务后的状态。
  1. 掉电场景由 journal replay 兜底
重启后 fs 挂载时,kernel 扫 journal:
  • 事务已 commit → replay,呈现新状态
  • 事务未 commit → 丢弃,呈现旧状态
  • 不存在"半 commit" —— 这是 journal 设计的核心不变量(commit record 自身带 checksum)

四、原子性的边界 —— 这才是真正容易踩坑的地方

os.replace 的原子保证有 三个隐含前提,缺一个都会破功:
前提
违反后果
对策
tmp 和 target 必须在 同一文件系统/同一挂载点
跨 fs 时退化为 copy + unlink,完全不原子
tmp 必须建在 target 的同目录
rename 只保证 目录项切换 原子,不保证 tmp 文件内容已落盘
rename 成功但内容还在 page cache,掉电后 target 指向一个空 inode
rename 前必须 fsync(tmp_fd)
rename 改的是父目录,父目录本身的元数据也需要落盘
rename 在内存生效,目录项变更未刷,掉电后整个文件可能"消失"
rename 后必须 fsync(dir_fd)
这就是为什么"原子写入四件套"是 tmp → fsync(file) → os.replace → fsync(dir)少任何一步,原子性都只剩半张嘴

五、一句话总结

os.replace 的原子性 = POSIX rename(2) 契约 + 文件系统对目录项变更的 journal 事务化 + VFS 锁串行化并发可见性
它保证的是 "target 这条名字 → inode 的映射切换"是原子的不保证内容已经落盘。所以在嵌入式掉电场景里,os.replace必要不充分 条件,必须配 fsync(file) + fsync(dir) 才能闭环。

上一篇
Data Structure and Algorithm
下一篇
用面试拷问嵌入式技术栈

Comments
Loading...