【Linux手册】缓冲区:深入浅出,从核心概念到实现逻辑
文章目录
- 前言
- 缓冲区是什么
- 缓冲区在哪
- 缓冲区的刷新策略
- 为什么要有缓冲区
- 缓冲区是独立的
- 模拟实现缓冲区
- 实现FILE结构体
- 实现fopen
- 实现fflush
- 实现fwrite
- 实现fclose
- 总结
前言
在学习C语言的时候,我们经常会听到缓冲区这一概念,尤其是初学C语言的时候老师给我们展示:
int main()
{printf("hello world");sleep(5);return 0;
}
代码运行printf时居然没有打印内容,而是在5秒之后内容才输出的。老师只告诉我们printf内容时先放在缓冲区里面的,\n,程序退出时都会刷新缓冲区,所以才在5秒之后打印内容。
但是对于缓冲区到底是什么,为什么要有缓冲区,缓冲区在哪等等我们还是无法理解。本文将对缓冲区各方面知识进行剖析,整合归类,相信通过本文你会对缓冲区的理解更深一步。
缓冲区是什么
在前面关于文件操作接口的文章中,我们介绍了各种对文件操作的接口,其中既有库函数也有系统接口,我们也知道操作系统是通过文件描述符来识别文件的。
printf打印的内容是先放在缓冲区里面的,当程序结束或遇到\n后才刷新到文件中去,那么如果在程序结束之前将stdout流关闭,那是不是就打印不出来内容了???
上面的代码确实没有输出任何内容,那么如果将库函数换成write系统接口会不会打印出来内容???
输出结果如下图所示:
使用write确实打印出了内容,而使用printf却没有打印出内容,这是为什么???
是因为write输出的内容不需要先写到缓冲区里面吗???
实际上在计算机中有两个缓冲区分别是文件缓冲区和语言提供的用户级别的缓冲区(以下称其为用户级缓冲区),我们常说的缓冲区通常指的是用户级别的缓冲区。
- 对于文件缓冲区来说:文件缓冲区就在操作系统内部,一些操作系统接口进行读取和写入的时候都是从文件缓冲区中进行读取和写入的,所以write向文件中进行写入时要先向文件缓冲区中进行写入。
- 对于用户级缓冲区来说:用户级缓冲区不是操作系统提供的,可以理解为操作系统都不知道有这个缓冲区,该缓冲区是语言提供的,在调用一些库函数进行写入的时候要先将数据写入到该缓冲区中,当缓冲区中的内容满足一定的条件的时候就会被刷新到文件缓冲区中,再通过文件缓冲区写入到文件中。
那为什么close关闭stdout文件后,write能成功写入数据,而printf无法写入数据???
这是因为close()在关闭文件的时候会刷新该文件的文件缓冲区,所以write()的数据能够被刷新到文件中,那么为什么close()不去刷新用户级缓冲区,在前面我们说了用户级缓冲区操作系统根本就不知道也看不见,那么操作系统接口也同样不知道还有其他的缓冲区,close()接口无法对用户级缓冲区进行刷新;所以printf的内容在用户级缓冲区中没有被刷新,当程序结束时还是没有被刷新。
通过以上介绍,我们实际上就可以理解我们日常说的缓冲区刷新就是:将用户级缓冲区中的内容通过调用一些像write()的接口写入到文件缓冲区中。
缓冲区在哪
- 文件缓冲区毫无疑问是在操作系统内部,由操作系统进行管理;
- 对于用户级缓冲区是语言提供的,那么一定是使用的语言进行管理的,不同的语言可能还存在一些差异。
在C语言中是使用结构体对文件进行管理的FILE结构体,缓冲区就放在该结构体里面,是动态开辟出来的堆空间:
所以刷新缓冲区原理图如下:
所以这也就是问什么系统调用接口_exit()在总计进程的时候,我们感觉不会刷新缓冲区的原因,此处的缓冲区指的是用户缓冲区,因为_exit()是系统调用接口,根本就不知道上层还有缓冲区。
缓冲区的刷新策略
此处的缓冲区指的是用户级别的缓冲区,关于文件缓冲区刷新归操作系统管,我们不用操心。
- 无缓冲,直接进行刷新;
- 行缓冲,一行为单位进行刷新,当遇到\n的时候才刷新,比如显示器;
- 全缓冲,当缓冲区填满时,才进行刷新,比如普通文件。
还有,当进程退出的时候也会刷新缓冲区,也可以调用fflush()函数进行刷新。
为什么要有缓冲区
- 使用缓冲区,先将需要输出/输入的信息缓存起来,等到信息多了之后在调用系统接口将信息刷新到文件缓冲区中,通过这种积累,打包的方式可以减少系统接口的调用,进而提高操作系统效率;
- 配合格式化,在调用printf时会使用%s,%d等占位符,所以不能直接将字符串写入到文件中,而需要一个中转站将这些占位符进行替换,然后在写入到文件中,而缓冲区就可以作为这个中转站。
缓冲区是独立的
此处的缓冲区指的依旧是用户级缓冲区。缓冲区在进程之间是独立的,这并不难理解,因为上面我们说了缓冲区其实就是C语言中FILE结构体中的堆空间,是进程的数据,进程之间是独立的,那么这些数据也必定是独立的。
以下代码的输出结果是什么样的???
输出结果:
为什么printf被打印了两次,而write只被打印了一次???
答案:因为缓冲区属于进程的数据,所以在创建子进程后,缓冲区中的数据父进程和子进程以写实拷贝的方式独自占有一份,最终在进程结束时都刷新到文件缓冲区中了;而write()系统调用接口的数据在创建子进程之前已经写入到了文件缓冲区中,已经不再是进程的数据了,所以只打印了一份。
以下代码会有什么结果???
输出结果:
为什么printf还是打印了两边,明明在打印末尾加了\n呀,为什么还是缓冲区没有刷新???
因为在将一号文件替换之后,一号文件变成了普通文件,普通文件的刷新策略不再是行缓冲了,而是全缓冲,所以没有进行刷新。
模拟实现缓冲区
关于缓冲区的原理和运行方式我们都已经有了一个初步的了解,能不能简单模拟设计一个缓冲区的方案,实现分为4个板块:
- 模拟实现FILE结构体;
- 模拟实现fopen打开文件;
- 模拟实现fflush刷新缓冲区;
- 模拟实现fwrite向文件中写入;
- 模拟实现fclose关闭文件。
实现FILE结构体
结构体中必须:
- 文件描述符来确定指向的文件;
- 输入输出缓冲区,以及使用情况;
- 缓冲区处理方案。
实现fopen
- 调用系统接口open打开文件;
- 对FILE结构体进行初始化。
实现fflush
- 调用系统接口write将缓冲区中的内容写到文件缓冲区中;
- 对FILE结构体进行更新。
实现fwrite
- 将字符串中的内容写到缓冲区中;
- 判断是否需要刷新缓冲区;
- 更新FILE成员。
实现fclose
- 刷新缓冲区中的数据;
- 调用系统接口close关闭文件。
总结
缓冲区实际上并不是太难理解,我们常说的缓冲区就是用户级缓冲区,实际上就是程序开辟的堆空间。具体实现也不是分复杂。关于缓冲区内容就该一段落了,有任何问题欢迎与博主实现交流。