🔑 write() 成功了,为什么文件还是可能丢?
很多人第一次写文件时都会有一个直觉:
这就带来一个非常重要的后果:程序看起来“保存成功”了,机器一断电,文件仍然可能丢,甚至可能只写进去一半。尤其是你在更新配置文件、日志文件、状态文件时,这个坑非常常见。很多人以为“我都 close 了,应该安全了吧”,也不完全对。
这也是为什么很多可靠软件不直接覆盖原文件,而是采用“先写临时文件,再
所以,
🧪 课后一题:一个程序先用
💡 易混淆点:
#CS
很多人第一次写文件时都会有一个直觉:
write() 已经返回成功,数据就应该已经“进硬盘”了。这个直觉是错的。write() 在大多数 Unix/Linux 系统里,通常只表示“内核已经收下这段数据”,而不是“存储设备已经真的写好”。内核之所以这样设计,不是偷懒,而是为了性能:如果每次写入都强行等硬盘落盘,程序会慢得像卡住一样。于是操作系统把数据先放进 page cache,等合适的时候再批量刷盘,这样吞吐量高得多。这就带来一个非常重要的后果:程序看起来“保存成功”了,机器一断电,文件仍然可能丢,甚至可能只写进去一半。尤其是你在更新配置文件、日志文件、状态文件时,这个坑非常常见。很多人以为“我都 close 了,应该安全了吧”,也不完全对。
close() 主要是释放文件描述符,刷盘可能仍然是延后的。真正要逼内核把修改推到稳定存储,通常要显式调用 fsync() 或 fdatasync()。这也是为什么很多可靠软件不直接覆盖原文件,而是采用“先写临时文件,再
fsync,再 rename”的套路。因为 rename 在同一文件系统内通常是原子的:要么旧文件还在,要么新文件完整替换上去,不容易出现“文件存在,但内容只剩半截”这种恶心状态。不过这里还有第二层坑:你只 fsync 了文件本身,还不一定够。如果你新建了文件或者依赖目录项变化,目录也可能需要 fsync,否则断电后名字映射未必稳定。设计上这是把“数据内容”和“目录元数据”分开处理,目的是避免每次小改动都付出巨大的同步成本。所以,
write() 保证的是“本次系统调用把字节交给了内核”,不是“崩溃后这些字节仍然活着”。这套设计很实用,因为大部分写操作根本不值得每次都同步到底层设备;但一旦你在做配置保存、钱包、索引、状态机快照、提交记录这类关键数据,就不能再装作 page cache 和断电不存在。🧪 课后一题:一个程序先用
write() 写完 config.json,然后立刻退出,没有调用 fsync()。如果这时机器突然断电,config.json 的新内容一定已经安全保存了吗?答案:write()💡 易混淆点:
write() 成功、close() 成功、文件“看起来能读出来”这三件事,都不等于“断电后数据仍然存在”。#CS