目录
引入一些奇怪的现象
**现象1:**为什么向文件写入的时候 write会先被打印出来?
cpp
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";
// C
printf("hello printf\n"); // stdout -> 1
fprintf(stdout, "hello fprintf\n"); // stdout -> 1
fwrite(fstr, strlen(fstr), 1, stdout); // fwrite, stdout -> 1
// 操作提供的 system call
write(1, str, strlen(str)); // 1
return 0;
}
打印结果

**现象2:**为什么加了fork之后,向文件写入时C接口会被调了两次??且向文件写入时write先被打印?
cpp
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";
// C
printf("hello printf\n"); // stdout -> 1
fprintf(stdout, "hello fprintf\n"); // stdout -> 1
fwrite(fstr, strlen(fstr), 1, stdout); // fread, stdout -> 1
// close(1); // 可选,关闭 stdout
// 操作提供的 system call
write(1, str, strlen(str)); // 1
fork(); // 创建子进程
return 0;
}
打印结果

**现象3:**close1号文件后,为什么就没有结果了??
cpp
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
// const char *fstr = "hello fwrite";
const char *str = "hello write";
// C
// printf("hello printf"); // stdout -> 1
// fprintf(stdout, "hello fprintf"); // stdout -> 1
// fwrite(fstr, strlen(fstr), 1, stdout); // fread, stdout -> 1
// 操作提供的 system call
write(1, str, strlen(str)); // 1
close(1); // 关闭标准输出(文件描述符 1)
fork(); // 创建子进程
return 0;
}
打印结果

带着上面的问题,我们继续往下学习缓冲区
缓冲区
stdio缓冲区机制
stdio缓冲区机制 是C语言标准输入输出库(stdio.h)提供的一种用于提高数据读写效率的机制。缓冲区是一段内存区域,用于临时存储输入输出数据**,以减少对磁盘或终端的直接读写次数,从而提高程序性能**。stdio库中的函数,如printf、scanf、fread、fwrite等,都使用了缓冲区机制
缓冲区的策略包括:
- 无缓冲------>直接刷新------>fflush函数
- 行缓冲------>遇到/n刷新------>显示器文件
- 全缓存------>写满才刷新------>普通文件
无缓冲
- 在无缓冲模式下,不对字符进行缓冲存储,即每次I/O操作都直接进行
- 标准错误流(stderr)通常是无缓冲的,以确保错误信息能够立即显示
行缓冲
- 在行缓冲模式下,当遇到换行符(\n)时,会执行I/O操作
- 当流涉及终端(如标准输出stdout和标准输入stdin)时,通常使用行缓冲模式
- 这使得输出能够按行显示,而不是等到缓冲区满时才显示
全缓冲
- 在全缓冲模式下,当缓冲区被填满时,才会进行实际的I/O操作
- 默认情况下,对磁盘文件的读写操作采用全缓冲模式
- 缓冲区的大小通常是固定的,如4096字节(但可以通过setvbuf函数调整)
为什么要有这些不同的方案??
一般来说写满再刷新效率高,因为这样可以减少调用系统接口的次数,而显示器之所以是行刷新,因为显示器是要给人给的,按行看符合我们的习惯,而文件采用的就是全缓存策略,因为用户不需要马上看到这些信息,所以这样可以提高效率。 而对于一些特殊情况我们就可以用fllush之前强制刷新出来。 ------>所以方案是根据不同的需求来的!
解释现象1
当我们从向显示器写入转变为向普通文件打印时,此时刷新策略从行刷新转变为全刷新,所以前三个C接口并没有直接写入,而是暂时保存在了缓冲区里面,而write是系统调用接口优先被打印了出来,之后当进程退出的时候,缓冲区的内容才被刷新出来。
解释现象2
跟现象1一样,前三个C接口的数据暂时被存在了缓冲区,而write的调用先被打了出来。当fork的时候,子进程会和父进程指向相同的代码和数据,当其中一方打算刷新缓冲区时,其实就相当于要修改数据,操作系统检测到之后就会发生写时拷贝,于是缓冲区的数据被多拷贝了一份,而后该进程退出时就会再刷新一次,因此C接口写入的数据被调了2次!!
缓冲区在哪?
通过现象3,我们可以观察到,一旦调用 **close()**后,缓冲区的内容就丢失了。因为现代操作系统不做浪费空间和时间的问题,所以操作系统在关闭文件之前会自动刷新缓冲区,避免浪费资源。 因此,**close()**作为系统调用,必定会在关闭文件之前确保缓冲区的内容被写入目标设备或文件。而在 close() 之后,缓冲区中的数据已经被清空,无法再访问。

所以我们的库函数接口是先把内容放到一个C提供的缓冲区,当需要刷新的时候,才会去调用write函数进行写入
那么现在我们也清楚了。close后刷新不出来的原因就是:进程退出后想要刷新的时候,文件描述符被关了,所以即使调了write也写不进去,缓冲区的数据被丢弃了
为什么要有缓冲区呢?
举个例子,比方说你和你的好朋友相隔千里,而你想要给他送个键盘,如果没有快递公司和菜鸟驿站(缓冲区)的话,那么你可能得坐车好几天才能到他那里,但如果你的楼下有菜鸟驿站和快递公司,那么你只需要下楼付点钱填个单子就行了,接着你可以去忙你自己的事情,当旁边的人问你键盘去哪里的时候,你会说已经寄给朋友了,其实这个时候你的键盘可能还在快递公司放着。
从总体来看东西是你送还是快递公司送其实都差不多,区别就是你不需要操太多心。因此缓冲区方便了用户!!
快递公司可以有不同的策略来提高整体的效率,比方说你这个快递不急,那么我就等快递车装满了再送(全刷新) ,如果比较急,我就装满一个袋子就送(行刷新),如果你特别急,可以通过加钱(fllus强制刷新)来加急。 所以缓冲区解决了效率问题
**配合格式化!**比方说C语言经常需要%d这样的格式化,我们的数字123 最后被打印的时候也是要转化成字符串的123 才能调用write写入,因此我们可以将这个解格式化的工作放在缓冲区去完成!!

理性理解:
CPU 计算速度非常快!而磁盘的读取速度相对于 CPU 来说是非常非常慢的,因此需要先将数据写入缓冲区中,依据不同的刷新策略,将数据刷新至内核缓冲区中,供 CPU 进行使用,这样做的是目的是尽可能的提高效率,节省调用者的时间
本来 IO 就慢,如果没有缓冲区的存在,那么速度会更慢,下面通过一个代码来看看是否进行 IO 时,CPU 的算力差距
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int count = 0;
int main()
{
//定一个 1 秒的闹钟,查看算力
alarm(1); //一秒后闹钟响起
while(true)
{
cout << count++ << endl;
}
return 0;
}
最终在 1s 内,count 累加了 10w+ 次(有 IO 的情况下)

下面改变程序,取消IO
cpp
int count = 0;
void handler(int signo)
{
cout << "count: " << count << endl;
exit(1);
}
int main()
{
//定一个 1 秒的闹钟,查看算力
signal(14, handler);
alarm(1); //一秒后闹钟响起
while(true) count++;
return 0;
}
最终在没有 IO 的情况下,count 累加了 5亿+ 次,由此可以看出频繁 IO 对 CPU 计算的影响有多大,假若没有缓冲区,那么整个累加值将会更多(因为需要花费更多的时间在 IO 上)

因此在进行 读取 / 写入 操作时,常常会借助 缓冲区 buffer
cpp
#include <iostream>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
assert(fd != -1);
char buffer[256] = { 0 }; //缓冲区
int n = read(0, buffer, sizeof(buffer)); //读取信息至缓冲区中
buffer[n] = '\0';
//写入成功后,在写入文件中
write(fd, buffer, strlen(buffer));
close(fd);
return 0;
}

用户缓冲区在哪?
我们回忆一下exit和_exit 区别就是exit会先调用一次fllush把缓冲区的数据刷新出来。我们会注意到fllush传递的参数是FILE* 类型

FILE* 不仅封装了fd的信息,还维护了对应文件的缓冲区字段和文件信息!
FILE*是用户级别的缓冲区(任何语言都属于用户层),当我们打开一个文件的时候语言层给我们malloc(FILE),同时也会维护一个专属于该文件的缓冲区!! 所以如果有10个文件就会有10个缓冲区!
内核缓冲区在哪?
内核缓冲区也是由操作系统的file结构体维护的一段空间,和语言的缓冲区模式是类似的,作为用户我们不需要太关心操作系统什么时候会刷新**,我们只需要认为数据只要刷新到了内核,就必然可以到达硬件,因为现代操作系统不做任何浪费空间和时间的事情。!!**
