文件原子操作
假设一个场景:
进程 A 往文件中写入数据
另一个进程 B 不停的读取文件的内容, 进行一些操作
这个场景类似于 竞争条件, 如果进程 A 写数据时执行到一半, 那么 B 进程读取到的数据就是不完整的, 导致 B 进程不能正常工作.
要解决这个问题, 就必须保证每个进程在操作文件时, 没有其他进程同时操作, 保证操作是原子操作即可.
哪些操作是原子操作?
说明: 以下原子操作说明基于 linux 操作系统, Windows 和 OS X 有部分的操作和 linux 不一致, 不能保证原子操作.
- mv -T <oldsymlink> <newsymlink>
- link(oldpath, newpath)
- symlink(oldpath, newpath)
- rename(oldpath, newpath)
- open(pathname, O_CREAT | O_EXCL, 0644)
- mkdir(dirname, 0755)
这些命令具体的操作结果参考 linux 帮助文档
使用 rename 保证原子操作
这里使用 rename 的方式有很多种, 最终都会进行原子的系统调用 例如:
- 使用 C 标准库中的 rename (man 2 rename)
- 使用 Shell 中的 rename 或者 mv (man 1 mv)
由于 rename
是原子操作, 所以我们可以依靠这个特性来避免文章开始假设场景中出现异常.
具体做法是:
- 创建临时文件 tmp
- 在临时文件 tmp 中写入内容
- 将 tmp rename 为目标文件
这里是一个 shell 例子
# 目标文件
dst="/var/some/file"
# 生成一个临时文件, 保证文件名不会与其他文件冲突
tmp="${trg%/*}/tmp/${trg##*/}.`date +%s`.$$"
printf "foo\nbar\nbaz" > "$tmp" || exit 111
mv "$tmp" "$dst"
注: rename 之前保证文件已经写入磁盘是更好实践
例如使用 flush
, fsync
等系统调用.
这里是一个 python 例子
def test():
f = open(tmpfile, 'w')
f.write(content)
f.flush()
os.fsync(f.fileno())
f.close()
os.rename(tmpfile, dstfile)
rename 几条说明
具体参考 man 2 rename
, 使用 mv
命令测试, 下面使用 rename(oldpath, newpath) 进行说明
- 如果 newpath 已经存在, 会覆盖 newpath (有权限的话)
- 如果 newpath 存在, 但是 rename 失败, newpath 不会被删除
- 如果 oldpath 是一个 symbolic link, 那么 link 被重命名(指向的文件不变)
- 如果 newpath 是一个 symbolic link, 那么 link 会被覆盖(指向的文件不变)
例外情况
- rename 的的 src 和 dst 所在文件系统如果不一样, 可能不会是原子操作(特别的, 使用 C 语言标准库中的 rename 时, 文件系统不一致会导致失败)
- OS X中的 mv 不是原子操作, 参考
- Windows 中使用 C 标准库的 rename 时, 如果 dst 已经存在, 调用会失败 (使用 Windows API 可以保证原子操作)
参考:
atomic writing to file with Python
Things UNIX can do atomically
Atomically Replacing Files and Directories
Safely Updating Critical Files on a Linux System
Rename (computing)
Linux 同步方法剖析