Linux标准IO(五)-I/O缓冲详解

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 标志)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1547565.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

蓝桥杯--STM32G431RBT6(TIM定时器的输出频率和占空比,含详细原理介绍和使用方法)

目录 一、前言 二、代码 实现功能&#xff1a;​编辑 按如图配置 定义变量 编写执行代码 显示在LCD上 加入按键效果 三、效果展示 四、代码开源 一、前言 ARR 即自动重装载值&#xff08;Auto Reload Register&#xff09;。相当于一个水杯&#xff0c;水杯容量&am…

【EXCEL数据处理】000002 案列 条件格式之重复值。使用的软件是微软的Excel操作的。处理数据的目的是让数据更直观的显示出来,方便查看。

【EXCEL数据处理】000002 案列 条件格式之重复值。使用的软件是微软的Excel操作的。处理数据的目的是让数据更直观的显示出来&#xff0c;方便查看。 在日常的数据处理和分析工作中&#xff0c;Excel 是一款强大而广泛使用的工具。其中&#xff0c;条件格式中的重复值功能对于快…

Golang | Leetcode Golang题解之第438题找到字符串中所有字母异位词

题目&#xff1a; 题解&#xff1a; func findAnagrams(s, p string) (ans []int) {sLen, pLen : len(s), len(p)if sLen < pLen {return}count : [26]int{}for i, ch : range p {count[s[i]-a]count[ch-a]--}differ : 0for _, c : range count {if c ! 0 {differ}}if diff…

Java | Leetcode Java题解之第438题找到字符串中所有字母异位词

题目&#xff1a; 题解&#xff1a; class Solution {public List<Integer> findAnagrams(String s, String p) {int sLen s.length(), pLen p.length();if (sLen < pLen) {return new ArrayList<Integer>();}List<Integer> ans new ArrayList<Int…

滚珠丝杠在人形机器人及线控制动和转向中大放异彩

直线驱动器用于对旋转角度不大、高负载的场景,在人形机器人中多用于四肢。直线驱动器多采取“电机+丝杠”,将旋转运动转为关节末端的直线运动,能够起到较好的支撑和承重效果,能够较好的适配应用场景的负载需求。 特斯拉人形机器人Optimus 双足、双臂采用连杆结构,连杆末端…

25:stm32的低功耗模式

低功耗模式 1、PWR电源控制2、低功耗模式 1、PWR电源控制 PWR&#xff08;Power Control&#xff09;电源控制。PWR负责管理STM32内部的电源供电部分&#xff0c;可以实现可编程电压监测器和低功耗模式的功能&#xff0c;这里我们只学习低功耗模式的功能&#xff0c;低功耗模式…

MySQL --用户管理

文章目录 1.用户1.1用户信息1.2创建用户1.3删除用户1.4修改用户密码 2.数据库的权限2.1给用户授权2.2回收权限 如果我们只能使用root用户&#xff0c;这样存在安全隐患。这时&#xff0c;就需要使用MySQL的用户管理。 1.用户 1.1用户信息 MySQL中的用户&#xff0c;都存储在系…

性能调优知识点(mysql)一

Mysql 索引 索引介绍 1.索引是排好序的数据结构。他的目的是为了提升查询效率。 2.mysql存储引擎分为innodb和myisam。它是用来形容表的。 innodb支持事务、外键、行锁 myisam不支持事务、外键 3.myisam使用3个文件来存储每张表数据&#xff0c;每个文件名以表名开头&#x…

Python | Leetcode Python题解之第440题字典序的第K小数字

题目&#xff1a; 题解&#xff1a; class Solution:def getSteps(self, cur: int, n: int) -> int:steps, first, last 0, cur, curwhile first < n:steps min(last, n) - first 1first * 10last last * 10 9return stepsdef findKthNumber(self, n: int, k: int)…

GS-SLAM论文阅读笔记--MM3DGS SLAM

前言 多传感器融合GS-SLAM的另一个IROS2024,不过这篇没有用到激光雷达&#xff0c;而是相机和IMU结合而实现的。今天看一下这篇。 文章目录 前言1.背景介绍2.关键内容2.1 跟踪2.2 深度监督2.3 惯性融合2.4建图2.5 总体流程 3.文章贡献4.个人思考 1.背景介绍 虽然SLAM方法使用…

class 023 随机快速排序

这篇文章是看了“左程云”老师在b站上的讲解之后写的, 自己感觉已经能理解了, 所以就将整个过程写下来了。 这个是“左程云”老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐. https://space.bilibili.com/8888480?spm_id_f…

MySql简介及发展

MySql简介及发展 1、MySql起源和分支 MySQL 是最流行的关系型数据库软件之一&#xff0c;由于其体积小、速度快、开源免费、简单易用、维护成本 低等&#xff0c;在集群架构中易于扩展、高可用&#xff0c;因此深受开发者和企业的欢迎。 Oracle和MySQL是世界市场占比最高的两…

从入门到入土:计算机视觉CV学习路线图

在当今这个被数据和图像淹没的世界&#xff0c;计算机视觉&#xff08;CV&#xff09;正如一位聪明绝顶的魔术师&#xff0c;能够从无数的图像中提取出有意义的信息。对于那些初入这个领域的新人&#xff0c;学习计算机视觉既是一场冒险&#xff0c;也是一场盛宴。让我作为一位…

C语言进阶之泛型列表(Generic List)

1.前言 数据结构是需要泛型的,而在C语言中实现泛型就只能去用指针魔法了,来跟我一起实现吧!所有代码经测试未发现明显bug,可放心食用. 2.代码截图展示 1.list.h 2.main.c 3.list.c 3.结语 这次分享的列表采用动态数组的方式实现,下次我会去用链表实现,两种实现方式各有优劣,希…

20 vue3之自定义hooks

Vue3 自定义Hook的作用 主要用来处理复用代码逻辑的一些封装 Vue3 的 hook函数 相当于 vue2 的 mixin, 不同在与 hooks 是函数Vue3 的 hook函数 可以帮助我们提高代码的复用性, 让我们能在不同的组件中都利用 hooks 函数 这个在vue2 就已经有一个东西是Mixins mixins就是将…

代码随想录算法训练营第57天 | 寻宝

寻宝 题目描述 在世界的某个区域&#xff0c;有一些分散的神秘岛屿&#xff0c;每个岛屿上都有一种珍稀的资源或者宝藏。国王打算在这些岛屿上建公路&#xff0c;方便运输。 不同岛屿之间&#xff0c;路途距离不同&#xff0c;国王希望你可以规划建公路的方案&#xff0c;如何…

PostgreSQL 创建表,常规表、外部表、分区表区别讲解

PostgreSQL 创建表&#xff0c;常规表、外部表、分区表区别讲解 创建表&#xff0c;常规表、外部表、分区表区一、常规表1. 定义和特点&#xff1a;2. 适用场景&#xff1a; 二、外部表1. 定义和特点&#xff1a;2. 适用场景&#xff1a; 三、分区表1. 定义和特点&#xff1a;2…

什么是Agent智能体?

你好&#xff0c;我是三桥君 近期&#xff0c;从各大厂商的年度大会到多个大型AI峰会&#xff0c;三桥君明显感受到行业风气的转变。这些会议不仅展示了众多AI Agent的实际应用案例&#xff0c;还有专家们对未来发展的预测。一时间&#xff0c;“Agent”这个词成为了热门词汇&…

【论文阅读】Diffusion Policy: Visuomotor Policy Learning via Action Diffusion

Abstract 本文介绍了扩散策略&#xff0c;这是一种通过将机器人的视觉运动policy表示为条件去噪扩散过程来生成机器人行为的新方法。我们对来自 4 个不同的机器人操作基准的 15 个不同任务的扩散策略进行了基准测试&#xff0c;发现它始终优于现有的 state-of-the-art 机器人学…

【AndroidStudio】关于AndroidStudio的常见控件TextView和Button

作者&#xff1a;CSDN-PleaSure乐事 欢迎大家阅读我的博客 希望大家喜欢 使用环境&#xff1a;AndroidStudio 1.常见控件TextView 1.1基本信息 TextView主要用于在界面上显示一段文本信息。最基本的代码格式如下&#xff1a; <TextView android:id"id/text_vie…