1.简介
出于速度和效率的考虑,系统 I/O 调用(即文件 I/O,open、read、write 等)和标准 C 语言库 I/O 函数(即标准 I/O 函数)在操作磁盘文件时会对数据进行缓冲,本小节将讨论文件 I/O 和标准 I/O 这两种 I/O 方式的数据缓冲问题,并讨论其对应用程序性能的影响。
除此之外,本小节还讨论了屏蔽或影响缓冲的一些技术手段,以及直接 I/O 技术—绕过内核缓冲直接访问磁盘硬件。
2.文件 I/O 的内核缓冲
read()和 write()系统调用在进行文件读写操作的时候并不会直接访问磁盘设备,而是仅仅在用户空间缓冲区和内核缓冲区(kernel buffer cache)之间复制数据。譬如调用 write()函数将 5 个字节数据从用户空间内存拷贝到内核空间的缓冲区中:
write(fd, "Hello", 5); //写入 5 个字节数据
调用 write()后仅仅只是将这 5 个字节数据拷贝到了内核空间的缓冲区中,拷贝完成之后函数就返回了,在后面的某个时刻,内核会将其缓冲区中的数据写入(刷新)到磁盘设备中,所以由此可知,系统调用 write()与磁盘操作并不是同步的,write()函数并不会等待数据真正写入到磁盘之后再返回。如果在此期间,其它进程调用 read()函数读取该文件的这几个字节数据,那么内核将自动从缓冲区中读取这几个字节数据返回给应用程序。
与此同理,对于读文件而言亦是如此,内核会从磁盘设备中读取文件的数据并存储到内核的缓冲区中,当调用 read()函数读取数据时,read()调用将从内核缓冲区中读取数据,直至把缓冲区中的数据读完,这时,内核会将文件的下一段内容读入到内核缓冲区中进行缓存。
我们把这个内核缓冲区就称为文件 I/O 的内核缓冲。这样的设计,目的是为了提高文件 I/O 的速度和效率,使得系统调用 read()、write()的操作更为快速,不需要等待磁盘操作(将数据写入到磁盘或从磁盘读取出数据),磁盘操作通常是比较缓慢的。同时这一设计也更为高效,减少了内核操作磁盘的次数,譬如线程1 调用 write()向文件写入数据"abcd",线程 2 也调用 write()向文件写入数据"1234",这样的话,数据"abcd"和"1234"都被缓存在了内核的缓冲区中,在稍后内核会将它们一起写入到磁盘中,只发起一次磁盘操作请求;
加入没有内核缓冲区,那么每一次调用 write(),内核就会执行一次磁盘操作。
前面提到,当调用 write()之后,内核稍后会将数据写入到磁盘设备中,具体是什么时间点写入到磁盘,这个其实是不确定的,由内核根据相应的存储算法自动判断。
通过前面的介绍可知,文件 I/O 的内核缓冲区自然是越大越好,Linux 内核本身对内核缓冲区的大小没有固定上限。内核会分配尽可能多的内核来作为文件 I/O 的内核缓冲区,但受限于物理内存的总量,如果系统可用的物理内存越多,那自然对应的内核缓冲区也就越大,操作越大的文件也要依赖于更大空间的内核缓冲。
3.刷新文件 I/O 的内核缓冲区
强制将文件 I/O 内核缓冲区中缓存的数据写入(刷新)到磁盘设备中,对于某些应用程序来说,可能是很有必要的,例如,应用程序在进行某操作之前,必须要确保前面步骤调用 write()写入到文件的数据已经真正写入到了磁盘中,诸如一些数据库的日志进程。
联系到一个实际的使用场景,当我们在 Ubuntu 系统下拷贝文件到 U 盘时,文件拷贝完成之后,通常在拔掉 U 盘之前,需要执行 sync 命令进行同步操作,这个同步操作其实就是将文件 I/O 内核缓冲区中的数据更新到 U 盘硬件设备,所以如果在没有执行 sync 命令时拔掉 U 盘,很可能就会导致拷贝到 U 盘中的文件遭到破坏!
4.控制文件 I/O 内核缓冲的系统调用
Linux 中提供了一些系统调用可用于控制文件 I/O 内核缓冲,包括系统调用 sync()、syncfs()、fsync()以及 fdatasync()。
(一)、fsync()函数
系统调用 fsync()将参数 fd 所指文件的内容数据和元数据写入磁盘,只有在对磁盘设备的写入操作完成之后,fsync()函数才会返回,其函数原型如下所示:
#include <unistd.h>
int fsync(int fd);
参数 fd 表示文件描述符,函数调用成功将返回 0,失败返回-1 并设置 errno 以指示错误原因。
前面提到了元数据这个概念,元数据并不是文件内容本身的数据,而是一些用于记录文件属性相关的数据信息,譬如文件大小、时间戳、权限等等信息,这里统称为文件的元数据,这些信息也是存储在磁盘设备中的,在 3.1 小节中介绍过。
使用示例
示例代码 实现了一个文件拷贝操作,将源文件(当前目录下的 rfile 文件)的内容拷贝到目标文件中(当前目录下的 wfile 文件)。
示例代码fsync()函数使用示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define BUF_SIZE 4096
#define READ_FILE "./rfile"
#define WRITE_FILE "./wfile"
static char buf[BUF_SIZE];
int main(void) {
int rfd, wfd;
size_t size;
/* 打开源文件 */
rfd = open(READ_FILE, O_RDONLY);
if (0 > rfd) {
perror("open error");
exit(-1);
}
/* 打开目标文件 */
wfd = open(WRITE_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (0 > wfd) {
perror("open error");
exit(-1);
}
/* 拷贝数据 */
while(0 < (size = read(rfd, buf, BUF_SIZE)))
write(wfd, buf, size);
/* 对目标文件执行 fsync 同步 */
fsync(wfd);
/* 关闭文件退出程序 */
close(rfd);
close(wfd);
exit(0);
}
代码没什么好说的,主要就是拷贝完成之后调用 fsync()函数,对目标文件的数据进行了同步操作,整个操作完成之后 close 关闭源文件和目标文件、退出程序。
(二)、fdatasync()函数
系统调用 fdatasync()与 fsync()类似,不同之处在于 fdatasync()仅将参数 fd 所指文件的内容数据写入磁盘,并不包括文件的元数据;同样,只有在对磁盘设备的写入操作完成之后,fdatasync()函数才会返回,其函数原型如下所示:
#include <unistd.h>
int fdatasync(int fd);
(三)、sync()函数
系统调用 sync()会将所有文件 I/O 内核缓冲区中的文件内容数据和元数据全部更新到磁盘设备中,该函数没有参数、也无返回值,意味着它不是对某一个指定的文件进行数据更新,而是刷新所有文件 I/O 内核缓冲区。其函数原型如下所示:
#include <unistd.h>
void sync(void);
在 Linux实现中,调用 sync()函数仅在所有数据已经写入到磁盘设备之后才会返回;然后在其它系统中,sync()实现只是简单调度一下 I/O 传递,在动作未完成之后即可返回。
5.控制文件 I/O 内核缓冲的标志
调用 open()函数时指定一些标志也可以影响到文件 I/O 内核缓冲,譬如 O_DSYNC 标志和 O_SYNC 标志,这些标志在 2.3 小节并未向大家介绍过,联系本小节所学内容,接下来向大家简单地介绍下。
(一)、O_DSYNC 标志
在调用 open()函数时,指定 O_DSYNC 标志,其效果类似于在每个 write()调用之后调用 fdatasync()函数进行数据同步。譬如:
fd = open(filepath, O_WRONLY | O_DSYNC);
(二)、O_SYNC 标志
在调用 open()函数时,指定 O_SYNC 标志,使得每个 write()调用都会自动将文件内容数据和元数据刷新到磁盘设备中,其效果类似于在每个 write()调用之后调用 fsync()函数进行数据同步,譬如:
fd = open(filepath, O_WRONLY | O_SYNC);
对性能的影响
在程序中频繁调用 fsync()、fdatasync()、sync()(或者调用 open 时指定 O_DSYNC 或 O_SYNC 标志)对性能的影响极大,大部分的应用程序是没有这种需求的,所以在大部分应用程序当中基本不会使用到。
6. 直接 I/O:绕过内核缓冲
从 Linux 内核 2.4 版本开始,Linux 允许应用程序在执行文件 I/O 操作时绕过内核缓冲区,从用户空间直接将数据传递到文件或磁盘设备,把这种操作也称为直接 I/O(direct I/O)或裸 I/O(raw I/O)。在有些情况下,这种操作通常是很有必要的,例如,某应用程序的作用是测试磁盘设备的读写速率,那么在这种应用需要下,我们就需要保证 read/write 操作是直接访问磁盘设备,而不经过内核缓冲,如果不能得到这样的保证,必然会导致测试结果出现比较大的误差。
然后,对于大多数应用程序而言,使用直接 I/O 可能会大大降低性能,这是因为为了提高 I/O 性能,内核针对文件 I/O 内核缓冲区做了不少的优化,譬如包括按顺序预读取、在成簇磁盘块上执行 I/O、允许访问同一文件的多个进程共享高速缓存的缓冲区。如果应用程序使用直接 I/O 方式,将无法享受到这些优化措施所带来的性能上的提升,直接 I/O 只在一些特定的需求场合,譬如磁盘速率测试工具、数据库系统等。
我们可针对某一文件或块设备执行直接 I/O,要做到这一点,需要在调用 open()函数打开文件时,指定O_DIRECT 标志,该标志至 Linux 内核 2.4.10 版本开始生效,譬如:
fd = open(filepath, O_WRONLY | O_DIRECT);
直接 I/O 的对齐限制
因为直接 I/O 涉及到对磁盘设备的直接访问,所以在执行直接 I/O 时,必须要遵守以下三个对齐限制要求:
- 应用程序中用于存放数据的缓冲区,其内存起始地址必须以块大小的整数倍进行对齐;
- 写文件时,文件的位置偏移量必须是块大小的整数倍;
- 写入到文件的数据大小必须是块大小的整数倍。
如果不满足以上任何一个要求,调用 write()均为以错误返回 Invalid argument。以上所说的块大小指的是磁盘设备的物理块大小(block size),常见的块大小包括 512 字节、1024 字节、2048 以及 4096 字节,那我们如何确定磁盘分区的块大小呢?可以使用 tune2fs 命令进行查看,如下所示:
tune2fs -l /dev/sda1 | grep "Block size"
-l 后面指定了需要查看的磁盘分区,可以使用 df -h 命令查看 Ubuntu 系统的根文件系统所挂载的磁盘分区:
查看根文件系统挂载的磁盘分区
通过上图可知,Ubuntu 系统的根文件系统挂载在/dev/sda1 磁盘分区下,接着下使用 tune2fs 命令查看该分区的块大小:
磁盘块大小
从上图可知/dev/sda1 磁盘分区的块大小为 4096 个字节。
直接 I/O 测试与普通 I/O 对比测试
接下来编写一个使用直接 I/O 方式写文件的测试程序和一个使用普通 I/O 方式写文件的测试程序,进行对比。示例代码 4.9.2 演示了以直接 I/O 方式写文件的操作,首先我们需要在程序开头处定义一个宏定义_GNU_SOURCE,原因在于后面 open()函数需要指定 O_DIRECT 标志,这个宏需要我们在程序中定义了O_DIRECT 宏之后才能使用,否则编译程序就会报错提示:O_DIRECT 未定义。
Tips:_GNU_SOURCE 宏可用于开启/禁用 Linux 系统调用和 glibc 库函数的一些功能、特性,要打开这些特性,需要在应用程序中定义该宏,定义该宏之后意味着用户应用程序打开了所有的特性;默认情况下,_GNU_SOURCE 宏并没有被定义,所以当使用到它控制的一些特性时,应用程序编译将会报错!定义该宏的方式有两种:
⚫ 直接在源文件中定义:#define _GNU_SOURCE
⚫ gcc 编译时使用-D 选项定义_GNU_SOURCE 宏:
gcc -D_GNU_SOURCE -o testApp testApp.c
gcc 的-D 选项可用于定义一个宏,并且该宏定义在整个源码工程中都是生效的,是一个全局宏定义。使用以上哪种方式都可以。
示例代码 直接 I/O 示例程序
/** 使用宏定义 O_DIRECT 需要在程序中定义宏_GNU_SOURCE
- 不然提示 O_DIRECT 找不到 **/
/** 使用宏定义 O_DIRECT 需要在程序中定义宏_GNU_SOURCE
** 不然提示 O_DIRECT 找不到 **/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
/** 定义一个用于存放数据的 buf,起始地址以 4096 字节进行对其 **/
static char buf[8192] __attribute((aligned (4096)));
int main(void) {
int fd;
int count;
/* 打开文件 */
fd = open("./test_file",
O_WRONLY | O_CREAT | O_TRUNC | O_DIRECT,
0664);
if (0 > fd) {
perror("open error");
exit(-1);
}
/* 写文件 */
count = 10000;
while(count--) {
if (4096 != write(fd, buf, 4096)) {
perror("write error");
exit(-1);
}
}
/* 关闭文件退出程序 */
close(fd);
exit(0);
}
前面提到过,使用直接 I/O 方式需要满足 3 个对齐要求,程序中定义了一个 static 静态数组 buf,将其作为数据存放的缓冲区,在变量定义后加了__attribute((aligned (4096)))修饰,使其起始地址以 4096 字节进行对其。
Tips:attribute 是 gcc 支持的一种机制(也可以写成__attribute),可用于设置函数属性、变量属性以及类型属性等,对此不了解的读者请自行查找资料学习,本书不会对此进行介绍!
程序中调用 open()函数是指定了 O_DIRECT 标志,使用直接 I/O,最后通过 while 循环,将数据写入文件中,循环 10000 次,每次写入 4096 个字节数据,也就是总共写入 4096*10000 个字节(约等于 40MB)。
首次调用 write()时其文件读写位置偏移量为 0,之后均以 4096 字节进行递增,所以满足直接 I/O 方式的位置偏移量必须是块大小的整数倍这个要求;每次写入大小均是 4096 字节,所以满足了数据大小必须是块大小的整数倍这个要求。
接下来编译测试:
直接 I/O 测试结果
通过 time 命令测试可知,每次执行程序需要花费 2.7 秒左右的时间,使用直接 I/O 方式向文件写入约40MB 数据大小。
Tips:对于直接 I/O 方式的 3 个对齐限制,大家可以自行进行验证,譬如修改上述示例代码使之不满足3 个对齐条件种的任何一个,然后编译程序进行测试,会发生 write()函数会报错,均是“Invalid argument”错误。
对示例代码 进行修改,使其变成普通 I/O 方式,其它功能相同,最终修改后的示例代码如下所示:
示例代码普通 I/O 方式
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
static char buf[8192];
int main(void) {
int fd;
int count;
/* 打开文件 */
fd = open("./test_file", O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (0 > fd) {
perror("open error");
exit(-1);
}
/* 写文件 */
count = 10000;
while(count--) {//循环 10000 次,每次写入 4096 个字节数据
if (4096 != write(fd, buf, 4096)) {
perror("write error");
exit(-1);
}
}
/* 关闭文件退出程序 */
close(fd);
exit(0);
}
普通 I/O 测试结果
使用 time 命令得到的程序运行时间大约是 0.13~0.14 秒左右,相比直接 I/O 方式的 2.7 秒,时间上提升了 20 倍左右(测试大小不同、每次写入的大小不同,均会导致时间上的差别),原因在于直接 I/O 方式每次 write()调用均是直接对磁盘发起了写操作,而普通方式只是将用户空间下的数据拷贝到了文件 I/O 内核缓冲区中,并没直接操作硬件,所以消耗的时间短,硬件操作占用的时间远比内存复制占用的时间大得多直接 I/O 方式效率、性能比较低,绝大部分应用程序不会使用直接 I/O 方式对文件进行 I/O 操作,通常只在一些特殊的应用场合下才可能会使用,那我们可以使用直接 I/O 方式来测试磁盘设备的读写速率,这种测试方式相比普通 I/O 方式就会更加准确。
7. stdio 缓冲
介绍完文件 I/O 的内核缓冲后,接下来我们聊一聊标准 I/O 的 stdio 缓冲。
标准 I/O(fopen、fread、fwrite、fclose、fseek 等)是 C 语言标准库函数,而文件 I/O(open、read、write、close、lseek 等)是系统调用,虽然标准 I/O 是在文件 I/O 基础上进行封装而实现(譬如 fopen 内部实际上调用了 open、fread 内部调用了 read 等),但在效率、性能上标准 I/O 要优于文件 I/O,其原因在于标准 I/O 实现维护了自己的缓冲区,我们把这个缓冲区称为 stdio 缓冲区,接下来我们聊一聊标准 I/O 的 stdio 缓冲。
前面提到了文件 I/O 内核缓冲,这是由内核维护的缓冲区,而标准 I/O 所维护的 stdio 缓冲是用户空间的缓冲区,当应用程序中通过标准 I/O 操作磁盘文件时,为了减少调用系统调用的次数,标准 I/O 函数会将用户写入或读取文件的数据缓存在 stdio 缓冲区,然后再一次性将 stdio 缓冲区中缓存的数据通过调用系统调用 I/O(文件 I/O)写入到文件 I/O 内核缓冲区或者拷贝到应用程序的 buf 中。
通过这样的优化操作,当操作磁盘文件时,在用户空间缓存大块数据以减少调用系统调用的次数,使得效率、性能得到优化。使用标准 I/O 可以使编程者免于自行处理对数据的缓冲,无论是调用 write()写入数据、还是调用 read()读取数据。
对 stdio 缓冲进行设置
C 语言提供了一些库函数可用于对标准 I/O 的 stdio 缓冲区进行相关的一些设置,包括 setbuf()、setbuffer()以及 setvbuf()。
(一)、setvbuf()函数
调用 setvbuf()库函数可以对文件的 stdio 缓冲区进行设置,譬如缓冲区的缓冲模式、缓冲区的大小、起
始地址等。其函数原型如下所示:
#include <stdio.h>int setvbuf(FILE *stream, char *buf, int mode, size_t size);
使用该函数需要包含头文件<stdio.h>。
函数参数和返回值含义如下:
stream:FILE 指针,用于指定对应的文件,每一个文件都可以设置它对应的 stdio 缓冲区。
buf:如果参数 buf 不为 NULL,那么 buf 指向 size 大小的内存区域将作为该文件的 stdio 缓冲区,因为stdio 库会使用 buf 指向的缓冲区,所以应该以动态(分配在堆内存,譬如 malloc,在 7.6 小节介绍)或静态的方式在堆中为该缓冲区分配一块空间,而不是分配在栈上的函数内的自动变量(局部变量)。如果 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 缓冲区中的数据被刷入到内核缓冲区或被读取之后,这些数据就不会存在于缓冲区中了,数据被刷入了内核缓冲区或被读走了。
(二)、setbuf()函数
setbuf()函数构建与 setvbuf()之上,执行类似的任务,其函数原型如下所示:
#include <stdio.h>
void setbuf(FILE *stream, char *buf);
setbuf()调用除了不返回函数结果(void)外,就相当于:
setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ);
要么将 buf 设置为 NULL 以表示无缓冲,要么指向由调用者分配的 BUFSIZ 个字节大小的缓冲区
(BUFSIZ 定义于头文件<stdio.h>中,该值通常为 8192)。
(三)、setbuffer()函数
setbuffer()函数类似于 setbuf(),但允许调用者指定 buf 缓冲区的大小,其函数原型如下所示:
#include <stdio.h>
void setbuffer(FILE *stream, char *buf, size_t size);
setbuffer()调用除了不返回函数结果(void)外,就相当于:
标准输出 printf()的行缓冲模式测试
我们先看看下面这个简单地示例代码,调用了 printf()函数,区别在于第二个 printf()没有输出换行符。
示例代码 printf()输出测试
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main**(void)**
{
printf**("Hello World!\n");**
printf**("Hello World!");**
for ( ; ; )
sleep**(1);**
}
printf()函数是标准 I/O 库函数,向终端设备(标准输出)输出打印信息,编译测试:
运行之后可以发现只有第一个 printf()打印的信息显示出来了,第二个并没有显示出来,这是为什么呢?
这就是 stdio 缓冲的问题,前面提到了标准输出默认采用的是行缓冲模式,printf()输出的字符串写入到了标准输出的 stdio 缓冲区中,只有输出换行符时(不考虑缓冲区填满的情况)才会将这一行数据刷入到内核缓冲区,也就是写入标准输出文件(终端设备),因为第一个 printf()包含了换行符,所以已经刷入了内核缓冲区,而第二个 printf 并没有包含换行符,所以第二个 printf 输出的"Hello World!"还缓存在 stdio 缓冲区中,需要等待一个换行符才可输出到终端。
联系 之前介绍的格式化输入 scanf()函数,程序中调用 scanf()函数进行阻塞,用户通过键盘输入数据,只有在按下回车键(换行符键)时程序才会接着往下执行,因为标准输入默认也是采用了行缓冲模式。
譬如对示例代码进行修改,使标准输出变成无缓冲模式,修改后代码如下所示:
示例代码将标准输出配置为无缓冲模式
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main**(void)**
{
/* 将标准输出设置为无缓冲模式 / if (setvbuf(stdout, NULL, _IONBF*,** 0**)) {**
perror**("setvbuf error");**
exit**(0);**
}
printf**("Hello World!\n");**
printf**("Hello World!");**
for ( ; ; )
sleep**(1);**
}
在使用 printf()之前,调用 setvbuf()函数将标准输出的 stdio 缓冲设置为无缓冲模式,接着编译运行:
可以发现该程序却能够成功输出两个“Hello World!”,并且白色的光标在第二个“Hello World!”后面,意味着输出没有换行,与程序中第二个 printf 没有加换行符的效果是一直。
所以通过以上两个示例代码对比可知,标准输出默认是行缓冲模式,只有输出了换行符时,才会将换行符这一行字符进行输出显示(也就是刷入到内核缓冲区),在没有输出换行符之前,会将数据缓存在 stdio缓冲区中。
刷新 stdio 缓冲区
无论我们采取何种缓冲模式,在任何时候都可以使用库函数 fflush()来强制刷新(将输出到 stdio 缓冲区中的数据写入到内核缓冲区,通过 write()函数)stdio 缓冲区,该函数会刷新指定文件的 stdio 输出缓冲区,此函数原型如下所示:
#include <stdio.h>
int fflush(FILE *stream);
参数 stream 指定需要进行强制刷新的文件,如果该参数设置为 NULL,则表示刷新所有的 stdio 缓冲区。函数调用成功返回 0,否则将返回-1,并设置 errno 以指示错误原因。
接下来我们对示例代码 进行修改,在第二个 printf 后面调用 fflush()函数,修改后示例代码如下所示:
示例代码 使用 fflush()刷新 stdio 缓冲区
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main**(void){**
printf**("Hello World!\n");**
printf**("Hello World!");**
fflush**(stdout);** //刷新标准输出 stdio 缓冲区
for ( ; ; )
sleep**(1);**
}
可以看到,打印了两次“Hello World!”,这就是 fflush()的作用了强制刷新 stdio 缓冲区。
除了使用库函数 fflush()之外,还有其它方法会自动刷新 stdio 缓冲区吗?是的,使用库函数 fflush()是一种强制刷新的手段,在一些其它的情况下,也会自动刷新 stdio 缓冲区,譬如当文件关闭时、程序退出时,接下来我们进行演示。
(一)、关闭文件时刷新 stdio 缓冲区
同样还是直接对示例代码进行修改,在调用第二个 printf 函数后关闭标准输出,如下所示:
示例代码 关闭标准输出
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main**(void)**
{
printf**("Hello World!\n");**
printf**("Hello World!");**
fclose**(stdout);** //关闭标准输出
for ( ; ; )
sleep**(1);**
}
至于运行结果文档中就不贴出来了,运行结果与图 4.9.7 是一样的。所以由此可知,文件关闭时系统会自动刷新该文件的 stdio 缓冲区。
㈡、程序退出时刷新 stdio 缓冲区可以看到上面使用的测试程序中,在最后都使用了一个 for 死循环,让程序处于休眠状态无法退出,为什么要这样做呢?原因在于程序退出时也会自动刷新 stdio 缓冲区,这样的话就会影响到测试结果。同样对示例代码进行修改,去掉 for 死循环,让程序结束,修改完之后如下所示:
示例代码 程序结束
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main**(void)**
{
printf**("Hello World!\n");**
printf**("Hello World!");**
}
运行结果如下:
从结果可知,当程序退出时,确实会自动刷新 stdio 缓冲区。但是,与程序退出方式有关,如果使用 exit()、return 或像上述示例代码一样不显式调用相关函数或执行 return 语句来结束程序,这些情况下程序终止时会自动刷新 stdio 缓冲区;如果使用_exit 或_Exit()终止程序则不会刷新,这里各位读者可以自行测试、验证。
关于刷新 stdio 缓冲区相关内容,最后进行一个总结:
⚫ 调用 fflush()库函数可强制刷新指定文件的 stdio 缓冲区;
⚫ 调用 fclose()关闭文件时会自动刷新文件的 stdio 缓冲区;
⚫ 程序退出时会自动刷新 stdio 缓冲区(注意区分不同的情况)。
关于本小节内容就给大家介绍这么多,笔者觉得已经非常详细了,如果还有不太理解的地方,希望大家能够自己动手进行测试、验证,然后总结出相应的结论,前面笔者一直强调,编程是一门实践性很强的工作,
一定要学会自己分析、验证。
8.I/O 缓冲小节
本小节对前面学习的内容进行一个简单地总结,概括说明文件 I/O 内核缓冲区和 stdio 缓冲区之间的联系与区别,以及各种 stdio 库函数,首先应用程序调用标准 I/O 库函数将用户数据写入到 stdio 缓冲区中,stdio 缓冲区是由 stdio 库所维护的用户空间缓冲区。针对不同的缓冲模式,当满足条件时,stdio 库会调用文件 I/O(系统调用 I/O)将 stdio 缓冲区中缓存的数据写入到内核缓冲区中,内核缓冲区位于内核空间。最终由内核向磁盘设备发起读写操作,将内核缓冲区中的数据写入到磁盘(或者从磁盘设备读取数据到内核缓冲区)。
应用程序调用库函数可以对 stdio 缓冲区进行相应的设置,设置缓冲区缓冲模式、缓冲区大小以及由调用者指定一块空间作为 stdio 缓冲区,并且可以强制调用 fflush()函数刷新缓冲区;而对于内核缓冲区来说,应用程序可以调用相关系统调用对内核缓冲区进行控制,譬如调用 fsync()、fdatasync()或 sync()来刷新内核缓冲区(或通过 open 指定 O_SYNC 或 O_DSYNC 标志),或者使用直接 I/O 绕过内核缓冲区(open 函数指定 O_DIRECT 标志)。