出于速度和效率考虑,系统I/O调用(即内核)和标准 C 语言库 I/O 函数(即stdio函数)在操作磁盘文件时会对数据进行缓存。
一、文件I/O的内核缓冲:缓冲区高速缓存
read() 和 write() 系统调用在操作磁盘文件时不会直接发起磁盘访问,而是仅仅在用户空间缓冲区和内核缓冲区高速缓存之间复制数据。
缓冲区大小对 I/O 系统调用性能的影响:
如果与文件发生大量的数据传输,通过采用大块空间缓冲数据,以及执行更少的系统调用,可以极大提高 I/O 性能。
二、stdio 库的缓冲
当操作磁盘文件时,缓冲大块数据以减少系统调用,C 语言函数库的 I/O 函数(比如,fprintf()、fscanf()、fgets()、fputs()、fputc()、fgetc())正是这么做的。因此,使用 stdio 库可以使编程者免于自行处理对数据的缓冲。
1、设置 stdio 流缓冲模式
调用 setvbuf() 函数,可以控制 stdio 库使用缓冲的形式。
#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
1、参数
1、stream
标识要修改哪个文件流的缓冲,stream 为 fopen() 函数返回值。
打开流后,必须在调用任何其他 stdio 函数之前先调用 setvbuf()。setvbuf() 调用将影响后续在指定流上进行的所有 stdio 操作。
2、buf
如果 buf 不为 NULL,使用 size 标识缓存器大小。
如果 buf 为 NULL,那么 stdio 库会为 stream 自动分配一个缓冲区。
3、mode
_IONBF:不对 I/O 进行缓存。stderr 默认属于这一类,从而保证错误能立即输出。
_IOLBF:采用行缓存 I/O。对于输出流,在输出一个换行符前将缓冲数据。对于输入流,每次读取一行数据。
_IOFBF:采用全缓冲 I/O。单次读、写数据的大小与缓冲区相同。
2、返回值
0:执行成功
非0:执行失败
3、其他
#include <stdio.h>
void setbuf(FILE *stream, char *buf);
相当于
setvbuf(stream, buf, (buf != NULL)?_IOFBF:_IONBF, BUFSIZ);
buf 为 NULL 时,表示无缓冲。
buf 不为 NULL 时,指向由调用者分配的 BUFSIZ 个字节大小的缓冲。
BUFSIZ 定义在 <stdio.h> 头文件中。
#include <stdio.h>
void setbuffer(FILE *stream, char *buf, size_t size);
相当于
setvbuf(stream, buf, (buf != NULL)?_IOFBF:_IONBF, size);
2、刷新 stdio 缓冲区
无论当前采用何种缓冲区模式,在任何时候,都可以使用 fflush() 库函数 强制将 stdio 输出流中数据(即通过 write())刷新到内核缓冲区中。
#include <stdio.h>
int fflush(FILE *stream);
1、参数
1、stream
如果 stream 为 NULL,则 fflush() 将刷新所有的 stdio 缓冲区。
如果 stream 不为 NULL,刷新指定 stdio 流。
2、返回值
执行成功,返回0。
执行失败,返回非0。
3、其他
如果将 fflush() 函数应用于输入流,将丢弃已缓冲的输入数据。
当关闭相应流时,将自动刷新其 stdio 缓冲区。
三、控制文件I/O的内核缓冲
强制刷新内核缓冲区到输出文件是可能的,也是必要的。
1、同步I/O数据完整性和同步I/O文件完整性
同步I/O完成:某一 I/O 操作,要么已经成功完成到磁盘的数据传递,要么被诊断为不成功。
SUSv3定义了两种不同类型的同步 I/O 完成,二者之间区别涉及用于描述文件的元数据,亦即内核针对文件而存储的数据。
1、同步 I/O 数据完整性:确保针对文件的一次更新传递了足够的信息(到磁盘),以便于之后对数据的获取。
- 就读操作而言,这意味着被请求的文件已经(从磁盘)传递给进程。
- 就写操作而言,这意味着写请求所指定的数据已传递(至磁盘)完毕,且用于获取数据的所有文件元数据已传递(至磁盘)完毕。
2、同步 I/O 文件完整性:是同步 I/O 数据完整性的超级。对于文件的一次更新过程中,要将所有发生更新的文件元数据都传递到磁盘上,即使有些在后续对文件数据的读操作中并不需要。
2、控制文件I/O内核缓冲的系统调用
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
fsync() 系统调用将缓冲数据和已打开文件描述符 fd 相关的所有元数据都刷新到磁盘上。调用 fsync() 会强制文件处于同步 I/O 文件完整性状态。
fdatasync() 系统调用的运作类似于 fsync(),只是强制文件处于 同步 I/O 数据完整性状态。
fsync() 系统调用:更新数据 ,并且更新元数据。
fdatasync() 系统调用:更新数据,不更新元数据。
fsync() 系统调用会使包含更新文件信息的所有内核缓冲区(数据块、指针块、元数据等)刷新到磁盘上。
3、使所有写入同步:O_SYNC
调用 open() 函数时如指定 O_SYNC 标志,则会使所有后续输出同步。
fd = open(pathname, O_WRONLY|O_SYNC);
调用 open() 后,每个 write() 调用会自动将文件数据和元数据刷新到磁盘上。
4、O_SYNC 对性能的影响
采用 O_SYNC 标志(或者频繁调用 fsync()、fdatasync() 或 sync() )对性能影响极大。
表 13-3 所示为采用不同缓冲区大小,在有、无 O_SYNC 标识的情况下将一百万字节写入一个新创建文件所需要的时间。
- 使用 O_SYNC 标志使运行总用时大为增加。
- 使用 O_SYNC 标志执行写操作时,运行总用时和 CPU 时间的巨大差异(原因:向磁盘传递数据时,程序会阻塞)。
总之,在程序设计时,如果需要强制刷新内核缓冲区,使用大尺寸 write() 缓冲区,或者调用 fsync() 或 fdatasync(),而不是在打开文件时设置 O_SYNC 标志。
5、O_DSYNC 和 O_RSYNC 标志
O_DSYNC:要求写操作按照同步 I/O 数据完整性来执行。
O_SYNC:要求写操作按照同步 I/O 文件完整性来执行。
O_RSYNC:与 O_DSYNC 和 O_SYNC 标志一起使用,将这些标志对写操作作用结合到读操作中。
四、I/O 缓冲小结
五、就 I/O 模式向内核提出建议
posix_fadvise() 系统调用允许进程就自身访问文件数据时可能采取的模式通知内核。
#include <fcntl.h>
int posix_fadvise(int fd, off_t offset, off_t len, int advice);
内核可以(但不必非要)根据 posix_fadvise() 所提供的信息来优化对缓冲区高速缓存的使用,进而提高进程和整个系统的性能。
1、参数
1、fd
fd 指定文件描述符。
2、offset
offset 指定了区域起始的偏移量。
3、len
len 指定了区域的大小。如果 len 为 0 表示从 offset 开始,直至文件结尾。
4、advice
- POSIX_FADV_NORMAL:进程对访问模式无特别建议。Linux 将文件预读窗口大小设置为默认值。
- POSIX_FADV_SEQUENTIAL:进程预计会从低偏移量到高偏移量顺序读取数据。Linux 将文件预读窗口大小设置为默认值的两倍。
- POSIX_FADV_RANDOM:进程预计以随机顺序访问数据。Linux 禁用文件预读。
- POSIX_FADV_WILLNEED:进程预计会在不久的将来访问指定的文件区域。内核将由 offset 和 len 指定区域的文件数据预先填充到缓冲区高速缓存中。
- POSIX_FADV_DONTNEED:进程预计会在不久的将来不会访问指定的文件区域。这一操作将给内核的建议是释放相关的高速缓存页面(如果存在的话)。
- POSIX_FADV_NOREUSE:进程预计会一次性地访问指定文件区域,不在复用。这等于提示内核对指定区域访问一次后即可释放页面。
2、返回值
成功返回 0,错误返回错误码。
六、绕过缓冲区高速缓存:直接 I/O
始于内核 2.4,Linux 允许应用程序在执行磁盘 I/O 时绕过缓冲区高速缓存,从用户空间直接将数据传递到文件或磁盘设备。有时也称此为直接 I/O 或者裸 I/O。
通过在调用 open() 系统调用时指定 O_DIRECT 标志,开启直接 I/O。
1、直接I/O的对齐限制
因为直接 I/O (针对磁盘设备和文件)涉及对磁盘的直接访问,所以执行 I/O 时,必须遵守一些限制。
- 用于传递数据的缓冲区,其内存边界必须对齐为块的整数倍。
- 数据传输的开始点,亦即文件和设备的偏移量,必须是块大小的整数倍。
- 待传递数据的长度必须是块大小的整数倍。
七、混合使用库函数和系统调用进行文件 I/O
在同一文件上执行 I/O 操作时,还可以将系统调用和标准 C 语言库函数混合使用。fileno() 和 fdopen() 函数有助于完成这一工作。
#include <stdio.h>
int fileno(FILE *stream);
给定一个(文件)流,fileno() 函数返回相应的文件描述符。随即可以在 read()、write() 等系统调用中正常使用该文件描述符。
#include <stdio.h>
FILE *fdopen(int fd, const char *mode);
给定一个文件描述符,fdopen() 函数创建一个使用该描述符进行文件 I/O 的相应流。mode 参数和 fopen() 函数中 mode 含义相同。若该参数与文件描述符 fd 的访问模式不一致,fdopen() 调用将失败。
fdopen() 使用场景:创建套接字和管道的系统调用总返回文件描述符,为了在这些文件类型上使用 stdio 库函数,必须使用 fdopen() 函数来创建相应文件流。