出于速度和效率的考虑,系统
I/O
调用(即文件
I/O
,
open
、
read
、
write
等)和标准
C
语言库
I/O
函数(即标准 I/O
函数)在操作磁盘文件时会对数据进行缓冲。
1. 文件 I/O 的内核缓冲
read()
和
write()
系统调用在进行文件读写操作的时候并不会直接访问磁盘设备,而是仅仅在用户空间缓 冲区和内核缓冲区(kernel buffer cache
)之间复制数据。
比如调用 write()后仅仅只是将这 5 个字节数据拷贝到了内核空间的缓冲区中,拷贝完成之后函数就返回了,在后面的某个时刻,内核会将其缓冲区中的数据写入(刷新)到磁盘设备中,所以由此可知,系统调用 write()与磁盘操作并不是同步的,write()函数并不会等待数据真正写入到磁盘之后再返回。如果在此期间,其它进程调用 read()函数读取该文件的这几个字节数据,那么内核将自动从缓冲区中读取这几个字节数据返回给应用程序。
这个内核缓冲区就称为文件
I/O
的内核缓冲。
2. 文件 I/O 的内核缓冲区刷新函数
Linux
中提供了一些系统调用可用于控制文件
I/O
内核缓冲,包括系统调用
sync()、syncfs()、fsync()以 及 fdatasync()。
1. fsync()函数
系统调用
fsync()
将参数
fd
所指文件的内容数据和元数据写入磁盘,只有在对磁盘设备的写入操作完成 之后,fsync()
函数才会返回,其函数原型如下:
#include <unistd.h>
int fsync(int fd);
参数
fd
表示文件描述符,函数调用成功将返回
0
,失败返回
-1
并设置
errno
以指示错误原因。
2. fdatasync()函数
系统调用
fdatasync()
与
fsync()
类似,不同之处在于
fdatasync()
仅将参数
fd
所指文件的内容数据写入磁盘,并不包括文件的元数据;同样,只有在对磁盘设备的写入操作完成之后,fdatasync()
函数才会返回,其函数原型如下:
#include <unistd.h>
int fdatasync(int fd);
3. sync()函数
系统调用
sync()
会将所有文件
I/O
内核缓冲区中的文件内容数据和元数据全部更新到磁盘设备中,该函 数没有参数、也无返回值,意味着它不是对某一个指定的文件进行数据更新,而是刷新所有文件 I/O
内核缓冲区。其函数原型如下:
#include <unistd.h>
void sync(void);
此外,调用 open()函数时指定一些标志也可以影响到文件 I/O 内核缓冲,也就是比如 O_DSYNC 标志和O_SYNC标志。在调用open()函数时和其他表示的语法一致。
O_DSYNC
标志,其效果类似于在每个
write()
调用之后调用
fdatasync()
函数进行数据同步。
O_SYNC
标志,使得每个
write()
调用都会自动将文件内容数据和元数据刷新到磁盘设备中,其效果类似于在每个 write()
调用之后调用
fsync()
函数进行数据同步。
3. 直接 I/O
从
Linux
内核
2.4
版本开始,
Linux
允许应用程序在执行文件
I/O
操作时绕过内核缓冲区,从用户空间直接将数据传递到文件或磁盘设备,把这种操作也称为直接 I/O
(
direct I/O
)或裸
I/O
(
raw I/O
)。
这种方式,用得比较少,直接 I/O 方式效率、性能比较低,绝大部分应用程序不会使用直接 I/O 方式对文件进行 I/O 操作,通常只在一些特殊的应用场合下才可能会使用,所以感兴趣的朋友可以专门去查阅这一部分的用法。
4. stdio 缓冲
文件
I/O
内核缓冲,是由内核维护的缓冲区,而标准
I/O
所维护的
stdio
缓冲是用户空间的缓冲区,当应用程序中通过标准 I/O
操作磁盘文件时,为了减少调用系统调用的次数,标准
I/O
函数会将用户写入或读取文件的数据缓存在 stdio
缓冲区,然后再一次性将
stdio
缓冲区中缓存的数据通过调用系统调用 I/O
(文件
I/O
)写入到文件
I/O
内核缓冲区或者拷贝到应用程序的
buf
中。
C
语言提供了一些库函数可用于对标准
I/O
的
stdio
缓冲区进行相关的一些设置,包括
setbuf()、setbuffer()以及 setvbuf()。
1. setvbuf()函数
调用
setvbuf()
库函数可以对文件的
stdio
缓冲区进行设置,譬如缓冲区的缓冲模式、缓冲区的大小、起始地址等。其函数原型如下:
#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
函数参数和返回值解释如下:
stream
:
FILE
指针,用于指定对应的文件,每一个文件都可以设置它对应的
stdio
缓冲区。
buf
:
如果参数
buf
不为
NULL
,那么
buf
指向
size
大小的内存区域将作为该文件的
stdio
缓冲区,因为stdio 库会使用
buf
指向的缓冲区,所以应该以动态(分配在堆内存,譬如
malloc
)或静态的方式在堆中为该缓冲区分配一块空间,而不是分配在栈上的函数内的自动变量(局部变量)。如果 buf
等于 NULL
,那么
stdio
库会自动分配一块空间作为该文件的
stdio
缓冲区(除非参数
mode
配置为非缓冲模式)。
mode
:
参数
mode
用于指定缓冲区的缓冲类型,可取值如下:
_IONBF
:
不对
I/O
进行缓冲(无缓冲)。意味着每个标准
I/O
函数将立即调用
write()
或者
read()
,并且忽略 buf
和
size
参数,可以分别指定两个参数为
NULL
和
0
。标准错误
stderr
默认属于这一种类型,从而保证错误信息能够立即输出。
_IOLBF
:
采用行缓冲
I/O
。在这种情况下,当在输入或输出中遇到换行符
"\n"
时,标准
I/O
才会执行文件 I/O
操作。对于输出流,在输出一个换行符前将数据缓存(除非缓冲区已经被填满),当输出换行符时,再将这一行数据通过文件 I/O write()
函数刷入到内核缓冲区中;对于输入流,每次读取一行数据。对于终端设备默认采用的就是行缓冲模式,譬如标准输入和标准输出。
_IOFBF
:
采用全缓冲
I/O
。在这种情况下,在填满
stdio
缓冲区后才进行文件
I/O
操作(
read
、
write
)。 对于输出流,当 fwrite
写入文件的数据填满缓冲区时,才调用
write()
将
stdio
缓冲区中的数据刷入内核缓冲区;对于输入流,每次读取 stdio
缓冲区大小个字节数据。默认普通磁盘上的常规文件默认常用这种缓冲模式。
size
:
指定缓冲区的大小。
返回值:
成功返回
0
,失败将返回一个非
0
值,并且会设置
errno
来指示错误原因。
注意,当
stdio
缓冲区中的数据被刷入到内核缓冲区或被读取之后,这些数据就不会存在于缓
冲区中了,数据被刷入了内核缓冲区或被读走了。
2. setbuf()函数
setbuf()
函数构建与
setvbuf()
之上,执行类似的任务,其函数原型如下:
#include <stdio.h>
void setbuf(FILE *stream, char *buf);
3. setbuffer()函数
setbuffer()
函数类似于
setbuf()
,但允许调用者指定
buf
缓冲区的大小,其函数原型如下:
#include <stdio.h>
void setbuffer(FILE *stream, char *buf, size_t size);
4. fflush()函数
无论我们采取何种缓冲模式,在任何时候都可以使用库函数
fflush()
来强制刷新(将输出到
stdio
缓冲区中的数据写入到内核缓冲区,通过 write()
函数)
stdio
缓冲区,该函数会刷新指定文件的
stdio
输出缓冲区,此函数原型如下:
#include <stdio.h>
int fflush(FILE *stream);
参数
stream
指定需要进行强制刷新的文件,如果该参数设置为
NULL
,则表示刷新所有的
stdio
缓冲区。函数调用成功返回 0
,否则将返回
-1
,并设置
errno
以指示错误原因。
在一些其它的情况下,也会自动刷新
stdio
缓冲区,譬如当文件关闭时、程序退出时。
不断学习中,共勉!!!