上文中, 我们介绍了fflush()系统调用可以强制刷新输出文件的内核缓冲区。有时,如果应用程序在继续执行之前必须确保输出确实已写入磁盘,本文将继续介绍操作系统提供的其他的方式来满足该场景。同步的两种数据
SUSv3定义了术语“同步I/O完成”,意指“要么已成功传输(到磁盘)的I/O操作,要么已被诊断为失败的I/O操作”. “I/O同步”包含了两部分的数据: 描述文件的元数据和文件数据, 其中元数据包括以下信息即可:文件所有者和组、文件权限、文件大小、指向文件的(硬)链接数、指示最后文件访问、最后文件修改和最后元数据更改时间的时间戳,以及文件数据块指针等; 文件数据则是写入文件的数据了. Linux操作系统提供了ls命令用于查看元数据, cat命令可以查看文件数据.如何区分元数据和文件数据呢?ls -al coze.txt-rw-r--r-- 1 yang staff 147 3 5 11:33 coze.txtcat coze.txttest
SUSv3针对这两类数据, 也定义了两种写操作的“同步I/O完成”,第一种同步I/O完成类型是同步I/O数据完整性完成。它关注的是确保文件数据更新能够传输足够的信息,以便后续能够检索到这些数据。这意味着写请求中指定的数据已传输(到磁盘),并且检索该数据所需的所有文件元数据也一并传输了。这里需要注意的关键点是,并非所有修改过的文件元数据属性都需要传输才能检索文件数据。一个需要传输的修改过的文件元数据属性示例是文件大小(如果写操作扩展了文件)。相比之下,修改过的文件时间戳则不需要在后续数据检索能够进行之前传输到磁盘。第二种是同步I/O完成类型是同步I/O文件完整性完成, 在文件更新期间,所有更新的文件元数据都会被传输到磁盘,即使这些元数据对于后续读取文件数据的操作并非必需。可以说第二种是第一种的超集.文件描述符系统调用
有了上面的基础, 对于系统调用提供的两个函数, 就能理解和区分它们的作用了. fsync() 系统调用会使与打开文件描述符 fd 关联的缓冲数据和所有元数据都被刷新到磁盘。调用 fsync() 会强制文件进入同步I/O文件完整性完成状态, 它就是上面定义的第二种同步I/O完成. fdatasync() 系统调用的操作与 fsync() 类似,但它仅强制文件进入同步I/O数据完整性完成状态, 也就是上面第一种定义。 介绍了这两者的差别, 下面介绍一下为什么需要fdatasync(), 不直接默认使用fsync()更方便吗? 使用 fdatasync() 可以将 fsync() 所需的两次磁盘操作潜在减少为一次。例如,如果文件数据已更改,但文件大小未变,则调用 fdatasync() 只会强制更新数据(如前所述,同步I/O数据完成不需要传输像最后修改时间戳这样的文件元数据属性变更)。相比之下,调用 fsync() 还会强制将元数据也传输到磁盘。以这种方式减少磁盘I/O操作的数量,对于某些性能至关重要且精确维护某些元数据(如时间戳)并非必需的应用程序来说非常有用。对于进行多次文件更新的应用程序而言,这可以产生显著的性能差异:由于文件数据和元数据通常存储在磁盘的不同部分,同时更新它们将需要在磁盘上来回进行多次寻道操作。其他系统调用和实现
注意, 上面介绍的fsync()和fdatasync()都是有f的前缀, 表示该方法仅仅对某个文件描述符生效, 而sync() 系统调用会使所有包含已更新文件信息(即数据块、指针块、元数据等)的内核缓冲区都被刷新到磁盘。 另外如果不想使用每次调用write()方法以后, 都使用一次fsync()来同步数据, 是不是可以给文件描述符设置一个属性, 让操作系统在每次调用write()方法以后自行刷新数据呢? fd = open(pathname, O_WRONLY | O_SYNC); 中的O_SYNC属性可以达成这个目标.I/O缓冲总结
从结构上来看, 会有stdio缓冲和内核缓冲, 使用setbuf()可以设置缓冲的模式, 而fflush()可以把stdio缓冲的数据强制写入到内核, 否则只能等待stdio缓冲的数据达到一定大小之后会自动写入, 但是这不代表数据会马上写入到文件. 如果希望写入的数据能马上从内核缓冲写入到文件, 则可以使用open()函数设置O_SYNC属性, 或者在调用write()之后调用sync()相关函数来强制数据写入到文件中.