如何原子性替换文件内容(避免半写状态)

8次阅读

os.replace() 是最安全的原子文件替换方法,它在 POSIX 和 windows 上均保证路径切换不可分割,但要求源目标同文件系统,临时文件须与目标同目录且用 mkstemp 生成。

如何原子性替换文件内容(避免半写状态)

os.replace() 替换文件最安全

python 3.3+ 中,os.replace() 是原子性替换的首选。它在 POSIX 系统上调用 rename(2),在 windows 上调用 MoveFileExW(带 MOVEFILE_REPLACE_EXISTING),两者均保证「旧文件消失」和「新文件出现」是不可分割的操作——不会出现读到半写内容的情况。

关键点:

  • 目标路径必须与源路径在同一个文件系统(同磁盘、同挂载点),否则会抛出 OSError: [errno 18] Invalid cross-device link
  • 不要用 shutil.move():它在跨设备时会退化为复制 + 删除,完全不原子
  • 写入临时文件时,务必用 os.path.dirname(target_path) 作为临时目录,避免跨文件系统(比如临时文件默认写到 /tmp,而目标在 /home

临时文件必须和目标同目录且加随机后缀

临时文件若不在目标目录下,os.replace() 会失败;若后缀固定(如 .tmp),并发写入可能冲突或被误删。

正确做法:

  • tempfile.mkstemp(dir=os.path.dirname(target_path), suffix='.tmp') 创建临时文件(返回句柄和路径)
  • 写完后立即 os.close(fd),再调用 os.replace(tmp_path, target_path)
  • 避免用 open(..., 'w') 直接创建临时路径——没有原子性保障,且可能因权限/SElinux 导致写入失败

Linux 下用 mv -T 命令行也原子

终端场景下,mv -T src dstgnu coreutils ≥8.24)等价于 os.replace(),可安全用于脚本。

注意:

  • 不加 -Tmv src dstdst 是目录时会把 src 移进去,不是替换,极易出错
  • cp src dst && rm src 完全非原子:中间存在 dst 已覆盖但 src 未删的窗口,且 cp 本身也不保证写入顺序
  • 如果目标路径含符号链接,mv -T 作用于链接指向的目标,不是链接本身

Windows 上要注意硬链接和删除语义

NTFS 支持硬链接,但 os.replace() 在 Windows 上的行为是「删除原路径、将新路径重命名过去」,不涉及硬链接更新。这意味着:

  • 若原文件有其他硬链接,替换后那些链接仍指向旧内容(因为旧 inode 未被真正回收,直到所有引用消失)
  • 若程序正打开原文件读取,Windows 默认允许删除(unix 不允许),但读取进程可能继续看到旧数据,直到关闭句柄
  • 想彻底阻断旧内容访问,需确保无进程打开该文件,或改用内存映射 + 写时复制方案(复杂度陡增)

原子性只管「路径切换」这一瞬间,不解决「旧内容是否还在磁盘上」「其他进程是否缓存了旧数据」这类问题。实际部署时,得配合进程重启、缓存失效、或服务优雅停机来闭环。

text=ZqhQzanResources