出于速度和效率考虑,系统IO调用和标准C语言库函数在操作磁盘文件时会对数据进行缓冲。
1、文件IO的内核缓冲
read和write系统调用在操作磁盘文件时不会直接发起磁盘访问,而仅是在用户空间缓冲区和内核缓冲区之间复制数据。
write调用后会立即返回,在后续某个时刻内核才会将缓冲区中的数据写入磁盘,即系统调用和磁盘操作并不同步。如果在此期间,另一进程试图读取文件的这几个字节,那么内核将自动从缓冲区中提供这些数据,而不是从文件中读取。
同理,对输入而言,内核从磁盘中读取数据并存储到内核缓冲区中。read调用将从该缓冲区中读取数据,直至把缓冲区读完,这时内核会将文件的下一段内容读入缓冲区(内核通常会执行预读)。
2、stdio库的缓冲
当操作磁盘文件时,缓冲大块数据以减少系统调用,C语言函数库的IO函数正是这么做的,使用stdio库可以免于自行处理对数据的缓冲。
设置缓冲模式
调用setvbuf函数,可以控制stdio库使用缓冲的形式。
#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);成功返回0,错误返回非0
参数stream标识要修改哪个流的缓冲。打开流后,必须在调用其它stdio函数之前调用setvbuf,setvbuf调用将影响后续在指定流上的所有stdio操作。
参数buf和size则指定针对参数stream要使用的缓冲区的位置和大小, buf为NULL时stdio库会自动分配。
参数mode指定了缓冲类型,可取下列值之一:
- _IONBF
不缓冲。每个stdio库函数将立即调用write或read,并且忽略buf和size参数。stderr默认属于这一类型,从而保证错误立即输出。 - _IOLBF
行缓冲。指代终端设备的流默认属于这一类型。对于输出流,在输出一个换行符时将刷新缓冲区;对于输入流,每次读取一行数据。 - _IOFBF
全缓冲。每次读写数据(通过read和write)的大小与缓冲区相同(即缓冲区满了才会调用write一次性写入磁盘),指代磁盘的流默认采用此模式。
无论当前采用何种缓冲模式,任何时候都可以使用fflush库函数强制刷新缓冲区到内核缓冲区。当关闭流时,也会刷新其缓冲区。
#include <stdio.h>
int fflush(FILE *stream);
3、控制文件IO的内核缓冲
有时,我们需要强制刷新内核缓冲区数据到磁盘文件中。比如数据库等应用,需要确保继续操作前将输出真正写入磁盘。
控制内核缓冲的系统调用
fsync
#include <unistd.h> int fsync(int fd);成功返回0,失败返回-1
fsync系统调用将使缓冲数据和与打开文件描述符fd相关的所有元数据都刷新到磁盘上。fsync会同时传递修改的文件数据和文件元数据到磁盘,且在数据传递到磁盘完成后才会返回。
fdatasync
#include <unistd.h> int fdatasync(int fd);成功返回0,失败返回-1
如果修改文件并未导致文件大小发生变化,则fdatasync只会同步修改的文件数据到磁盘,而不会同步文件元数据。相比fsync会少一次IO操作。
sync
#include <unistd.h> void sync(void);
sync系统调用会使包含更新文件信息的所有内核缓冲区(即数据块,指针块,元数据等)刷新到磁盘上。在Linux上,sync调用会在所有数据传递到磁盘上后返回。而SUSv3标准却允许sync实现只是简单高度一下IO传递,在同步未完成前就可以返回。
O_SYNC标志
调用open函数时如指定O_SYNC标志,则会使所有后续输出同步,每个write调用会自动将文件数据和元数据刷新到磁盘上。
注:采用O_SYNC标志或频繁调用fysnc、fdatasync、sync对性能的影响极大。