Linux中的文件I/O缓冲
近日阅读《Linux/UNIX系统编程手册》第13章 - 文件I/O缓冲,有些收获,是以此文以记之。以往只知道Linux的I/O操作有缓冲机制,但始终不知道具体的缓冲流程及使用方法。读完本章节后方才有种恍然大悟的感觉,久违的因读书而觉得舒爽的感觉。
好了,进入正题,下图摘自原文(13.4-I/O缓冲小结),此图概括了stdio
库及内核针对输出文件所用的缓冲以及各类缓冲的控制机制。本文依据此图逐步揭开文件I/O缓冲的面纱。
I/O缓冲的类型
在使用stdio
库中文件写操作相关的函数(如:printf
, fputc
, fputs
, fwrite
)时,待写入数据从用户空间内存到内核空间内存、再到磁盘会经过以下3类缓冲
- stdio库的缓冲区
- 文件I/O的内核缓冲区的高速缓存
- 磁盘驱动器内置高速缓存
下面逐一介绍。
stdio库的缓冲
如上图所示,stdio
库实现的缓冲位于用户空间内存当中,该缓冲区A会缓冲大块的文件数据以减少系统调用(如: read
, write
)。
需要知道的是,stdio
库函数内部会调用底层的系统调用,如fgets
调用read
,fputs
调用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
, _IOFBF
,buf
和size
分别对应指定的缓冲区指针及缓冲区大小。当然,当为不缓冲模式时,函数将忽略buf
,size
这两个参数。示例如下:
#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的内核缓冲
在某些情况下,我们可能需要强制将内核缓冲区内的数据刷新至磁盘,而不必等待内核线程等待特定时间后才写入。此时主要有两种选择
- 使用
fsync
,fdatasync
,sync
系统调用中的某一个将内核缓冲区的数据强制写入磁盘 - 以
O_SYNC
同步方式调用open
打开文件,此后每次读写操作都会自动立即写入磁盘
fsync
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
void sync(void);
fsync
保证同步I/O文件完整性,fdatasync
保证同步I/O数据完整性。两个完整性的区别在于前者会将所有更新的文件元数据写入磁盘,后者不会传递所有经过修改的文件元数据属性(如:时间戳)。
fsync
与fdatasync
均是刷新指定文件流数据,而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
库函数可以利用该库实现的用户空间缓冲区减少系统调用;read
,write
等系统调用默认不与磁盘直接传递数据,而是经过文件I/O的内核缓冲区作为中转,以此减少磁盘操作。
通过fflush
函数可以将stdio
流的缓冲区数据刷新至内核缓冲区;通过fsync
, fdatasync
函数可以将内核缓冲区数据刷新至磁盘。
使用setvbuf
, setbuf
等函数可以设置stdio
流的缓冲模式,甚至禁用缓冲;以O_SYNC
方式打开文件,可以在立即将数据同步至磁盘文件。
参考文献
- 《Linux/UNIX系统编程手册》 第13章 文件I/O缓冲
- buffering in standard streams
版权声明:本博客所有文章除特殊声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明出处 litreily的博客!