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

news2024/9/27 23:21:13

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.coloradmin.cn/o/2171441.html

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

相关文章

20 vue3之自定义hooks

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

8,STM32CubeMX配置SPI工程(读取norflash的ID)

1&#xff0c;前言 单片机型号&#xff1a;STM32F407 编程环境 &#xff1a;STM32CubeMX Keil v5 硬件连接 &#xff1a;SPI1&#xff0c;CS/SS--->PB14 注&#xff1a;本工程在1&#xff0c;STM32CubeMX工程基础&#xff08;配置Debug、时钟树&#xff09;基础上完…

ARM点灯---看手册

知识点&#xff1a; 一个程序可能会遇到内存泄漏问题&#xff0c;可能一次运行泄漏几M大小&#xff0c;执行几个小时才会泄漏到站崩溃&#xff0c;所以要查看是否有内存泄漏。 查看手册教程 0927-上午 视频1&#xff1a;25&#xff1b;00 硬件程序开发流程 最小系统:单片…

AI驱动的智能运维:行业案例与挑战解析

华为、蚂蚁、字节跳动如何引领智能运维&#xff1f; ©作者|潇潇 来源|神州问学 引言 OpenAI 发布的 ChatGPT 就像是打开了潘多拉的魔盒&#xff0c;释放出了生产环境中的大语言模型&#xff08;LLMs&#xff09;。一些新的概念&#xff1a;“大语言模型运维 (LLMOps)”…

边裁员边收购,思科逐渐变身软件并购之王

《网安面试指南》http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247484339&idx1&sn356300f169de74e7a778b04bfbbbd0ab&chksmc0e47aeff793f3f9a5f7abcfa57695e8944e52bca2de2c7a3eb1aecb3c1e6b9cb6abe509d51f&scene21#wechat_redirect 《Java代码审…

计算机毕业设计党建学习网站查看发布党建评论留言搜索部署安装/springboot/javaWEB/J2EE/MYSQL数据库/vue前后分离小程序

目录 ‌开发背景‌&#xff1a; ‌开发意义‌&#xff1a; ‌开发目标‌&#xff1a; 部署安装 主要功能 功能图 界面介绍 技术介绍 需求分析 1. 用户角色分析 2. 功能需求分析 3. 性能需求分析 4. 界面设计需求 5. 其他需求 ‌党建学习网站的开发背景、意义与目…

史上最详细论文word排版格式指导保姆级教学!

一、前言 首先&#xff0c;每个学校的论文排版格式都是不太相同的&#xff0c;但大体上都是相似的。 正常来说&#xff0c;论文的排版操作是十分枯燥并且重复的&#xff0c;但是word中的样式工具使得论文排版会变得容易。 接下来我将以某个学校论文格式要求为例&#xff0c;…

新手做TikTok用什么手机?当然是海外云手机!

在当今数字化时代&#xff0c;TikTok已成为全球领先的短视频平台&#xff0c;吸引了大量个人和企业入驻&#xff0c;希望在这一广阔的市场中脱颖而出。然而&#xff0c;对于刚刚进入TikTok的新手来说&#xff0c;选择合适的工具是开启TikTok之路的第一步。近年来&#xff0c;海…

开关电源频率是什么?正常范围是多少?

在电子设备的小型化和高效化趋势下&#xff0c;开关电源的频率选择显得尤为重要。本文将详细阐述开关电源频率的范围及其测试方法。  一、开关电源频率的范围 开关电源是一种高效的电力转换装置&#xff0c;为电子设备提供稳定的电源。而作为评估开关电源性能的指标之一&#…

统信服务器操作系统【开机自启动】配置方法

开机自启动的四种配置方法,包括systemctl命令、rc.local文件、crontab任务,通过desktop配置开机自动,前三种方法适合后台程序或者脚本启动,最后一种方法适合图形化程序启动。 文章目录 准备环境配置方法一、通过编写service的方法,使用systemctl配置开机自启二、通过rc.lo…

[EBPF] 实时捕获DM数据库是否存在SQL阻塞

1. 介绍 eBPF&#xff08;extened Berkeley Packet Filter&#xff09;是一种内核技术&#xff0c;它允许开发人员在不修改内核代码的情况下运行特定的功能。eBPF 的概念源自于 Berkeley Packet Filter&#xff08;BPF&#xff09;&#xff0c;后者是由贝尔实验室开发的一种网…

安卓开发板_MTK开发板_联发科开发评估套件Demo板接口介绍

开发板是一种功能丰富的电路平台&#xff0c;专为开发人员设计&#xff0c;集成了多种传感器、扩展接口和通信模块。这使得开发者能够高效进行原型设计和功能验证&#xff0c;极大地简化了软硬件开发的过程。 此次介绍的安卓开发板由MT8788核心板与底板构成&#xff0c;特别之处…

mac怎么设置ip地址映射

最近开发的项目分为了两种版本&#xff0c;一个自己用的&#xff0c;一个是卖出去的。 卖出的域名是和自己的不一样的&#xff0c;系统中有一些功能是只有卖出去的版本有的&#xff0c;但我们开发完之后还得测试&#xff0c;那就需要给自己的电脑配置一个IP地址映射了&#xf…

力扣面试150 寻找峰值 二分

Problem: 162. 寻找峰值 &#x1f468;‍&#x1f3eb; 参考图解 class Solution {public int findPeakElement(int[] nums) {int l 0; // 初始化左边界&#xff0c;表示数组的起始位置int r nums.length - 1; // 初始化右边界&#xff0c;表示数组的结束位置// 当左边界和…

“AI+Security”系列第3期(六):打造最懂安全的智能体-无极AI安全智能体平台落地与实践

近日&#xff0c;由安全极客、Wisemodel 社区、InForSec 网络安全研究国际学术论坛和海升集团联合主办的 “AI Security” 系列第 3 期技术沙龙&#xff1a;“AI 安全智能体&#xff0c;重塑安全团队工作范式” 活动顺利举行。此次活动吸引了线上线下超过千名观众参与。 活动…

基于微信开发助手企鹅音乐微信小程序的设计与实现(源码+文档+讲解)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

pyside6与协程

目录 一、常见错误 错误一、使用协程函数作为槽函数。 错误二、在Qt循环中创建新的loop 二、解决方法&#xff1a; ①安装库qasync ②修改Qt入口 ③异步槽函数 ④异步函数 ⑤整体示例 一、常见错误 错误一、使用协程函数作为槽函数。 这样是肯定是不行&#xff…

BufferQueue低延迟优化,以及SurfaceView帧率上限问题解决

目录 了解BufferQueue 为什么会出现问题&#xff1f; 如何优化&#xff1f; 最近在做一个与音视频播放相关的项目&#xff0c;使用到了MediaCodec解码后送到SurfaceView播放场景。发现SurfaceView播放上限是60HZ&#xff0c;不符合项目需求&#xff0c;故而进行了研究并找到…

Jupyter Notebook 产生 jupyter_notebook_config.py 配置文件

Jupyter Notebook 产生 jupyter_notebook_config.py 配置文件 引言正文第一步第二步第三步引言 今天遇到了一个问题,当我安装了 VS code,之后又在 VS code 中安装了 Jupyter notebook 的 extension。之后可以在 VS code 中正常使用 Jupyter Notebook 的所有功能。但是,当我…

sqli-labs时间盲注、布尔盲注脚本

script.py&#xff1a;提供参数&#xff0c;用于调用布尔盲注或时间注入的函数 import time_type import bool_type ​ ​ # inject_type: 1.布尔盲注2.时间注入 # http_type&#xff1a;1.GET请求2.POST请求 # dict_para_data&#xff1a;所有的参数&#xff0c;和默认值 # v…