不管是操作系统,还是学语言,大家应该都听过缓冲区,那么什么是缓冲区?缓冲区又位于什么地方?在这篇文章,小编将带你了解缓冲区。❄️❄️❄️先让我们欣赏下面的一张图,让我们恭僖EDG夺得冠军。❄️❄️❄️
对于下面的代码,他们的执行结果会是怎样的呢?
int main()
{
printf("你好呀,缓冲区!\n");
sleep(3);
return 0;
}
int main()
{
printf("你好呀,缓冲区!");
sleep(3);
return 0;
}
两个代码都是打印“你好呀,缓冲区!”,然后sleep 3s,但是唯一不同的是,代码1在打印内容后有一个“\n”的换行,而代码2是没有的,这小小的一个符号,会让我们的结果产生怎样的不同呢?
代码1运行结果如图:先打印字符串,然后休眠3s
|
|
|
|
相信读者都了解文件描述符和重定向的相关知识,如果对这方面不了解的读者可以查看小编的另一篇文章《文件描述符》。我们知道,对于下面的代码,当我们直接运行的时候,无论是显示器还是log.txt中都没有对应的数据。其结果如下:
int main()
{
close(1);
umask(0);
int fd = open("log.txt",O_WRONLY|O_CREAT, 0666);
if(fd<0){
perror("open file!\n");
return 1;
}
printf("你好呀,缓冲区!\n");
close(fd);
return 0;
}
|
|
int main()
{
close(1);
umask(0);
int fd = open("log.txt",O_WRONLY|O_CREAT, 0666);
if(fd<0){
perror("open file!\n");
return 1;
}
printf("你好呀,缓冲区!\n");
fflush(stdout);
close(fd);
return 0;
}
一般情况下,缓冲/刷新方案分为三种:a.无缓冲;b.行缓冲(常见的对显示器进行刷新数据时);c.全缓冲(对文件写入的时候采用的是全缓冲,这里的文件可以理解为磁盘文件)。
说白了就是在计算机内存中提供了一块内存空间,不断地往空间中写入数据,当行缓冲满了,或者写入了\n,他会把一行或者包括\n在内的所有内容全部刷新出来。如果没有+\n,但是也想要他刷新就要加fflush(stdout),就会将缓冲区的内容刷新出来;全缓冲就是这块缓冲区必须满了才会刷新到对应的磁盘中去。
那么为什么显示器是行缓冲,而文件写入是全缓冲呢?因为计算机遵循的是冯诺依曼体系,像文件这样的其实是属于外设,或者外设之上,比如键盘是外设,磁盘是外设,换句话说我们要把数据刷新到磁盘或者显示器上都叫做往外设中写入,外设的写入效率是很低的,所以我们把数据积累到内存缓冲区中,当积累的足够多的时候定期刷新,提高效率。
按照这说法,那是不是全缓冲效率最高,答案是对的!!!理论上全缓冲效率确实是最高的,那为什么显示器还要再进行行缓冲呢?可以这样理解,磁盘在文件写入的时候人是不会读的,显示器不一样,当一个人往显示器上写入数据的时候,他是想尽快拿到对应的输出结果,如果选择无缓冲,效率就会很低,全缓冲人看到消息就不及时,所以设置为行缓冲。所谓行缓冲就是在效率和可用性做的平衡。
为了理清这些问题,我们写一份代码让大家看一下奇怪的现象。下面这份代码是分别调用C库函数接口和系统调用接口打印三行代码,结果也是没有问题的打印到显示器中,我们./test>log.txt,将结果进行重定向,也是正确的。
int main()
{
//C语言库函数接口
printf("hello printf!\n");
rprintf(stdout, "hello rprintf!\n");
//系统调用接口
const char* str = "hello wirte!\n";
write(1 str, strlen(str));
return 0;
}
|
|
int main()
{
//C语言库函数接口
printf("hello printf!\n");
rprintf(stdout, "hello rprintf!\n");
//系统调用接口
const char* str = "hello wirte!\n";
write(1 str, strlen(str));
fork();//添加了一个fork函数,生成子进程
return 0;
}
|
|
结果分析: 我们往显示器上打,结果是3行,显示器采用的是行缓冲;如果重定向到文件里,此时缓冲方式发生了变化,成了全缓冲,而且在往文件中打印的时候,hello printf和hello fprintf打印了两次,hello write只打印了依次一次。根据上面的结果,我们得出结论:(1.重定向还是不重定向会更改进程的缓冲方式;(2.C接口打了两次,系统调用接口打印了一次。
原因剖析: fork是在程序的最后,根据程序自上而下执行的原理,当执行到fork的时候三个打印函数确实已经执行完了,但是有没有全部刷新呢?有没有全部显示呢?-答案是不一定。当你是往显示器上打印的时候,因为他是行刷新,并且都带了\n,所以三个打印函数都完成了打印&&刷新的工作;当你把程序运行起来重定向到log.txt中时,刷新方式变成了全缓冲,C语言的两个库函数仅仅完成了打印功能,内容还是在缓冲区中,没有刷新,即hello printf和hello fprintf只是打印在了缓冲区,并没有显示出来,当执行到fork的时候,这个缓冲区是是父进程中的一块内存区域,里面保存了当前进程的数据,所以当你fork的时候,后续完成之后,父子进程都要进行数据刷新,因为程序退出时,该缓冲区内存区域就要被刷新了,当父子进程各自进行刷新时,因进程具有独立性,不管父子进程谁先刷新缓冲区,一旦一个进程刷新了缓冲区,刷新缓冲区本质就是修改缓冲区数据,此时就要发生写时拷贝,所以C语言的库函数打印了两次,因为a.缓冲区的存在;b.发生了写时拷贝!
那为什么系统的接口write没有打印两次呢? 说明write没有缓冲区!!
如果缓冲区是操作系统提供的,绝对所有的接口都要打印两次。所以这个缓冲区是C语言自带的!所以我们通常所说的缓冲区是语言自带的缓冲区。所有的缓冲区都在内存,只不过问题是这个内存谁申请的,这个缓冲区是在内存的内核区还是内存的用户区。
我们前面学的是C语言提供的缓冲区,但是缓冲区不止这一个,还有其他的。内存分为用户区和内核区,操作系统是软硬件资源的管理者,我们不可能直接将FILE里面的缓冲区的内容直接刷新到磁盘和显示器中,而是要经过操作系统,所以在内核区还有一个属于操作系统级别的缓冲区,这个缓冲区有自己的刷新机制,会往磁盘和显示器这样的外设刷新缓冲区内容,用户里面的缓冲区内容是往内核中的缓冲区刷新的,我们不需要关心内核中的缓冲区刷新策略。