安全写文件需先写同目录临时文件、fsync刷盘、再rename原子替换;临时文件与目标须同挂载点,NFS等需额外同步策略,Windows rename可能失败需降级处理。

写文件前先写到临时文件再原子替换
直接 fopen 写目标路径,断电或崩溃时大概率丢数据——因为写入过程分“清空旧内容”和“写新内容”两步,中间任何异常都会留下空文件或截断文件。安全做法是:先写到同目录下的临时文件(如 "data.json.tmp"),写完调用 fsync 刷盘,再用 rename 原子替换原文件。
-
rename在同一文件系统下是原子操作,Linux/macOS 都保证不会出现“只有半份新数据”的中间态 - 临时文件必须和目标文件在同一个挂载点,否则
rename会失败,得 fallback 到 copy + unlink,失去原子性 - 务必在
rename前调用fsync(对临时文件的 fd),否则操作系统可能只把数据留在 page cache,没真正落盘 - 示例关键步骤:
int fd = open("data.json.tmp", O_WRONLY | O_CREAT | O_TRUNC, 0644);<br>write(fd, buf, len);<br>fsync(fd);<br>close(fd);<br>rename("data.json.tmp", "data.json");
避免 fclose 后数据未落盘
用 std::ofstream 或 fclose 关闭文件,不等于数据已写入磁盘——C++ 标准库和 libc 的缓冲区、内核 page cache 都可能缓存着数据。尤其在嵌入式或掉电频繁场景,这会导致“明明 close 成功了,重启后文件却是空的”。
- 用 C 风格时,
fflush只刷 stdio 缓冲区,还得fsync刷内核缓冲区;fclose自动fflush,但不fsync - 用 C++
std::ofstream时,os.flush()等价于fflush,仍需额外获取底层 fd 并fsync(通过os.rdbuf()->pubsync()不可靠,建议用fileno(os.rdbuf()->fd())) - 如果目标是“只要不掉电就安全”,
fsync是必选项;若追求性能可权衡用fdatasync(只刷数据,不刷元数据)
处理 NFS 或其他网络文件系统
NFS 默认关闭 sync 模式,fsync 调用可能直接返回成功,实际数据还在客户端缓存里。这时候原子 rename 也救不了你——临时文件写完、rename 完,数据照样没到服务端磁盘。
- 挂载 NFS 时加
sync选项(如mount -o sync ...),强制每次 write 都等服务端落盘,但性能下降明显 - 更现实的做法是:应用层接受“NFS 上无法 100% 防丢”,关键数据改用本地存储 + 定期同步,或走数据库
-
fsync返回 0 不代表绝对安全,在 NFS 上它只表示“发给了服务端”,不表示“服务端已落盘”
Windows 下注意 rename 和权限问题
Windows 的 MoveFileEx(对应 POSIX rename)在替换已有文件时,行为和 Linux 不同:若目标文件正被其他进程以 GENERIC_READ 打开且没设 FILE_SHARE_DELETE,替换会失败。而很多日志查看器、IDE 默认就以这种模式打开文件。
立即学习“C++免费学习笔记(深入)”;
- 不要依赖
rename在 Windows 上总能成功;失败时需 fallback 到DeleteFile+MoveFile,但这不是原子的 - 临时文件名建议带进程 ID 或时间戳(如
"config.json.tmp.12345"),避免多进程并发写时冲突 - 确保目标目录有写权限,且防病毒软件没锁定该路径——某些杀软会拦截对配置文件目录的
rename操作并静默失败
真正难的不是写对几行代码,而是想清楚“我这个文件在什么情况下会丢”:是突然断电?是程序崩溃?还是 NFS 服务端宕机?不同场景要堵的洞不一样。临时文件 + fsync + rename 这套组合,在本地 ext4/xfs 上很稳,但一进容器、一上云盘、一连 Windows 共享,就得重新评估每一步是否还成立。



















