文件I/O缓冲

为了速度和效率起见,I/O系统调用(即内核)和标准C库的I/O函数(即stdio函数)在对磁盘文件进行操作时,会缓冲(buffer)数据。本章我们将介绍缓冲的类型以及它们是如何影响性能的。我们还会阐述影响和禁用缓冲的各种技术,并且探讨名为 直接I/O(direct I/O) 的技术,用于在某些情况下绕过内核缓冲。

13.1 Kernel Buffering of File I/O: The Buffer Cache

当使用磁盘文件时,read()和write()系统调用不会直接发起磁盘访问。仅仅通过在 user-space buffer(用户空间缓冲) 和 内核中的 buffer cache(缓冲器高速缓存) 之间拷贝数据。例如,下面的调用会将3个字节的数据从用户空间内存的buffer中传输到内核空间的buffer cache中:

write(fd, "abc", 3);

write()随即返回。在随后的某个时刻,内核会将buffer chache中的数据写入(write,flush)到磁盘中(因此,系统调用与磁盘操作是异步(not synchronized)的)。如果,在期间另一个进程尝试读取这些文件的字节,那么内核会自动将buffer cache中的数据返回给它,而不是将磁盘文件中的(过时的)内容返回给它。
相应的,对于输入,内核从磁盘读取数据,并将它存储到内核的buffer cache中。调用read()会从buffer cache读取数据,直到数据读完。这时,内核会将文件的下一个分段(segment)读取到buffer cache中(这是简化的描述;对于顺序文件访问,内核一般执行 预读(read-ahead) 来确保(在进程需要这些数据之前)文件的下一个块被读到buffer cache中)。
这种设计的目的是为了read()和write()更加快速。因为,不需要等待(很慢的)磁盘操作。这种设计而且还很 高效 ,因为它降低了磁盘传输的次数。
Linux内核对于buffer cache的大小没有设置固定的上限值。内核会根据需要尽可能多地分配buffer cache pages,只受限于可用物理内存和用于其他目的的物理内存的数量。如果可用内存紧张,那么内核会将一些修改过的buffer cache pages写入到磁盘中,以便将这些pages进行重用。

Effect of buffer size on I/O system call performance

不管我们执行1000次写入,每次往磁盘写入1个字节,还是执行1次写入,一次性往磁盘写入1000个字节,内核都要执行相同次数的磁盘访问。然而,后者显然是更好的,因为它只需要一次系统调用,而前者需要1000次系统调用。虽然系统调用比磁盘操作更快,但是还是会消耗相当可观的时间,内核必须捕获调用,检查系统调用参数的有效性,并在用户空间和内核空间之间传输数据(参考3.1节)。
执行文件I/O时,使用不同buffer size对性能的影响可以通过使用不同的BUF_SIZE值来运行Listing 4-1 中的程序来查看(BUF_SIZE常量指定每次调用read()和write()时传输的字节数)。有关Table 13-1中的信息需要注意:总用时(Elapsed time)总CPU时间(Total CPU time) 列具有明显的含义。总CPU时间 可分成 用户CPU(User CPU)时间系统CPU(System CPU)时间 列,分别表示用户模式下(user mode)执行代码的耗时和执行内核代码时的耗时。
在这里插入图片描述

总之,如果我们要从(向)文件中传输很多数据,那么将数据缓冲到大的块(block)中
,执行更少的系统调用,可以大大提升I/O性能。
Table 13-1中的数据测量了一系列因素:执行read()和write()系统调用的耗时、在内核空间buffer和用户空间buffer之间传输数据的耗时。内核buffer和磁盘之间传输数据的耗时。显然,将输入文件的内容传递到buffer cache中是不可避免的。但是我们已经知道write()会将数据从用户空间传输到内核的buffer cache后立即返回。因为测试系统的RAM大小(4GB)远远超过文件拷贝的大小(100M),所以我们可以假定在程序结束时,输出文件实际上并没有被写入到磁盘中。因此,我们可以使用程序来进行实验,使用write()的不同buffer size,结果会与Table 13-2类似。

在这里插入图片描述

Table 13-2展示了write()系统调用所需要花费的时间,以及使用不同write() buffer size来从用户空间和内核buffer cache之间传输数据。正如Table 13-1所示,使用更大的buffer size对效率有显著的提升。 例如,对于一个65536字节的buffer size,Table 13-1中的总的时间是2.06秒,而Table 13-2是0.09秒。这是因为后者执行的时候没有进行实际的磁盘I/O 。换句话说,Table 13-1中(使用大的buffer size)花费的主要时间是在磁盘读取。
正如13.3节中所示,如果我们在数据被传输到磁盘之前对输出操作进行阻塞,那么write()调用的耗时会显著增加。

13.2 Buffering in the stdio Library

当操作磁盘文件时,C库I/O函数(例如fprintf()、fscanf()、fgets()、fputs()、fputc()、fgetc())将数据缓冲成大块,以减少系统调用的次数。因此,使用stdio库使得我们免于自己对数据进行缓冲任务,无论调用write()来输出,还是调用read()来输出。

Setting the buffering mode of a stdio stream

stdio库中的 setvbuf() 函数控制着缓冲的形式。

#include <stdio.h>
//成功时返回0,失败时返回非0
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

stream 参数用于标识要修改哪个文件流的缓冲。打开流后,在对流执行其他stdio函数之前必须先执行setvbuf()系统调用。setvbuf()的调用会在随后影响这个流上的所有其他stdio操作行为。
bufsize 参数则针对流中用到的缓冲。

Flushing a stdio buffer

可以使用fflush()函数强制将stdio输出流中的数据(通过使用write())刷到内核缓冲中。

#include <stdio.h>
// 成功时返回0,失败时返回EOF
int fflush(FILE *stream);

如果 stream 是null,刷新所有stdio缓冲。
fflush() 函数还可运用于输入流(input stream)中,这会导致缓冲中的输入被丢弃。 (当程序下次从流中读取时,缓冲会重新填满)
当流关闭时,会自动刷新stdio缓冲。

13.3 Controlling Kernel Buffering of File I/O

强制刷新内核缓冲到输出文件是允许的。有时是必要的,例如应用在执行下一步操作之前需要确保输出真正写入到磁盘中。

Synchronized I/O data integrity (数据完整性) and synchronized I/O file integrity (文件完整性)

SUSv3定义了术语 synchronized I/O completion(同步I/O完成),表示“某一I/O操作,要么将数据成功地写入到磁盘中,要么被诊断为不成功”。
SUSv3定义了两种不同的 synchronized I/O completion 类型。这两种类型的不同之处涉及到用于描述文件的 元数据(metadata) ,元数据中包含文件所属者、组、文件权限、文件大小、硬链接个数、用于表示文件最近访问、最近修改和元数据最近改变的时间戳以及指向文件数据块的指针。
SUSv3中定义的第一种 synchronized I/O completion 的类型是 Synchronized I/O data integrity completion,旨在确保针对文件的一次更新传递了足够的信息(到磁盘),以便之后对数据的获取。

  • 对于读取(read)操作,这意味着请求的数据已经从磁盘中传递到进程中。若存在任何影响到所请求数据的挂起写操作,那么在执行读操作之前,会将这些数据传递到磁盘。
  • 就写操作而言,这意味着写请求所指定的数据已传递(至磁盘)完毕,且用于获取数据的所有文件元数据也已传递至磁盘完成。

SUSv3中定义的另一种 synchronized I/O completion 类型是 synchronized I/O file integrity completion ,它是 Synchronized I/O data integrity completion 的超集(superset)。不同之处在于这种模式在对文件的一次更新过程中,要将所有发生更新的文件元数据都传递到磁盘上,即使有些在后续对文件数据的读操作中并不需要。

System calls for controlling kernel buffering of file I/O

fsync() 系统调用将使缓冲数据与打开文件描述符fd相关的所有元数据都刷新到磁盘上。调用fsync()会强制使文件处于Synchronized I/O file integrity completion状态。

#include <unistd.h>
// 成功时返回0,失败时返回-1
int fsync(int fd);

fdatasync() 系统调用操作与 fsync() 类似,只是强制文件处于 synchronized I/O data integrity completion的状态。

#include <unistd.h>
// 成功时返回0,失败时返回-1
int fdatasync(int fd);

使用fdatasync()会减少磁盘操作的数量,从fsync()所需的两次减少为一次。例如,如果文件数据发生了变化,但是文件的大小没有改变,那么调用fdatasync()只会强制将数据进行更新。(针对synchronized I/O data completion状态,像最近修改时间戳之类的元数据属性发生变化,是无需传递到磁盘的)。相比之下,fsync()调用会强制将元数据传递到磁盘上。
对某些应用而言,以这种方式来减少磁盘I/O操作的次数是很有用的,比如对性能要求极高,而对某些元数据(如时间戳)的准确性要求不高的应用。当应用程序同时进行多处文件更新时,二者存在相当的性能差异,因为文件数据和元数据通常驻留在磁盘的不同区域,更新这些数据需要反复在整个磁盘上执行寻道操作。
sync()系统调用会使包含更新文件信息的所有内核缓冲区(即数据块、指针块、元数据等)刷新到磁盘上。

#include <unistd.h>
void sync(void);

Making all writes synchronous: O_SYNC

调用open()时,指定 O_SYNC 标志会使所有随后的输出同步:

fd = open(pathname, O_WRONLY | O_SYNC);

在调用上面的open()之后,每次write()操作都会将文件数据和元数据刷新到磁盘中(即按照Synchronized I/O file integrity completion的要求执行写操作)。

Performance impact of O_SYNC

使用O_SYNC标志(或者频繁调用fsync()、fdatasync()或sync())会严重地影响性能
在这里插入图片描述

13.4 Summary of I/O Buffering

图13-1概括了stdio函数库和内核所采用的缓冲(针对输出文件),以及对各种缓冲类型的控制机制。从图中自上而下,首先是通过stdio库将用户数据传递到stdio缓冲区,该缓冲区位于用户态内存区。当缓冲区填满时,stdio库会调用write()系统调用,将数据传递到内核高速缓冲区(位于内核态内存区)。最终,内核发起磁盘操作,将数据传递到磁盘。
在这里插入图片描述

图13-1左侧所示,可以在任何时刻使用这些调用显示强制刷新各类缓冲区。右侧的调用可用于自动刷新:一是通过禁用stdio库的缓冲,二是在文件输出类的系统调用中启动同步,从而使每个write()调用立刻刷新到磁盘。

13.5 Advising the Kernel About I/O Patterns

posix_fadvise() 系统调用允许进程就自身访问文件数据时可能采取的模式通知内核。

#define _XOPEN_SOURCE 600
#include <fcntl.h>
// Returns 0 on success, or a positive error number on error
int posix_fadvise(int fd, off_t offset, off_t len, int advice);

内核可以通过使用 posix_fadvise() 中提供的信息来优化buffer cache的使用,所以从总体上提升进程的I/O性能。调用posix_fadvise()对程序的语义没有任何影响。
fd参数表示文件描述符。参数offset和len确定了建议所适用的文件区域。offset指定了区域起始的偏移量,len指定了区域的大小(以字节数为单位)。len为0表示从offset开始,直至文件结尾。(在内核2.6.6版本之前,len为0就表示长度为0个字节)
参数advice表示进程期望对文件采取的访问模式。具体为下列参数之一:

  • ** POSIX_FADV_NORMAL** : 进程对访问模式无特别建议。如果没有建议,就说默认行为。在Linux中,该操作将文件 预读(read-ahead) 窗口大小置为默认值(128KB)。
  • ** POSIX_FADV_SEQUENTIAL** : 进程预计会从低偏移量到高偏移量顺序读取数据。在Linux中,该操作将文件预读窗口大小置为默认值的两倍。
  • POSIX_FADV_RANDOM : 进程预计以随机顺序访问数据。在Linux中,该选项会禁用文件预读。
  • POSIX_FADV_WILLEED
  • POSIX_FADV_DONTNEED
  • POSIX_FADV_NOREUSE

13.6 Bypassing the Buffer Cache: Direct I/O

从内核2.4开始,当执行磁盘I/O时,linux允许应用绕过buffer cache,这样数据直接从用户空间传输到文件或者磁盘上。这有时被称为 直接I/O(direct I/O) 或者 裸I/O(raw I/O)
有时会将直接I/O误认为获取快速I/O性能的一种手段。然而对于大多数应用而言,使用直接I/O可能会大大降低性能。这是因为为了提高I/O性能,内核针对buffer cache做了不少优化,其中包括:按顺序预读取,在成簇(clusters)磁盘块上执行I/O,允许访问同一个文件的多个进程共享buffer cache。应用如使用了直接I/O将无法受益于这些优化举措。直接I/O只适用于有特定I/O需求的应用。例如数据库系统,其高速缓存和I/O优化机制都自成一体,无需内核消耗CPU时间和内存去完成相同任务。
可针对一个单独文件或者块设备(比如,一块磁盘)执行直接I/O。要做到这点,需要在调用open()打开文件或设备时指定O_DIRECT标志。

若一进程以O_DIRECT标志打开某文件,而另一进程以普通方式(即使用了高速缓存缓冲区)打开同一文件,则由直接I/O所读写的数据与缓冲区高速缓存中内容之间不存在一致性。应尽量避免这一场景。

13.8 Summary

输入输出数据的缓冲由内核和stdio库完成。有时可能希望组织缓冲,但这需要了解其对应用程序性能的影响。可以使用各种系统调用和库函数来控制内核和stdio缓冲,并执行一次性的缓冲区刷新。
进程使用posix_fadvise()函数,可就进程对特定文件可能采取的数据访问模式向内核提出建议。内核可使用该信息来优化使用buffer cache的应用,从而提高I/O性能。
在Linux环境下,open()所特有的O_DIRECT标识允许特定应用跳过buffer cache。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值