通过系统编程:从write()和fwrite()谈开来我们知道了系统调用和glibc库函数为了提升性能而设立的缓冲区,那么,什么情况下数据会从上一次缓冲区刷新到下一层存储介质(可能是缓冲区,也可能是永久存储介质)呢?fflush()库函数提供了强制将stdio库函数缓冲区数据刷新到内核缓冲区的功能,那么从内核缓冲区到磁盘呢?本文介绍的函数fsync()和fdatasync()将关注这个问题。
同步I/O数据完整性和同步I/O文件完整性
在介绍具体函数之前,先介绍两个概念:是同步IO数据完整性(synchronized IO data integrity completion)和同步IO文件完整性(synchronized IO file integrity completion)。SUSv3给出的同步IO完成定义:某一IO操作,要么你已经成功完成到磁盘的数据传输,要么被诊断为不成功。
我们知道,内核对于一个文件的存储,主要是两类信息:用户写入数据内容本身和文件元数据,元数据包括文件属主、属组、文件权限、大小、链接数量、时间戳、指向文件数据块的指针等等。那么,synchronized IO data integrity completion旨在确保针对文件的一次更新传递了足够的信息到磁盘,以便读写操作对数据的获取:
- 就读操作而言,这意味着被请求的文件数据已经从磁盘传递给了进程。若存在任何影响到所请求数据的挂起写操作,那么在执行读操作之前,会将这些数据传递到磁盘。(笔者:读到的数据一定是和磁盘上的数据是一致的。)
- 就写操作而言,这意味着写请求所指定的数据已传递至磁盘完毕,且用于获取数据的文件元数据也已传递到磁盘完毕。这里要点在于,要获取文件数据,并非需要所有修改过的文件元数据。(笔者:更改的数据传输到磁盘,并且读取更改数据所需要的部分元数据也传输到磁盘,而并非所有变动的元数据。)
在知道了synchronized IO data integrity completion之后,synchronized IO file integrity completion的含义也呼之欲出了,后者是前者的超集,它意味着,文件的一次更新过程,要将所有变更的元数据都传递到磁盘。
fsync()
fsync()系统调用将使缓冲数据和与打开文件描述符fd相关的所有元数据都刷新到磁盘上。调用fsync()会强制是文件处于synchronized IO file integrity completion状态。
#include <unistd.h>
// Returns 0 on success, or -1 on error
int fsync(int fd);
仅在对磁盘设备(或者至少是其高速缓存)的传递完成后,函数才返回。
在调用open()系统调用时如果指定了O_SYNC标志,fd = open(pathneme, O_WRONLY | O_SYNC);
则会使后续输出遵循synchronized IO file integrity completion语义。
fdatasync()
fdatasync()系统调用的功能类似fsync(),但是只会前置文件处于synchronized IO data integrity completion状态。
#include <unistd.h>
// Returns 0 on success, or -1 on error
int fdatasync(int fd);
fdatasync()可能会减少对磁盘操作的次数,由fsync()调用请求从2次变为1次。例如,若修改了文件数据,文件大小不变,那么fdatasync()只会更新数据(不刷新时间戳到磁盘),而fsync()则会强制刷新文件数据和元数据。
sync()
sync()系统调用会使包含更新文件信息的所有内核缓冲区(即数据库、指针块、元数据等)刷新到磁盘上。
#include <unistd.h>
void sync(void);
在Linux实现中,sync()调用仅在所有数据已经传递到磁盘上(或者至少到其高速缓存)时返回。
若内容发生变化的内核缓冲区在30秒内未经显式方式同步到磁盘上,则一条长期运行的内核线程会确保将其刷新到磁盘上。这一做法是为了规避缓冲区与相关磁盘文件内容长期处于不一致状态(以至于在系统崩溃时发生数据丢失)的问题。在Linux2.6版本中,该任务由pdflush内核线程执行。
性能
下表给出了采用不同缓冲区大小,在有、无O_SYNC标志情况下将1MB字节写入一个EXT2文件系统格式的文件中的时间。
简单的测试场景就可以看出,频繁调用fsync()、fdatasync()或sync()对性能影响极大,如果不使用磁盘的高速缓存,性能影响就恶劣。
如果在程序设计中,需要强制刷新内核缓冲区,那么就应该考虑是否使用大尺寸的write()缓冲区,或者在调用fsync()或fdatasync()时谨慎行事,而不是在打开文件时就加上O_SYNC标志。
参考
- Linux系统编程手册,[德] Michael Kerrisk