原子写入需用临时文件+fsync+rename:先写临时文件,调用fsync确保落盘,再rename原子替换,最后可选fsync目录;直接fwrite或o_trunc不安全,o_sync性能差且不解决部分写问题。

为什么直接 fwrite 或 ofstream::write 不是原子的
因为操作系统对文件的写入操作本身不保证原子性:即使只写一个字节,若进程在中途崩溃或被 kill,文件可能处于中间状态(比如只写了一半内容)。更常见的是覆盖旧文件时,open(..., O_WRONLY | O_TRUNC) 会清空原文件再写入,一旦写入失败,原始数据就永久丢失了。
用临时文件 + rename 实现用户级原子写入
这是 POSIX 和 Windows(通过 MoveFileEx)都支持的通用方案:先写入一个同目录下的临时文件(如 config.json.tmp),写完并 fsync 后,再用 rename(Linux/macOS)或 MoveFileEx(Windows)将其原子地替换目标文件。关键点在于:rename 在同一文件系统内是原子的,要么成功,要么失败,不会留下损坏状态。
- 临时文件必须与目标文件在同一挂载点(否则
rename会失败,需 fallback 到 copy + unlink) - 写完临时文件后,务必调用
fsync(fd)(Linux/macOS)或FlushFileBuffers(hFile)(Windows),否则数据可能还卡在页缓存里,rename后重启仍可能丢失 - 临时文件名建议用
mkstemp(Linux)或GetTempFileName(Windows)生成,避免竞态和重名 - C++ 标准库没有跨平台原子写入封装,需自己组合
std::ofstream、close()、fsync()和系统 API
Windows 下 MoveFileEx 的坑比 rename 多
Windows 的 MoveFileEx 默认不覆盖目标文件,必须显式传 MOVEFILE_REPLACE_EXISTING;而且如果目标文件正被其他进程以非共享方式打开(比如记事本没关),MoveFileEx 会失败——这和 Linux rename 行为不同(Linux 允许覆盖正在被读的文件)。
- 务必检查返回值,失败时不要静默忽略
- 临时文件写入后,应先
CloseHandle,再调用MoveFileEx,否则 Windows 会拒绝移动打开的文件 - 若需兼容只读文件属性(比如 NTFS 上的只读 flag),得在
MoveFileEx前用SetFileAttributes清除FILE_ATTRIBUTE_READONLY
要不要用 O_SYNC 或 O_DSYNC?
不推荐直接对目标文件用 O_SYNC 开启,因为它是写时同步,性能差且不能解决“写一半崩溃”的问题(它只保证本次 write 返回前落盘,但不保证整个文件内容一致)。而临时文件 + fsync 是可控的:你只在确认写完后同步一次,粒度更准。
-
O_SYNC会让每次write()都等磁盘确认,吞吐可能降 10–100 倍 -
O_DSYNC只同步数据不包括元数据(如 mtime),但rename涉及目录项更新,仍需fsync目录(Linux 2.6.33+ 支持fsync目录,但 C++ 无标准接口,常被忽略) - 真正安全的做法是:写完临时文件 →
fsync临时文件 fd →rename→fsync目标目录(可选,但强一致性场景需要)
fsync。C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











