
本文介绍在 posix 系统下,使用 python 标准库原子化创建新文件的方法:当目标文件已存在时,先将其安全重命名备份,再创建新文件,全程规避 toctou(time-of-check-to-time-of-use)竞态问题。
本文介绍在 posix 系统下,使用 python 标准库原子化创建新文件的方法:当目标文件已存在时,先将其安全重命名备份,再创建新文件,全程规避 toctou(time-of-check-to-time-of-use)竞态问题。
在多进程或高并发场景中,简单地先检查文件是否存在、再删除/移动、最后创建(即 os.path.exists() + os.rename() + open(..., 'w'))会引发严重的竞态条件(TOCTOU):两个进程几乎同时检测到文件不存在,随后都尝试创建,导致数据丢失或覆盖。真正的安全方案必须依赖原子性系统调用——而 Python 标准库恰好提供了关键原语:open(..., "x")(独占创建)和 os.rename()(POSIX 下对同一文件系统是原子的)。
以下是一个健壮、可重入、纯标准库实现:
import os
import time
def create_with_backup(filepath, backup_suffix=".bak"):
"""
原子化创建新文件:若 filepath 已存在,则将其重命名为 filepath + backup_suffix,
然后创建空的新文件。全程避免 TOCTOU。
:param filepath: 目标文件路径
:param backup_suffix: 备份文件后缀(默认 '.bak')
:return: 成功时返回 True;失败时抛出 OSError 或其子类
"""
backup_path = filepath + backup_suffix
while True:
try:
# 尝试独占创建 —— 仅当文件不存在时成功(原子!)
with open(filepath, "x") as f:
f.write("") # 可选:写入初始内容
return True
except FileExistsError:
# 文件已存在:尝试原子重命名它为备份
try:
os.rename(filepath, backup_path)
# 重命名成功,继续下一轮尝试创建
continue
except OSError as e:
if e.errno == os.errno.ENOENT:
# ENOENT:文件被其他进程抢先删除或移动 → 竞争者已处理,重新尝试创建
continue
elif e.errno == os.errno.EEXIST:
# 备份路径已被占用?加时间戳避免冲突(可选增强)
timestamp = int(time.time() * 1e6)
backup_path = f"{filepath}.bak.{timestamp}"
continue
else:
# 其他错误(如权限不足、跨设备等),不可恢复,直接抛出
raise
关键设计说明:
- ✅
"x"模式是核心:open(..., "x")在底层调用O_CREAT | O_EXCL,确保“创建且仅当不存在”,POSIX 保证该操作原子性,无竞态窗口。 - ✅
os.rename()是安全的备份手段:在同一挂载点内,rename()是原子的,不会出现“只移动一半”的中间状态;且它本身不关心源文件内容,仅修改目录项。 - ⚠️ 注意跨文件系统限制:
os.rename()跨设备(如不同磁盘分区)会失败(EXDEV)。本方案默认假设 POSIX 同一文件系统。如需跨设备支持,需回退到shutil.move()(非原子),此时无法完全规避 TOCTOU,应通过外部锁(如fcntl)协调。 - ? 循环逻辑处理竞争:当
rename()遇到ENOENT(文件消失),说明另一进程已处理该文件,当前进程应立即重试open("x");当遇到EEXIST(备份名冲突),则动态生成带时间戳的备份名,避免死锁。
使用示例:
create_with_backup("/tmp/config.json")
# 若 /tmp/config.json 存在 → 重命名为 /tmp/config.json.bak → 创建新文件
# 若 /tmp/config.json.bak 已存在 → 重命名为 /tmp/config.json.bak.1712345678901234
总结:该方案以最小依赖(仅标准库)、最大兼容性(Python 3.3+)和严格原子性,解决了“安全覆盖+自动备份”这一常见运维与配置管理需求。在编写守护进程、日志轮转或配置热更新脚本时,应优先采用此类基于系统调用原子语义的设计,而非基于条件检查的脆弱逻辑。










