《Linux系统编程(第2版)》——2.4 同步I/O

  1. 云栖社区>
  2. 博客>
  3. 正文

《Linux系统编程(第2版)》——2.4 同步I/O

异步社区 2017-05-02 11:36:00 浏览1269
展开阅读全文

本节书摘来自异步社区《Linux系统编程(第2版)》一书中的第2章,第2.4节,作者:【美】Robert Love著,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.4 同步I/O

虽然同步I/O是个很重要的主题,但不必过于担心延迟写的问题。写缓冲带来了极大的性能提升,因此,任何操作系统,甚至是那些“半吊子”的操作系统,都因支持缓冲区实现了延迟写而可以称为“现代”操作系统。然而,有时应用希望能够控制何时把数据写到磁盘。在这种场景下,Linux内核提供了一些选择,可以牺牲性能换来同步操作。

2.4.1 fsync()和fdatasync()
为了确保数据写入磁盘,最简单的方式是使用系统调用fsync(),在POSIX.1b标准中定义如下:

screenshot

系统调用fsync()可以确保和文件描述符fd所指向的文件相关的所有脏数据都会回写到磁盘上。文件描述符fd必须以写方式打开。该调用会回写数据和元数据,如创建的时间戳以及索引节点中的其他属性。该调用在硬件驱动器确认数据和元数据已经全部写到磁盘之前不会返回。

对于包含写缓存的硬盘,fsync()无法知道数据是否已经真正在物理磁盘上了。硬盘会报告说数据已经写完了,但是实际上数据还在硬盘驱动器的写缓存上。好在,在硬盘驱动器缓存中的数据会很快写入到磁盘上。

Linux还提供了系统调用fdatasync():

screenshot

fdatasync()的功能和fsync()类似,其区别在于fdatasync()只会写入数据以及以后要访问文件所需要的元数据。例如,调用fdatasync()会写文件的大小,因为以后要读该文件需要文件大小这个属性。fdatasync()不保证非基础的元数据也写到磁盘上,因此一般而言,它执行更快。对于大多数使用场景,除了最基本的事务外,不会考虑元数据如文件修改时间戳,因此fdatasync()就能够满足需求,而且执行更快。

fsync()通常会涉及至少两个I/O操作:一是回写修改的数据,二是更新索引节点的修改时间戳。因为索引节点和文件数据在磁盘上可能不是紧挨着——因而会带来代价很高的seek操作——在很多场景下,关注正确的事务顺序,但不包括那些对于以后访问文件无关紧要的元数据(比如修改时间戳),使用fdatasync()是提高性能的简单方式。
fsync()和fdatasync()这两个函数用法一样,都很简单,如下:

screenshot

而fdatasync()的使用方式如下:

screenshot

这两个函数都不保证任何已经更新的包含该文件的目录项会同步到磁盘上。这意味着如果文件链接最近刚更新,文件数据可能会成功写入磁盘,但是却没有更新到相关的目录中,导致文件不可用。为了保证对目录项的更新也都同步到磁盘上,必须对文件目录也调用fsync()进行同步。

返回值和错误码
成功时,两个调用都返回0。失败时,都返回-1,并设置errno值为以下三个值之一:

EBADF

给定文件描述符不是以写方式打开的合法描述符。

EINVAL

给定文件描述符所指向的对象不支持同步。

EIO

在同步时底层I/O出现错误。这表示真正的I/O错误,经常在发生错误处被捕获。

对于某些Linux版本,调用fsync()可能会失败,因为文件系统没有实现fsync(),即使实现了fdatasync()。某些“固执”的应用可能会在fsync()返回EINVAL时尝试使用fdatasync()。代码如下:

screenshot

在POSIX标准中,fsync()是必要的,而fdatasync()是可选的,因此在所有常见的Linux文件系统上,都应该为普通文件实现fsync()系统调用。但是,特殊的文件类型(比如那些不需要同步元数据的)或不常见的文件系统可能只实现了fdatasync()系统调用。

2.4.2 sync()
sync()系统调用用来对磁盘上的所有缓冲区进行同步,虽然它效率不高,但还是被广泛应用:

screenshot

该函数没有参数,也没有返回值。它总是成功返回,并确保所有的缓冲区——包括数据和元数据——都能够写入磁盘[4]。

POSIX标准并不要求sync()一直等待所有缓冲区都写到磁盘后才返回,只需要调用它来启动把所有缓冲区写到磁盘上即可。因此,一般建议多次调用sync(),确保所有数据都安全地写入磁盘。但是对于Linux而言,sync()一定是等到所有缓冲区都写入了才返回,因此调用一次sync()就够了。

sync()的真正用途在于同步功能的实现。应用应该使用fsync()和fdatasync()将文件描述符指定的数据同步到磁盘中。注意,当系统繁忙时,sync()操作可能需要几分钟甚至更长的时间才能完成。

2.4.3 O_SYNC标志位
系统调用open()可以使用O_SYNC标志位,表示该文件的所有I/O操作都需要同步:

screenshot

读请求总是同步操作。如果不同步,无法保证读取缓冲区中的数据的有效性。但是,正如前面所提到的,write()调用通常是非同步操作。调用返回和把数据写入磁盘没有什么关系,而标志位O_SYNC则将二者强制关联,从而保证write()调用会执行I/O同步。

O_SYNC标志位的功能可以理解成每次调用write()操作后,隐式执行fsync(),然后才返回。这就是O_SYNC的语义,虽然Linux内核在实现上做了优化。

对于写操作,O_SYNC对用户时间和内核时间(分别指用户空间和内核空间消耗的时间)有些负面影响。此外,根据写入文件的大小,O_SYNC可能会使进程消耗大量的时间在I/O等待时间,因而导致总耗时增加一两个数量级。O_SYNC带来的时间开销增长是非常可观的,因此一般只在没有其他方式下才选择同步I/O。

一般来说,应用要确保通过fsync()或fdatasync()写数据到磁盘上。和O_SYNC相比,调用fsync()和fdatasync()不会那么频繁(只在某些操作完成之后才会调用),因此其开销也更低。

2.4.4 O_DSYNC和O_RSYNC
POSIX标准为open()调用定义了另外两个同步I/O相关的标志位:O_DSYNC和O_RSYNC。在Linux上,这些标志位的定义和O_SYNC一致,其行为完全相同。

O_DSYNC标志位指定每次写操作后,只同步普通数据,不同步元数据。O_DSYNC的功能可以理解为在每次写请求后,隐式调用fdatasync()。因为O_SYNC提供了更严格的限制,把O_DSYNC替换成O_SYNC在功能上完全没有问题,只有在某些严格需求场景下才会有性能损失。

O_RSYNC标志位指定读请求和写请求之间的同步。该标志位必须和O_SYNC或O_DSYNC一起使用。正如前面所提到的,读操作总是同步的——只有当有数据返回给用户时,才会返回。O_RSYNC标志位保证读操作带来的任何影响也是同步的。也就是说,由于读操作导致的元数据更新必须在调用返回前写入磁盘。在实际应用中,可以理解成在read()调用返回前,文件访问时间必须更新到磁盘索引节点的副本中。在Linux中,O_RSYNC和O_SYNC的含义相同,虽然这没有什么意义(与O_SYNC和O_DSYNC的子集关系不同)。在Linux中,O_RSYNC无法通过当前行为来解释,最接近的理解是在每次read()调用后,再调用fdatasync()。实际上,这种行为极少发生。

网友评论

登录后评论
0/500
评论
异步社区
+ 关注