fsync 在更新协议中完成存储同步请求
普通 write(2) 成功,可以让实际接受的字节通过内核文件状态被访问,但本身不能建立崩溃持久性。fsync 请求同步文件数据及相关元数据,并等待存储栈报告完成。它是更新协议中的一个操作,仍需要正确顺序、错误检查、目录项处理和底层真实保证。不能只在代码里出现一次 fsync,就推断整个更新已经具有完整事务与恢复语义。 坏掉的样子 用户态缓冲还形成更早一层边界。语言缓冲区尚未把字节提交给内核时,对描述符调用 fsync 无法同步这些字节。应先 flush 缓冲,并检查两个阶段的错误。普通 write 也可能只完成部分写入,延迟存储错误则可能在同步时才出现,而不是在最初写入时报告,因此每层返回结果都需要按契约处理。 同步新建文件,不一定同步命名它的目录项。在 Linux 上,持久创建或替换协议还需要适当的目录同步。原子 rename 提供运行期间的命名空间可见性保证,却不会独立证明目录变化能够经历崩溃保留。当前打开新路径能读到数据,与重启后仍能通过该名字找到预期对象,是不同问题。 fdatasync 不意味着完全不处理元数据。它同步数据,以及后续正确读取数据所需的元数据,包括变化后的文件长度;对于读取不必需的其他元数据更新,则可以省略。把它理解为与 fsync 在所有方面相同,或者完全无视元数据,都会掩盖选择两者的真正依据,应根据应用需要保持的状态理解同步范围。 快速验证 把脚本保存为 syncfile.py,在装有 Python 和 strace 的一次性 Linux 系统中运行 python3 syncfile.py。临时目录必须支持普通文件和目录同步。实测使用 Linux 6.12.76、Python 3.11.2、strace 6.1 和 ext4 卷。样本检查真实调用与运行结果,不会切断存储电源: from pathlib import Path from tempfile import TemporaryDirectory import subprocess,sys program=r''' from pathlib import Path import errno,os,sys root=Path(sys.argv[1]);current=root/'current';candidate=root/'candidate' current.write_bytes(b'old');old=current.open('rb') try: with candidate.open('wb') as handle: handle.write(b'new');handle.flush();os.fsync(handle.fileno()) os.replace(candidate,current) directory=os.open(root,os.O_RDONLY|os.O_DIRECTORY) try:os.fsync(directory) finally:os.close(directory) assert current.read_bytes()==b'new' and old.read()==b'old' with current.open('ab') as handle: handle.write(b'-extended');handle.flush();os.fdatasync(handle.fileno()) assert current.read_bytes()==b'new-extended' and current.stat().st_size==12 read_end,write_end=os.pipe() try: try:os.fsync(write_end) except OSError as error:assert error.errno==errno.EINVAL else:raise AssertionError('fsync on a pipe unexpectedly succeeded') finally:os.close(read_end);os.close(write_end) finally:old.close() print('replacement content, old open reference, appended length, and unsupported-pipe error verified') ''' with TemporaryDirectory(prefix='xbcatm-fsync-') as directory: root=Path(directory);trace=root/'trace' result=subprocess.run(['strace','-yy','-s','200','-e','trace=%file,write,fsync,fdatasync','-o',str(trace),'--',sys.executable,'-c',program,str(root)],capture_output=True,text=True,timeout=10) assert result.returncode==0,result.stderr lines=trace.read_text().splitlines() file_sync=next(i for i,line in enumerate(lines) if 'fsync(' in line and str(root/'candidate') in line and '= 0' in line) rename=next(i for i,line in enumerate(lines) if line.startswith('rename') and str(root/'current') in line and '= 0' in line) directory_sync=next(i for i,line in enumerate(lines) if 'fsync(' in line and '<'+str(root)+'>' in line and '= 0' in line) assert file_sync<rename<directory_sync assert any('fdatasync(' in line and str(root/'current') in line and '= 0' in line for line in lines) print(result.stdout,end='') for line in lines: if line.startswith(('fsync(','fdatasync(','rename')):print(line) print('syscall order verified: flush data, fsync candidate, replace name, fsync parent directory') 子进程创建内容为 old 的当前文件,并保留一份打开读取引用。然后把 new 写入独立候选文件,flush Python 缓冲,再对候选描述符调用 fsync。候选文件位于同一个目录,也使此实验的后续 rename 保持在同一文件系统内,不依赖跨挂载复制等不同操作来模拟替换。 ...