Linux中的文件I/O缓冲

近日阅读《Linux/UNIX系统编程手册》第13章 - 文件I/O缓冲,有些收获,是以此文以记之。以往只知道Linux的I/O操作有缓冲机制,但始终不知道具体的缓冲流程及使用方法。读完本章节后方才有种恍然大悟的感觉,久违的因读书而觉得舒爽的感觉。

好了,进入正题,下图摘自原文(13.4-I/O缓冲小结),此图概括了stdio库及内核针对输出文件所用的缓冲以及各类缓冲的控制机制。本文依据此图逐步揭开文件I/O缓冲的面纱。

I/O缓冲

I/O缓冲的类型

在使用stdio库中文件写操作相关的函数(如:printf, fputc, fputs, fwrite)时,待写入数据从用户空间内存到内核空间内存、再到磁盘会经过以下3类缓冲

  1. stdio库的缓冲区
  2. 文件I/O的内核缓冲区的高速缓存
  3. 磁盘驱动器内置高速缓存

下面逐一介绍。

stdio库的缓冲

stdio buffer

如上图所示,stdio库实现的缓冲位于用户空间内存当中,该缓冲区A会缓冲大块的文件数据以减少系统调用(如: read, write)。

需要知道的是,stdio库函数内部会调用底层的系统调用,如fgets调用readfputs调用write。但是在调用之前,

  • 对于读操作,库函数会先检查缓冲区A内是否已有所需数据,如果有则直接从缓冲区A读取;否则先执行系统调用read,从内核缓冲区B中读取数据到缓冲区A,然后从缓冲区A读取数据
  • 对于写操作,库函数会先检查缓冲区A是否还有空闲,如果有则先存入缓冲区A;否则先执行库函数fflush,将缓冲区A中数据刷新至内核缓冲区B,然后将当前待写入数据写入缓冲区A

对于stdio库的缓冲数据,在执行库函数之后的某一时刻,系统会通过fflush函数将数据刷新至内核缓冲区。当然,我们也可以手动执行fflush函数强制刷新数据至内核缓冲区。

fflush定义如下:

#include <stdio.h>
int fflush(FILE *stream);
  • fflush(stdin)会清空缓冲区内的标准输入数据
  • fflush(stdout)会将缓冲区内的写入数据刷新至终端输出
  • stderr默认不使用缓冲
  • fflush(stream)将文件流stream的缓冲数据刷新至内核缓冲区

文件I/O的内核缓冲

不管使不使用stdio库函数,最终都会直接或间接的调用open, read, write, lseek等系统调用读写文件I/O,那么系统就会在写操作后将数据存入内核缓冲区,但此时还并未存入磁盘。

也就是说,在执行write后,函数直接返回,但数据只是存在内核缓冲区中。当有新的读取请求时,会先在内核缓冲区中查找,如果有则直接返回;如果没有则先从磁盘读入大块数据至内核缓冲区,这样可以减少磁盘读写操作。毕竟,相比于系统调用和用户空间与内核空间之间的数据传输,磁盘读写所花费的时间要长得多。

若内容发生变化的内核缓冲区在30s内未经显式方式同步到磁盘上,则一条长期运行的内核线程会确保将其刷新到磁盘上。

不同版本的linux,其内核线程不一样,可以使用指令ps aux |grep flush粗略查看一下。

# 查看内核线程刷新之前脏缓冲区必须达到的时间,单位:0.01s
$ cat /proc/sys/vm/dirty_expire_centisecs
3000

# 查看内核线程执行周期,单位:0.01s
$ cat /proc/sys/vm/dirty_writeback_centisecs
500

磁盘驱动器内置缓冲

内核缓冲区的数据在真正存入物理磁盘前,会先存入磁盘驱动器内置的高速缓存,之后在某一时刻写入磁盘。具体过程没有深究过,目前只需知道还有这么一层缓冲区即可。

控制文件I/O缓冲

通过某些方式,我们可以控制I/O缓冲机制,其中包括修改缓冲区大小、缓冲模式,禁用缓冲,强制刷新缓冲数据等。

控制stdio库的缓冲

对于stdio库分配的缓冲区,在默认情况下会分配BUFSIZ大小的缓冲区,该参数值的大小说法不一,但据原文说明,在glibc中定义为8192字节。

stdio流的缓冲模式分以下三类:

  • _IONBF - 不缓冲
  • _IOLBF - 行缓冲,针对终端设备,在输出一个换行符前缓冲数据。对输入流,每次读取一行数据
  • _IOFBF - 全缓冲,单词读、写数据(read, write)的大小与缓冲区大小一致,磁盘默认使用此模式

我们可以通过库函数setvbuf, setbuffer, setbuf三者之一设置stdio流的缓冲模式。

#include <stdio.h>
void setbuf(FILE *stream, char *buf);
void setbuffer(FILE *stream, char *buf, size_t size);
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

setvbuf函数中的mode对应的就是缓冲模式,可选项为_IONBF, _IOLBF, _IOFBFbufsize分别对应指定的缓冲区指针及缓冲区大小。当然,当为不缓冲模式时,函数将忽略bufsize这两个参数。示例如下:

#define BUF_SIZE 4096
static char buf[BUF_SIZE];
FILE *fp;

fp = fopen("test.txt", 'w');
if(setvbuf(fp, buf, _IOFBF, BUF_SIZE) !=0 )
    exit(EXIT_FAILURE);

从下面的uClibc源码可以看出,setbuffer, setbuf仅仅是对setvbuf的简单封装。但要注意的是,SUSv3标准并未对setbuffer函数加以定义,在使用时需要加上宏定义_BSD_SOURCE

void setbuffer(FILE * __restrict stream, register char * __restrict buf, size_t size)
{
#ifdef __STDIO_BUFFERS
    setvbuf(stream, buf, (buf ? _IOFBF : _IONBF), size);
#endif
}

void setbuf(FILE * __restrict stream, register char * __restrict buf)
{
#ifdef __STDIO_BUFFERS
    setvbuf(stream, buf, ((buf != NULL) ? _IOFBF : _IONBF), BUFSIZ);
#endif
}

如果想要禁用缓冲,通常可以使用setbuf(stream, NULL)实现。但通常不推荐这么做,应该合理组织代码,在特定情况使用fflush刷新数据,这样可以在有效利用用户空间缓冲作用的同时,减少系统调用,适宜的存储数据至内核缓冲区。

控制文件I/O的内核缓冲

在某些情况下,我们可能需要强制将内核缓冲区内的数据刷新至磁盘,而不必等待内核线程等待特定时间后才写入。此时主要有两种选择

  1. 使用fsyncfdatasync, sync系统调用中的某一个将内核缓冲区的数据强制写入磁盘
  2. O_SYNC同步方式调用open打开文件,此后每次读写操作都会自动立即写入磁盘

fsync

#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
void sync(void);

fsync保证同步I/O文件完整性fdatasync保证同步I/O数据完整性。两个完整性的区别在于前者会将所有更新的文件元数据写入磁盘,后者不会传递所有经过修改的文件元数据属性(如:时间戳)。

fsyncfdatasync均是刷新指定文件流数据,而sync()函数会更新所有内核缓冲区数据至磁盘,对应shell指令sync

O_SYNC

fsync等系统调用需要被手动调用,仅在被调用处起作用;而以O_SYNC方式open的文件流,后续所有写操作都将把数据直接写入磁盘。

不到万不得已还是不要以这种方式打开文件流,因为这将严重影响性能,当write缓冲区较小时尤为突出。

禁用磁盘高速缓存

按照书上描述,使用hdparm -W0可以禁用磁盘上的高速缓存,但我没有实际试过。

直接I/O

当以O_DIRECT方式open文件流时,数据流会绕过内核缓冲区高速缓存,从用户空间直接传入文件或磁盘,此类过程称为直接I/O

使用直接I/O对I/O操作有诸多对齐限制,主要保证读取和写入时,偏移量、数据长度以及内存边界需要是块设备基本单元(通常为512字节)的整数倍,否则会导致EINVAL错误。

使用直接I/O时,可以结合memalign函数动态分配内存。

小结

使用stdio库函数可以利用该库实现的用户空间缓冲区减少系统调用;readwrite等系统调用默认不与磁盘直接传递数据,而是经过文件I/O的内核缓冲区作为中转,以此减少磁盘操作。

通过fflush函数可以将stdio流的缓冲区数据刷新至内核缓冲区;通过fsync, fdatasync函数可以将内核缓冲区数据刷新至磁盘。

使用setvbuf, setbuf等函数可以设置stdio流的缓冲模式,甚至禁用缓冲;以O_SYNC方式打开文件,可以在立即将数据同步至磁盘文件。

参考文献

  1. 《Linux/UNIX系统编程手册》 第13章 文件I/O缓冲
  2. buffering in standard streams