
于此止?: 个人主页
本文选自专栏: 《Linux篇》
《C++与STL剖析篇》 《数据结构篇》 《Python基础篇》《嵌入式砺剑录》
⭐️纵有狂风拔地起,我亦乘风破万里⭐️

缓冲区
前言:本篇是一篇短文,在这里主要进行介绍,我们对应的用户级缓冲和内核级缓冲区的作用以及这两种缓冲区的刷新机制,还有我们对应的在C语言的文件缓冲区和FILE的原理。
目录/索引
一、什么是缓冲区
缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
二、为什么要有对应的缓冲区机制?
读写文件时,如果不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作 (读、写等),那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到 CPU 状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的 CPU 时间,频繁的磁盘访问对程序的执行效率造成很大的影响。
我们在进行系统调用的时候也是费一些时间的, 而且我们的系统主要还得完成我们对应的进程之间的调度,如果我们经常使用系统调用,那么我们的调度队列就会调用该函数,这样我们的调度队列中的任务过多会导致我们的调度的时间变长,从而损失我们对应的OS的效率。

为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。比如我们从磁盘里取信息,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。

又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的 CPU 可以处理别的事情。可以看出,缓冲区就是一块内存区 ,它用在输入输出设备和 CPU 之间,用来缓存数据。它使得低速的输入输出设备和高速的 CPU 能够协调工作,避免低速的输入输出设备占用 CPU,解放出 CPU,使其能够高效率工作。
三、缓冲区的类型
- 全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行 I/O 系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。
- 行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准 I/O 库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准 I/O 库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行 I/O 系统调用操作,默认行缓冲区的大小为 1024。
- 无缓冲区:无缓冲区是指标准 I/O 库不对字符进行缓存,直接调用系统调用。标准出错流 stderr 通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。
这里给出更友好的方式进行理解
缓冲区满了刷新------> 全缓冲区------>效率最快。
缓冲区写满一行------> 行缓冲区------>效率居中。
立即刷新------>无缓冲区------>频繁调用系统调用效率最慢。
四、缓冲区操作的代码演示
我们这里先测试一组简单的代码来观察它的效果
我们的printf只认我们的文件描述符1, 所以我们的printf内部封装了我们对应的文件描述符所以我们这里发现它在C语言提供的语言级缓冲区因为我们的printf是C语言级别的函数,但是这样看不明显我们下面再看一下下面的代码。
cpp
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
close(1);
int fd = open("test.txt",O_WRONLY|O_CREAT|O_TRUNC, 0666);
printf("你好,我是测试缓冲区的代码");
return 0;
}

下面的代码我们在printf写之前我们就提前关闭我们对应的文件,他还没有进行刷新我们对应的缓冲区,就让我们关闭了该文件。我们做一个测试,我们如果用户层存在缓冲区的话我们调用我们的fflush就能刷新出来对应的内容了。那我们试一试吧。
cpp
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
close(1);
int fd = open("test.txt",O_WRONLY|O_CREAT|O_TRUNC, 0666);
printf("你好,我是测试缓冲区的代码");
close(1);
return 0;
}

我们的stdout是我们的一个FILE类型的,我们这里的FILE是一个结构体我们这个FILE封装了对应的文件描述符,我们的stdout内部封装了我们的文件描述符1.所以这里我们对1进行刷新。就能将我们的内容进行一次刷新。所以我们也就能看到我们对应的内容了。所以我们发现我们用户层存在我们对应的缓冲区。
cpp
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
close(1);
int fd = open("test.txt",O_WRONLY|O_CREAT|O_TRUNC, 0666);
printf("你好,我是测试缓冲区的代码");
fflush(stdout);
close(1);
return 0;
}

那我们看一下我们的内核缓冲区吧
我们发现我们调用内核是没有问题的,那我们接下来关闭我们对应的文件描述符来看看。
cpp
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
close(1);
int fd = open("test.txt",O_WRONLY|O_CREAT|O_TRUNC, 0666);
const char* add = "你好,我是测试缓冲区的代码";
write(fd,add,strlen(add));
return 0;
}

这里我们看到我们即使关闭了我们对应的文件描述符我们对应的文件内容还是存在的,下面我们输出对应的结论。
cpp
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
close(1);
int fd = open("test.txt",O_WRONLY|O_CREAT|O_TRUNC, 0666);
const char* add = "你好,我是测试缓冲区的代码";
write(fd,add,strlen(add));
close(1);
return 0;
}

结论:
1.我们存在缓冲区,并且存在两个缓冲区,一个用户级缓冲区(这里我们可以说C语言库给我们提供的缓冲区)还有一个是内核级缓冲区。
2.我们用户级缓冲区写入后不主动刷新,我们对应的数据就会丢失,只有我们刷新到我们对应的内核级缓冲数据才会真正的被进行写入。
3.我们c语言是写满时刷新,如何刷新我们对应写慢会调用系统调用write将我们数据写入到我们的内核级缓冲区中。我们fflush也是类似的我们底层肯定调用了write将用户级缓冲区的内容写到了我们对应的内核级缓冲区中。
我们刚才就一直再说我们FILE是一个结构体,那么现在我们来看一下
五、FILE简介:
我们的FILE的内部的方法实现在:
ubuntu的
/usr/include/x86_64-linux-gnu/bits/types/struct_FILE.h
我们打开这个文件

我们下面的主要代码块:
我们看下面也能看到我们封装了我们对应的文件描述符。
cpp
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
六、总结
- 一般 C 库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printffwrite库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。- 而我们放在缓冲区中的数据,就不会被立即刷新,甚至 fork 之后
- 但是进程退出之后,会统一刷新,写入文件当中。
- 但是 fork 的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
write没有变化,说明没有所谓的缓冲。
综上:printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS 也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢?printf fwrite 是库函数,write 是系统调用,库函数在系统调用的 "上层",是对系统调用的 "封装",但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是 C,所以由 C 标准库提供。