文章目录
- 缓冲区
- fork后的缓冲区
缓冲区
什么是缓冲区?
缓冲区(Buffer)就是在内存中预留指定大小的存储空间用来对输入/输出(I/O)的数据作临时存储,这部分预留的内存空间就叫做缓冲区。
缓冲区分为内核缓冲区和用户缓冲区
内核缓冲区是网络传输、文件io等等的“中转”,由操作系统控制,是不可见的,用户必须使用系统提供的接口才能使用到,所以说操作系统不相信任何人,只提供相关接口(read、write等)。
一般每个语言都给用户提供了缓冲区,这里以C语言来举例
在使用C语言的printf函数时,默认是向stdout写入的,那么下面做几个测试:
测试1:stdout的类别是显示器
可以看出,没有\n 打印的hello不会及时向显示器输出,而是最后return时才显示。
测试2:stdout的类别是磁盘文件
可以看出,关闭test,5行hello并没有写入到文件,而不关闭fd 却写入文件里了
首先要知道,stdout的类型是FILE*,FILE是C语言提供的,FILE是一个结构体只不过typedef了。
FILE结构体包括了很多数据,其中就有 缓冲区,以及fd=1.
- 因为是c语言提供的FILE,所以这个缓冲区属于C语言缓冲区
- stdout代表显示器,固定去找struct file* array[]中1的位置
测试1和测试2的fprintf是C接口,他们输出的信息都会先缓存到C的缓存区也就是用户缓存区。
测试1没关闭1,此时1的类别属于显示器,那么他就是向显示器输出
- 当我们在函数结尾没加\n时,它不刷新(注意,man函数的return会刷新缓存,所以最后结束时他会刷新一下)
- 当我们加了\n时,他就会刷新缓冲区
测试1的fd1表示显示器,向显示器输出的刷新策略是行缓存:换行是刷新缓冲区;
测试2关闭1,之后在代开test.txt文件,根据文件描述符的分配规则,此时1的类别属于磁盘文件,那么就是向文件输出
- 关闭fd时,向文件输出的字符没有被写入,在结束进程时会刷新缓冲区,但是提前关闭fd就等于“没路了”
- 未关fd时,反而写入文件中了
测试2的fd1表示磁盘文件,向磁盘文件输出的刷新策略是全缓存:全部缓存区满了才会刷新
最终C语言的缓冲区会通过系统调用接口刷新到文件内核缓冲区,通过文件内核缓冲区刷新到外设(磁盘、显示器等等),而测试2中close(fd)等于是提前把test.txt的fd切断了,C缓冲区无法与文件内核缓冲区建立联系,最后数据就留在了缓冲区
注意:内核将缓冲区中的数据“写”到标准输入磁盘文件里,这里“写”不是将缓冲区中的数据移动到磁盘文件里,而是复制到磁盘文件里,也就说此时磁盘文件里保留了一份缓冲区内容的备份
综上,缓冲刷新策略有以下三种:
- 行缓冲(如向显示器刷新)
- 全缓冲(如向磁盘文件刷新)
- 无缓冲(直接写入)
对于测试1和测试2中,可以用fflush(),强制刷新
假如没有缓冲区,我们要从磁盘中读取数据,有几个字符,CPU就要读写几次,CPU是高速率的
例如让打印机打印某个文本,打印机是低速的,CPU是高速的,将数据放入缓冲区,就进行其他的工作了
fork后的缓冲区
假如我们利用fork创建一个子进程,那么子进程的缓冲区是什么样的?
测试:
把输出重定向到文件里,当目标文件发生变化时,操作系统和C语言会自动调整刷新策略
我们知道:
- 先fork在执行代码,子进程和父进程共享同一份,等于打印两份
- 先执行代码再fork,本应该不会打印两份,而实际上除了系统接口write的写入(write写到内核缓冲区,由OS刷新到外设中),全都执行了两份
原因:
重定向到文件时,刷新策略变为全缓冲,而父进程的C的缓冲区属于代码数据,是FILE结构体里的一部分,所以要被共享。可是进程在退出时会把用户缓冲区的数据刷新到系统(进程对数据进行写入),父子进程谁先写入谁就会先发生写时拷贝(只要有一个进程刷新用户缓冲区就代表代码数据发生改变),于是子和父进程就各有一份
解决这个问题,我们可以在 fork 前调用 fflush(stdout) 函数强制把父进程缓冲区的数据刷新到系统。
行写入),父子进程谁先写入谁就会先发生写时拷贝(只要有一个进程刷新用户缓冲区就代表代码数据发生改变),于是子和父进程就各有一份
解决这个问题,我们可以在 fork 前调用 fflush(stdout) 函数强制把父进程缓冲区的数据刷新到系统。