如何使用write() API对文件全部或部分内容执行原子RMW写入操作
原生write() API无法满足你的两类原子性要求
首先明确:标准POSIX write() 本身不支持原子RMW(读-修改-写)语义,也没法同时保障你要求的故障原子性和并发原子性:
- 并发原子性层面:只有
O_APPEND模式下的追加写入、且写入大小不超过PIPE_BUF时才被POSIX保证原子,普通位置的覆盖写入完全不保证多进程并发下不会出现内容穿插,更不可能保证两个写入操作最终只会完整保留其中一个的内容。 - 故障原子性层面:默认
write()写入的是操作系统页缓存,没有显式刷盘的话进程崩溃/系统掉电就会丢失数据;即使调用fsync()刷盘,也无法避免写入中途掉电出现新旧内容混合的问题。
符合要求的类CAS写入方案
以下是几种不同场景下的可行实现:
1. 全量内容原子替换(兼容性最强,通用所有POSIX系统)
这个方案是工业界最常用的文件原子更新实现,完全满足你要的两类原子性:
- 先将完整的新内容写入到和目标文件同挂载点的临时文件中,调用
fsync()将临时文件的内容、元数据全部刷入持久化存储,保证故障重启后临时文件内容完整。 - 调用
rename()系统调用直接用临时文件覆盖目标文件路径。POSIX标准明确保证rename()是原子操作:- 并发场景下多个进程同时执行替换操作,最终只会保留最后一次成功
rename的文件内容,不会出现不同写入的内容混合,符合最终值要么是A要么是B的要求。 - 故障场景下
rename()要么完全生效、要么完全不生效,不会出现半修改的中间状态,只要临时文件已经刷盘,操作成功后就算重启目标文件也会是完整的新内容。
- 并发场景下多个进程同时执行替换操作,最终只会保留最后一次成功
2. 部分内容原子RMW实现
如果不需要修改整个文件,只需要修改部分片段,可以选以下两种方案:
方案A:基于文件锁封装CAS逻辑
适配所有POSIX系统,只需要所有访问文件的进程遵守统一的加锁规则即可:
- 调用
fcntl()对目标文件加排他锁,保证同一时间只有一个进程可以修改文件。 - 加锁成功后读取你要修改的片段的当前值,判断是否符合你的修改前置条件。
- 条件满足则写入新内容,调用
fsync()刷盘后释放锁。
如果有进程不遵守加锁规则直接写文件,原子性会被打破。
方案B:文件系统特有原子写入接口
性能更高但兼容性有限,主流本地文件系统(ext4、XFS、Btrfs等)都支持:
- 使用带
RWF_ATOMIC标志的pwritev2()系统调用,只要写入大小不超过当前文件系统支持的原子写入上限(通常为1个物理页面大小,部分文件系统支持更大的块),就能保证写入操作是原子的,并发场景下不会出现内容穿插。 - 搭配
RWF_DSYNC或RWF_SYNC标志可以直接让写入返回时就完成落盘,不需要额外调用fsync(),同时满足故障原子性要求。
目前通用操作系统没有原生的文件级compare-exchange系统调用,如果你需要严格的“只有文件内容符合预期时才写入”的语义,可以在文件头部存储版本号,结合上面的全量替换/加锁方案自己封装实现:写入前先读版本号,写入时将版本号+1,修改时仅当当前版本号和你读取的版本号一致时才提交修改。
内容的提问来源于stack exchange,提问作者Curious
相关产品推荐
相关产品推荐

