第13章 文件I/O缓冲

        出于速度和效率考虑,系统I/O调用(即内核)和标准C语言函数库I/O函数(即stdio函数)在操作磁盘文件时会对数据进行缓冲。本章描述了两种类型的缓冲,并讨论了其对应用程序性能的影响。本章还讨论了可以凭不或影响缓冲的各种技术,以及直接I/O技术---在某西需要绕过内核缓冲的场景中非常有用。

13.1 文件I/O的内核缓冲:缓冲区高速缓存

        read()和write()系统调用在操作磁盘文件时不会直接发起磁盘访问,而是仅仅在用户空间缓冲区与内核缓冲区高速缓存(kernel buffer cache)之间复制数据。例如,如下调用将3个字节的数据从用户空间内存传递到内核空间的缓冲区中:

write(fd,"abc",3);

        write()随即返回。在后续某个时刻,内核会将缓冲区中的数据(刷新至)磁盘。(因此,可以说系统调用与磁盘操作并不同步。)如果在此期间,另一进程试图读取该文件的这几个字节,那么内核将自动从缓冲区高速缓存中提供这些数据,而不是从文件中(读取过期的内容)。

        与此同理,对输入而言,内核从磁盘中读取数据并存储到内核缓冲区中。read()调用将从该缓冲区中读取数据,直至把缓冲区的数据取完,这时,内核会将文件的下一段内容读入缓冲区高速缓存。(这里的描述有所简化。对于序列化的文件访问,内核通常会尝试执行预读,以确保在需要之前就将文件的下一数据块读入缓冲区的告诉缓存中。)

        采用这一设计,意在使read()和write()调用的操作更为快速,因为他们不需要等待(缓慢的)磁盘操作。同时,这一设计也极为高效,因为这减少了内核必须执行的磁盘传输次数。

        Linux内核对缓冲区告诉缓存的大小没有固定上限。内核会分配尽可能多的缓冲区高速缓存页,而仅受限于两个因素:可用的物理内存总量,以及出于其他目的对物理内存的需求(例如,需要将正在运行进程的文本和数据页保留在物理内存中)。若可用内存不足,则内核会将一些修改过的缓冲区高速缓存页内容刷新到次哦按,并释放其供系统重用。

        更确切的说,从内核2.4开始,Linux不再维护一个单独的缓冲区高速缓存。相反,会将文件I/O缓冲区置于面页面高速缓存中,其中还含有诸如内存映射文件的页面。然而,正文的讨论采用了“缓冲区高速缓存”这一术语,因为这时UNIX实现中历史悠久的通称。

缓冲区大小对I/O系统调用性能的影响

        无论是让磁盘写1000次,每次写入一个字节,还是一次写入1000个字节,内核访问磁盘的字节数都是相同的。然而,我们更属意于后者,因为它只需要一次系统调用,而前者则需要调用1000次。尽管比磁盘操作要快许多,但系统调用所耗费的时间总量也相当可观:内核必须捕获调用,检查系统调用参数的有效性,在用户空间和内核空间之间传输数据。

        为BUF_SIZE(BUF_SIIZE指定了每次调用read()和write()时所传输的字节数)设定不同的大小来运行程序清单4-1,可以观察到不同大小的缓冲区对执行文件I/O所产生的影响。表13-1所示为在Liunux ext2 文件系统上复制文件大小为100MB的文件,该程序在使用不同BUF_SIZE值所需要的时间。有关本表的信息,需要注意以下几点。

  • 总用时和总CPU时间这两列含义很明显。而用户CPU和系统CPU两列是将总CPU用时分解为在用户模式下执行代码所需的时间和执行内核代码所需的时间(比如系统调用)。
  • 表中测试结果地自语2.6.30普通(vanilla)内核下,块大小为4096字节的ext2文件系统。

所谓普通内核(vanilla kernel),意指未打补丁的主线(mainline)内核。与之形成鲜明对比的是大多数发行生所提供的内核,常常包含各种补丁来修复和添加新功能。

  • 每行显示的结果为给定的缓冲区大小下运行20次的均值。在这些测试以及本章后续提及的其他测试里,在程序每次的执行间隔中,会写在并再次重新装配文件系统,以确保文件系统的缓冲区高速缓存为空。计时则由shell命令time完成。

         因为采用不同的缓冲区大小时,数据的传输总量(因此招致磁盘操作的数量)是相同的,表13-1所示为发起read()和write()调用的开销。缓冲区大小为1字节时,需要调用read()write()1亿次,缓冲区大小为4096个字节时,需要调用read()和write()24000次左右,几乎达到最优性能。设置再超过这个值,对性能的提升就不显著了,这是因为在用户空间和内核空间之间复制数据以及执行实际磁盘I/O所花费的时间相比,read()和write()系统调用的成本就显得微不足道了。

从表13-1的最后一行可以粗略估算出在用户空间与内核空间之间传输数据以及执行文件I/O的总耗时。因为此时系统调用的次数相对较少,所以他们所花费的时间相对于总耗时和CPU时间可以忽略不计。据此可认为,系统CPU时间主要时测量用户空间与内核空间之间数据传输所消耗的时间。而总耗时则是对于磁盘传输数据所需时间的估算。(正是下面所提到的,时间主要花在了对磁盘的读操作上。)

        总之,如果与文件发生大量的传输,通过采用大块空间缓冲数据,以及执行更少的系统调用,可以极大提高I/O性能。

        表13-1度量了一系列因素:执行read()和write()系统调用所需的时间、内核空间和用户空间缓冲区之间传输数据所需的时间、内核缓冲区于磁盘之间传输数据所需的时间。再进一步考虑以下最后一个因素,显然,将输入文件的内容传输到缓冲区高速缓存是不可避免的。然而,当数据从用户空间传输到内核之后,write()调用立即返回。由于测试系统上的RAM大小(4GB)远超欲复制文件的大小(100MB),据此推断,当程序文成后,输出文件实际尚未写入磁盘。因此再进一步做个实验,运行一个程序,使用不同大小的缓冲区,以write()随意像文件中写入一些数据。运行结果如表13-2所示。

        同样,表13-2中数据是来自于内核2.6.30,以及块大小为4096字节的ext2文件系统,并且每行显示为运行了20次后的均值。本节并未列出测试程序(filebuff/write_bytes.c)的代码清单,可在随本书一起发行的源码中获取。

 表13-2 显示为使用不同大小的缓冲区调用write()从用户空间向内核缓冲区高速缓存传输数据所花费的成本。缓冲区越大有,与表13-1中数据的差异就越明显。例如,对于一个65536字节的缓冲区,在表13-1中总耗时为2.06秒,而表13-2中仅为0.09秒。这是因为在后者的情况下并未执行实际的磁盘I/O操作。换言之,表13-1中采用大缓冲区时的耗时绝大部分花在了对磁盘的读取上。

        正如13.3节所述,若强制在数据传输到磁盘前阻塞输出操作,则调用write()所需的时间会显著上升。

        最后值得注意的是,表13-2(以及表13-3)中信息仅仅代表了对文件系统评价基准的形式之一,还不完善。此外文件系统不同,结果可能也会有所不同。对文件系统的度量还有各种其他标准,比如多用户、高负载下的性能表现,创建和删除文件的速度,在一个大型目录下搜索一个文件所需的时间,存储小文件所需的空间,或在遭遇系统崩溃时对文件完整性的维护。只要I/O或是其他文件操作系统的性能直观重要,那么在目标平台上针对特定应用的测试基准就不可替代。

 13.2 stdio库的缓冲

        当操作磁盘文件时,缓冲大块数据以减少系统调用,C语言函数库的I/O函数(比如,frpintf()、fscanf()、fgets()、fputs()、fputc()、fgtec())正式这么做的。因此,使用stdio库可以式编程者免于自行处理对数据的缓冲,无论是调用write()来输出,还是调用read()来输入。

设置一个stdio流的缓冲模式

        调用setbuf()函数,可以控制stdio库使用缓冲的形式。

#include <stdio.h>
int setvbuf(FILE *stream,char *buf,int mode,size_t size);
                    Returns 0 on success,or nonzero on error

        参数stream 标识将要修改哪个文件流的缓冲。打开流后,必须在调用任何其他stdio函数之前先调用setvbuf()。setvbuf()调用将影响后续在指定流上的所有stdio操作。 

        参数buf 和Ssize则针对参数stream要使用的缓冲区,指定这些参数有如下这两种方式。

  • 如果参数buf不为NULL,那么其指向size大小的内存块以作为stream的缓冲区。因此为stdio库将要使用buf指向的缓冲区,所以应该以动态或静态在堆中为该缓冲区分配一块空间(使用malloc或类似函数),而不应是在分配在栈上的函数本地变量。否则函数函数时将销毁其栈帧,从而导致混乱。
  • 若buf为NULL,那么stdio库会为stream自动分配一个缓冲区(除非在选择非缓冲的I/O,如下所述)。SUSv3允许,但不强制要求库实现使用size来确定其缓冲区的大小。glibc实现会在该场景下忽略size参数。 

        参数mode指定的缓冲类型,并具有下列值之一。

_IONBF

        不对I/O进行缓冲。每个stdio库函数将立即调用write()或者read(),并且忽略buf和size参数,可以分别指定两个参数为NULL和0.stderr默认属于这一类型,从而确保错误能立即输出。

_IOLBF

        采用行缓冲I/O。指代终端设备的流默认属于这一类型。对于输出流,在输出一个换行符(除非缓冲区已经填满)前将缓冲数据。对于输入流,每次读取遗憾数据。

_IOFBF

        采用全缓冲I/O。单次读、写数据(通过read()或write()系统调用)的大小与缓冲区相同。指代磁盘的流默认采用此模式。

下面的代码演示了setvbuf()函数的用法:

#define BUF_SIZE 1024
static char buf[BUF_SIZE];

if(setvbuf(stdout,buf,_IOFBF,BUF_SIZE) !=0)
{
    perror("setvbuf:");
    return -1;
}

        注意:setvbuf()出错时返回非0值(不一定是-1)

setbuf()函数构建与setvbuf()智障,执行了类似任务。

#include <stdio.h>
void setbuf(FILE *stream,char *buf);

        setbuf(fd,buff)调用除了不反悔函数结果外,就相当于:

setvbuf(fd,.buf,(buf != NULL)? _IOFBF:_iONBF,BUFSIZ);

        要么将参数buf指定为NULL以标识无缓冲,要么指向由调用者分配的BUFSIZ个字节大小的缓冲区。(BUFSIZ定义于<stdio.h>头文件中。glibc库实现将此常量定义为一个典型值8192.)

        setbuffer()函数类似于setbuf()函数,但允许调用者指定buf缓冲区大小。

#define _BSD_SOURCE
#include <stdio.h>
void setbuffer(FILE *stream,char *buf,size_t size);

        对setbuff(fp.buf,size)的调用相当于如下调用:

                setvbuf(fp,buf,(buf != NULL)?_IOFBF:IONBF,size);

SUSv3并卫队setbuffer()函数加以定义,但是大多数UNIX实现均支持它。

刷新stdio缓冲区

        无论当前采用何种缓冲区模式,在任何时候,都可以使用fflush()库函数强制将stdio输出流中的数据(即通过write())刷新到内核缓冲区中。此函数会刷新指定stream的输出缓冲区。

#include <stdio.h>

int fflush(FILE *stream);
            Returns 0 on success,EOF on error

        若参数stream为NULL,则fflush()将刷新所有的stdio缓冲区。

        也能将fflush()函数应用于输入流,这将丢弃业已缓冲的输入数据。(当程序下一次尝试从流中读取数据时,将重新装满缓冲区。)

        当关闭相应流时,将自动刷新其stdio缓冲区。

        在包括glibc库在内的许多C函数库实现中,若stdin和stdout指向已中断,那么无论何时从stdin中读取输入时,都将隐含调用一次fllush(stdout)函数。这将刷新写入stdout的任何提示,但不包括终止换行符(比如,printf("Date:"))。然而,SUSv3和C99并未规定这一行为,也并非所有的C语言函数库都实现了这一行为。要确保程序的可移植性,应用应使用显示的fflush(stdout)调用来确保显示这些提示。

        若打开一个流同时用于输入输出,则C99标准中提出了亮相要求。首先,一个输出操作不能紧跟一个输入操作,必须在二者之间调用fflush()函数或者一个文件定位函数(fseek()、fsetopts()或者rewind())。其次一个输入操作不能紧跟一个输出操作,必须在二者之间调用一个文件定位函数,除非输入操作遭遇函数结尾。 

 13.3 控制问价I/O的内核缓冲

        强制刷新内核缓冲区到输出文件是可能的。这有时很有必要,例如,当应用程序(诸如数据库的日志进程)要确保在继续操作前将输出真正写入磁盘(或者至少写入磁盘的硬件高速缓存中)。

        在描述用于控制内核缓冲的系统调用之前,有必要先熟悉以下SUSv3中的相关定义。

同步I/O数据完整性和同步I/O文件完整性

        SUSv3将同步I/O完成定义为:某一I/O操作,要么已成功完成到磁盘的数据传递,要么被诊断为不成功。

        SUSv3定义了两种不同类型的同步I/O完成,二者之间的区别涉及用于描述文件的元数据(关于数据的数据),亦即内核针对文件而存储的数据。14.4节在描述文件i-node时将详细讨论文件的元数据,但就目前而言,了解文件元数据包含了些什么,诸如文件属主、属组、文件权限、文件大小、文件(硬)链接数量,表明文件最近访问、修改以及元数据发生变化的时间戳,指向文件数据块的指针,就足够了。

        SUSv3定义的第一种同步I/O完成类型是 sysnchronized I/O data integrity completion,旨在确保针对文件的一次更新传递了足够的信息(到磁盘),以便于之后对数据的获取。

  • 就读操作而言,这意味着被请求的文件数据已经(从磁盘)传递给进程。若存在任何影响到所请求数据的挂起操作,那么在执行读操作之前,会将这些操作传递到磁盘。
  • 就写操作而言,这意味着写请求所指定的数据已经传递(至磁盘)完毕,且用于获取数据的所有文件元数据也已传递(至磁盘)完毕。此处的要点在于要获取文件数据,并非需要传递所有经过修改的文件元数据属性。发生修改的文件元数据中需要传递的属性之一是文件大小(如果写操作确实扩展了文件)。相形之下,如果是文件时间戳发生了变化,就无需在下次获取数据前将其传递到磁盘。

        Sysnchronized I/O file integrity completion是SUSv3定义的另一种同步I/O完成,也是上述sysnchronized I/O data integrity completion的超集。该I/O完成模式的区别在于对文件的一次更新过程中,要将所有发生更新的文件元数据都传到磁盘上,即使有些在后续对文件数据的读操作中并不重要。

用于控制文件I/O内核缓冲的系统调用

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

#include <unistd.h>
int fsync(int fd);
            Returns 0 on success, or -1 on error

        仅对磁盘设备(或者至少是高速缓存)的传递完成后,fsync()调用才会返回。

         fdatasync()低通调用的运作类似于fsync(),只是强制将文件处于sysnchronized I/O data integrity completion的状态。

#include <unistd.h>
int fdatasync(int fd);
            Returns 0 on success, or -1 on error

 fdatasync()可能会减少对磁盘操作的次数,由fsync()调用请求的两次变为一次。例如,若修改了文件数据,而文件大小不变,那么调用fdatasync()只强制进行了数据更新。(前面已然述及,针对sysnchronized I/O data  completion状态,如果是诸如最近修改时间戳之类的元数据属性发生了变化,那么是无需传递到磁盘的。)相比之下,fsync()调用会强制将元数据传递到磁盘上。

        对某些应用而言,以这种方式来减少磁盘I/O操作的次数是很有用的,比如对性能要求极高,而对某些元数据(比如时间戳)的准确性要求不高的应用。当应用程序同时进行多处文件更新时,二者存在相当大的性能差异,因为文件数据和元数据通常主流在磁盘的不同区域,更新这些数据需要反复在整个磁盘上执行寻道操作。

        Linux2.2以及更早版本的内核将fdatasync()实现对fsync()的调用,因而性能也为获得提升。

        始于内核2.6.17,Linux 提供了非标准的系统调用sync_file_range(),当刷新文件数据时,该调用提供比fdatasync()调用更为精确的控制。调用者能指定待刷新的wenjianquyu-,并且还能指定标志,以控制该系统调用在遭遇写磁盘时是否阻塞。

        sync()系统调用会使包含更新文件信息的所有内核缓冲区(即数据块、指针快、元数据)刷新到磁盘上。

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

        在Linux实现中,sync()调用仅在所有数据已传递到磁盘上(或者至少高速缓存)时返回,然而,SUSv3却允许sync()实现只是简单调度以下I/O传递,在动作未完成之前即可返回。

        若内容发生变化的内核缓冲区在30秒内未经显式方式同步到磁盘上,则一条长期运行的内核线程就会确保将其刷新到磁盘上。这一做法为了规避缓冲区与相关磁盘文件内容长期处于不一致状态(以至于在系统崩溃时发生数据丢失)的问题。在Linux版本中,该任务由pdflush内核线程执行。(在Linux 2.4版本中,则由kupdated内核线程执行。)

        文件/proc/sys/vm/dirty_expire_centises谷底定了在pdflush刷新之前藏缓冲区必须达到的“年龄”(以1%秒为单位)。位于同一目录下的其他文件控制了pdflush操作的其他方面。

使所有写入同步:O_SYNC

        调用open()函数时如指定O_SYNC标志,则会使所有后续输出同步(synchronous).

        fd=open(pathname,O_WRONLY | O_SYNC);

        调用open()后,每个write()调用会自动将文件数据和元数据刷新到磁盘上(即按照Synchoronized I/O file interity completion的要求执行写操作)。

O_SYNC对性能的影响

        采用O_SYNC标志(或者频繁调用fsync()、fdatasync()或sync())对性能影响极大。表13-3所示为采用不同缓冲区大小,在有、无O_SYNC标识的情况下将一百万字节写入一个(位于ext2文件系统上的)新创建文件所需要的时间。运行(随本书一同发行源码中的filebuff/write_bytes.c程序)结果取自于vanila2.6.30内核以及块大小为4096字节的ext2文件系统。每行数据均为在给定缓冲区大小下运行20次的平均值。

        从表中可以看出,O_SYNC标志使运行总用时大为增加----在缓冲区为1字节的情况下,运行时间相差1000多倍。还要注意,以O_SYNC标志执行写操作时运行总时和CPU时间之间的巨大差异。这是因为系统在将每个缓冲区中数据向磁盘传递时会把程序阻塞起来。

        表13-3所示的结果还略去了使用O_SYNC时影响性能的一个深层次因素。现代磁盘驱动器军内置大型高速缓存,而默认情况下,使用O_SYNC只是将数据传递到该缓存中。如果禁用磁盘上的高速缓存(使用命令 hdparm -W0),那么O_SYNC对性能的影响将变得更为极端。在缓冲区为1字节的情况下,运行总用时从1030秒攀升到16000秒左右。而当缓冲区大小为4096字节时,运行总用时也会从0.34秒上升到4秒。

         总之,如果需要强制刷新内核缓冲区,那么在设计应用程序时就应考虑是否可以使用大尺寸的write()缓冲区,或者在调用fsync()或fdatasync()时谨慎行事,而不是在打开文件时就是用O_SYNC标志。

O_DSYNC和O_RSYNC标志

        SUSv3规定了两个与同步I/O相关的更为细化的打开文件状态标志:O_DSYNC和O_RSYNC 。

        O_DSYNC标志要求写操作按照Synchronized I/O data integrity complete 来执行(类似于fdatasync())。与之相映成趣的是O_SYNC标志,遵从Synchronized I/O file integrity complete(类似于fsync()函数)。

        O_RSYNC标志是与O_SYNC标志或O_DSYNC标志配合一起使用的,将这些标志对写操作的作用结合到读操作中。如果在打开文件时同时指定O_DSYNC和O_RSYNC 标志,那么就意味着遵照Synchronized I/O data integrity complete 的要求来完成所有后续读操作(即,在执行读操作之前,像执行O_DSYNC标志一样完成所有待处理的写操作)。而在打开文件时指定O_SYNC和O_RSYNC,则意味着会遵照Synchronized I/O file integrity complete的要求来完成所有后续读操作(即,在执行读操作之前,像执行O_SYNC标志一样完成所有待处理的写操作)。

        2.6.33版本之前的Linux 内核并未实现O_DSYNC和O_SYNC标志。glibc头文件当时只是将这些常量定义为O_SYNC标志。(以上描述实际上不适用于O_RSYNC,因为O_SYNC与读操作无关。)

        始于2.6.33版本,Linux内核实现了O_DSYNC标志的功能,而O_RSYNC标志的功能则可望在未来的版本中添加。

13.4 I/O缓冲小结

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

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

13.5 就I/O模式向内核提出建议

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

#include _XOPEN_SOURCE 600
#include <fcntl.h>

int posix_fadvise(int fd,off_t offset,off_set len, int advice);
                Returns 0 on success,or positive error number on error

        内核可以(但不必非要)根据posix_fadvise()所提供的信息来优化对缓冲区高速缓存的使用,进而提高进程和整个系统的性能。调用posix_fadvise()对程序语义无影响。

        参数fd所指为一文件描述符,调用期望通知内核进程对fd指代文件的访问模式。参数offset和len确定了建议所适用的文件区域。offset指定了区域起始的偏移量,len指定了区域的大小(以字节数为单位)。len为0标识从offset开始,直至文件结尾

        参数advice表示进程期望对文件采取的访问模式。具体为下列参数之一:

POSIX_FADV_NORMAL

        进程对方问模式并无特别建议。如果没有建议,这就是默认行为。在Linux中,该操作将文件预读窗口大小设置为默认值(128KB)。

POSIX_FADV_SEQUENTIAL

        进程预计会从低偏移量顺序读取数据。在Linux中,该操作将文件于都窗口大小置为默认值的两倍。

POSIX_FADVRANDOM

        进程预计以随即顺序访问数据。在Linux中,该选项会禁用文件预读。

POSIX_FADV_WILLNEED

        进程预计会在不久的将来访问指定的文件区域。内核将有offset和len指定区域的文件数据预先填充到缓冲区高速缓存中。后续对该文件的read()掉哦那个将不会阻塞磁盘I/O,只需从缓冲区高速缓存中抓取数据即可。对于从文件读取的数据在缓冲区高速缓存中能保留多长时间,内核并无保证。如果其他进程或内核的活动对内存存在强劲需求,那么最终会重用到这些页面。换言之,如果内存压力高,程序员就应该确保posix_fadvise()调用和后续read()调用间的总运行时长较短。(Linux特有的系统调用readahead()提供了与POSIX_FADV_WILLNEED操作等效的功能。)

POSIX_FADV_DONTNEED

        进程预计在必纠的将来将不会访问文件指定的文件区域。这一操作给内核的建议时释放相关的高速缓存页面(如果存在的话)。在Linux中,该操作分为两步执行。首先,如果底层设备目前没有挤满一系列排队的写操作请求,那么内核会对指定区域中已修改的页面进行刷新。之后,内核会尝试释放该区域的高速缓存页面。仅当该区域中已修改的页面在第一步中成功写入底层设备时,第二部才可能操作成功,也就是说,在该设备的写入操作请求没有发生拥塞的情况下。因为应用程序无法控制设备的拥塞(congestion),所以要确保释放告诉缓存页面,变通的方式之一狮子啊POSIX_FADV_DONTNEED操作之前对指定的参数fd调用sync()或fdatasync().

POSIX_FADV_NOREUSE

        进程预计会一次性的访问指定文件区域,不再复用。这等于提示内核对指定区域访问一次后即可释放页面。在Linux中该操作目前不起作用。

        对posix_fadvise()的规范时SUSv3新增内容,并非所有UNIX实现都支持该接口,Linux内核从2.6版本开始提供posix_fadvise()。

13.6 绕过缓冲区高速缓存:直接I/O

        始于内核2.4,Linux允许应用程序在执行磁盘I/O操作时绕过缓冲区高速缓存,从用户空间直接将数据传递到文件或磁盘设备。有时也成为直接I/O(direct I/O)或者裸I/O(raw I/O)。

        此处的描述细节为Linux 所特有,SUSv3并未对其进行规范。尽管如此,大多数UNIX实 现均对设备和文件提供了某种形式的直接I/O访问。

        有时会将直接I/O误认为获取快速I/O性能的一种手段。然而,对于大多数应用而言,使用直接I/O可能会大大降低性能。这是因为为了提高I/O性能,内核针对缓冲区高速缓存做了不少优化,其中包括:按顺序预读取,在成簇磁盘块上执行I/O,允许访问同一文件的多个进程共享告诉缓存的缓冲区。应用如使用了直接I/O将无法收益于这些优化举措。直接I/O只适用于有特定I/O需求的应用。例如数据库系统,其高速缓存和I/O优化机制均自成一体,无需内核消耗CPU时间和内存去完成相同任务。

        可针对一个单独文件或设备(比如一块磁盘)直接执行I/O。要做到这点,需要在调用open()打开文件或设备时指定O_DIRECT标志。

        O_DIRECT标志自内核2.4.10开始有效,并非所有Linux文件系统和内核版本都支持该标志。绝大多数原生(native)文件系统都支持O_DIRECT,但许多非UNIX文件系统(比如VFAT)则不支持。对于所关注的文件系统,有必要进行相关测试,(若文件系统不支持O_DIRECT,则open()将失败并返回错误号EINVAL)或是阅读内核源码,以此来加以验证。

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

直接I/O的对其限制

        因为直接I/O(针对磁盘设备和文件)涉及对磁盘的直接访问,所以在执行I/O时,必须尊说一些限制。

  • 用于传递数据的缓冲区,其内存边界必须对齐为块大小的整数倍。
  • 数据传输的开始点,亦即文件和设备的偏移量,必须是块大小的整数倍。
  • 待传递数据的长度必须是块大小的整数倍。

        不遵守上述上述任意限制均将导致EINVAL错误。在上述列表中,块大小(block size)之设备的物理块大小(通常为512字节)。

示例程序

        程序清单13-1提供了一个使用O_DIRECT标志打开一个文件读取数据的简单例子。该程序可指定多达4个命令行参数,一次为要读取的文件、要从文件中读取的字节数,都之前在文件中定位(seek)的偏移量和传递给read()的数据缓冲区对齐。最后两个为可选参数,默认值分别为0字节和4096字节。下面是运行该程序的一些示例:

程序清单13-1:使用O_DIRECT跳过缓冲区高速缓存  

#define _GNU_SOURCE  /*Obtain O_DIRECT definition from <fcntl.h>*/
#include <fcntl.h>
#include <malloc.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd;
    ssize_t numRead;
    ssize_t length,alignment;
    off_t offset;
    void *buf;
    if(argc <3 || strcmp(argv[1],"--help") ==0)
    {
        printf("%s file length [offset [alignment]]\n",argv[0]);
        return -1;
    }
    length = atol(argv[2]);
    offset = (argc >3)?atol(argv[3]):0;
    alignment = (argc>4)?atol(argv[4]):4096;

    fd = open(argv[1],O_RDONLY | O_DIRECT);
    if(fd == -1)
    {
        perror("open:");
        return -1;
    }

     /* memalign() allocates a block of memory aligned on an address that
       is a multiple of its first argument. By specifying this argument as
       2 * 'alignment' and then adding 'alignment' to the returned pointer,
       we ensure that 'buf' is aligned on a non-power-of-two multiple of
       'alignment'. We do this to ensure that if, for example, we ask
       for a 256-byte aligned buffer, we don't accidentally get
       a buffer that is also aligned on a 512-byte boundary. */
       
    buf = (char *)memalign(alignment *2,length+alignment)+alignment;
    if(buf == NULL)
    {
        perror("memalign:");
        return -1;
    }

    if(lseek(fd,offset,SEEK_SET) == -1)
    {
        perror("lseek:");
        return -1;
    }
    numRead = read(fd,buf,length);
    if(numRead == -1)
    {
        perror("read:");
        return -1;
    }

    printf("Read %ld bytes\n",(long)numRead);
    exit(EXIT_SUCCESS);
}

13.7 混合使用库函数和系统调用进行文件I/O

        在同一文件上执行I/O操作时,还可以将系统调用和标准C语言库函数混合使用。dileno()和fdopen()函数有助于完成这一项工作。

#include <stdio.h>
int fileno(FILE *STREAM);
        Returns file descriptor on success, or -1 on error
FILE *fdopen(int fd,const char *mode);
        Returns(new) file pointer on success, or NULL on error

        给定一个(文件)流,fileno()函数将返回相应的文件描述符(即stdio库在该流上已经打开的文件描述符)。随即可以在诸如read()、write()、dup()和fcntl()之类的系统调用中正常使用该文件描述符。

        fdopen函数于fileno()函数的功能相反,给定一个文件描述符,该函数将创建了一个使用该描述符进行文件I/O的相应流。莫得参数于fopen()函数中的mode参数含义相同。例如,r为读,w为写,a为追加。若该参数于文件描述符fd的访问模式不一致,则对fdopen()的掉作用将失败。

        fdopen()函数对常规文件描述符特别有用。正如后续章节提及的,创建套接字和管道的系统调用总是返回文件描述符。为了在这些文件类型上使用stdio库函数,必须势必要那个fdopen()函数来创建相应文件流。

        当使用stdio库函数,并结合系统I/O调用来实现对磁盘文件的I/O操作时,必须将缓冲问题牢记于心。I/O系统调用会直接将数据传递到内核缓冲区高速缓存,而stdio库函数会等到用户空间的流缓冲区填满,在调用write()将其传递到内核缓冲区gaosuhuancun-。请考虑如下向标准输出写入的代码:

print("To man the world os twofold,");
write(STDOUT_FILENO,"in accordance with his twoold attitude.\n",41);

        通常情况下,printf()函数的输出往往在write()函数的输出之后出现。音系代码产生如下输出:

in accordance with his twoold attitude.

To man the world os twofold,

        将I/O系统调用和stdio函数混合使用时,使用fflush()来规避这一问题,是明智之举。也可以使用setvbuf()或者setbuf()使缓冲区失效,但这样做可能会影响应用I/O的性能,因为每个输出操作将引起一次write 系统调用。

13.8总结

        输入输出数据的缓冲由内核和stdio库完成。有时可能希望组织缓冲,但这需要了解其对应用程序性能的隐形。可以使用各种系统调用可库函数来控制内核和stdio缓冲,并执行一次性的缓冲区刷新。

        进程可使用posix_fadvise()函数,可就进程对特定文件可能采取的数据访问模式向内核提出建议。内核可籍此来优化对缓冲区高速缓存的应用,进而提高I/性能。

        在Linux环境下,open()所特有的O_DIRECT标识允许特定应用跳过缓冲区告诉缓存。

        在对同一文件执行I/O操作时,fileno()和fdopen()有助于系统调用和标准C语言库函数的混合使用。给定一个流,fileno()将返回相应的文件描述符,fdopen()则反其道而行之,针对指定的打开文件描述符创建一个新的流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值