Linux第19节 --- 用户缓冲区和文件系统
对比试验一
假设我们当前正常使用四个函数进行往显示器进行打印数据:
此时无论是直接显示还是重定向到文件中,都可以正常显示!
但是当前我们如果在代码结尾处加上fork();
通过结果我们此时可以发现:加上fork()之后,此时在显示器上可以正常显示!但是如果重定向,此时的打印结果变得不一样!
对比试验二
- 如果我们不加\n,此时再来查看结果(只有C语言的接口):
结果无法显示!
即C提供的库函数接口无法显示!
- 接下来我们查看对于系统调用接口write:
此时write能够正常打印出来结果!
这是因为当我们调用C类型的接口的时候,此时没有+\n,但是输出的内容已经写入到了缓存区!
但是这个缓冲区不在操作系统内部,不是系统级别的缓冲区!
C语言会给我们提供一个缓冲区(用户级缓冲区!)
当我们调用printf/fprintf/fwrite/fputs这些C接口函数的时候,此时当我们不带\n时,我们是将数据写入到C给我们提供的缓冲区当中,然不是内核提供的缓冲区!
close只会刷新内核中的缓冲区!而不会对C提供的缓冲区进行刷新!
问题:为什么+上\n就可以显示成功?
这是因为显示器的刷新规则是行刷新!所以在printf在执行\n的时候,会将数据从C语言的缓冲区刷新到内核当中!
用户刷新的本质:就是将数据通过1+write写入到内核当中!
exit与_exit和缓冲区的联系
exit()是C语言提供的调用接口,能刷新缓冲区!
_exit()是系统调用接口,不能刷新缓冲区!
上面的就是exit()的实际上的操作;
这是因为C语言对_exit进行了封装,实际上这里exit()做的工作就是刷新 + _exit(),因此可以实现刷新,但是_exit()根本不知道上面一层C语言提供的缓冲区,因此没有刷新操作!
目前我们可以认为,只要我们将数据刷新到内核当中,操作系统就可以将数据写入或者刷新到显示器\磁盘当中!
接下来我们考虑缓冲区的刷新策略(C语言对应的缓冲区 --- 应用层缓冲区):
- 无缓冲 --- 直接进行刷新;标准错误(
stderr
)默认无缓冲,直接输出; - 行缓冲 --- 不刷新,直到遇到\n;标准输出(
stdout
)默认行缓冲,遇到\n
或缓冲区满时刷新; - 全缓冲 --- 缓冲区满了才刷新;文件操作默认全缓冲,需手动调用
fflush
或关闭文件时自动刷新
这里我们如何调用write取决于我们当前的刷新策略!
之前在C语言,我们学习过一个函数fflush,其是一个C语言的库函数!
主要功能:强制刷新输出缓冲区,确保数据立即写入目标设备(如屏幕、文件)或传递给内核,而非停留在用户态缓冲区。(因为刷新缓冲区,所以底层封装了write)
fflush底层是否会封装write?为什么?(deepseek)
答案:是的,fflush底层会间接调用write系统调用,这是由C标准库的缓冲机制设计决定的。
-
底层实现逻辑:
- 用户态缓冲区:C标准库(如fwrite、printf)在用户空间维护了一个缓冲区(位于
FILE
结构体中),用于暂存数据,以减少频繁的系统调用开销 。 - fflush的作用:当调用
fflush(fp)
时,C库会将用户态缓冲区(如_buf
数组)中的数据通过write
系统调用写入内核缓冲区(Page Cache),完成从用户态到内核态的数据传递 。 - 示例:在网页1的代码中,自定义的
_fflush
函数通过write(fp->_fileno, fp->_buf, fp->_size)
实现数据写入,直接调用了系统调用 。
- 用户态缓冲区:C标准库(如fwrite、printf)在用户空间维护了一个缓冲区(位于
-
设计原因:
- 减少系统调用开销:频繁的
write
会引发用户态与内核态的上下文切换,影响性能。通过缓冲区积累数据后批量写入(如缓冲区满或主动刷新),可显著提升效率 。 - 兼容性与标准化:C标准库通过封装系统调用(如
write
),提供跨平台的I/O接口,同时隐藏底层差异 。
- 减少系统调用开销:频繁的
当进程退出的时候,也会进行刷新!
例如当前我有如下所示代码:
此时printf往显示器打印,默认是行刷新,但是这里我们没有带\n,但是系统结束的时候也会打印出来!因为进程结束也会进行刷新!
但是如果我们加上close(1);
此时进程结果,操作系统想要进行刷新,但是对应的文件操作符已经关闭了!不能刷新!
为什么要有缓冲区呢?
- 解决效率问题(用户的效率问题) --- 类比快递公司,帮忙送快递而不需要人们自己去,且有快递点,满足条件才发货(类比刷新策略)
- 配合格式化输出(显示器显示的都是字符!因此对于printf(%d,a),实际上是将a转化为字符!写入到缓冲区当中!)
C语言中的printf实际上就是格式化输出接口;
格式化输出接口是编程语言中用于将数据按特定格式转换为字符串或直接输出到设备(如控制台、文件、网络)的标准化方法。
问题:这个缓冲区在哪里?
在C语言中,这个缓冲区在FILE对应的结构体里面!
在C语言中,所有对文件的操作都绕不开FILE这个结构体!
这个结构体除了包含fd文教操作符,还包含缓冲区字段和对应的维护信息!
如果我们在C语言打开10个文件,那么当前我们有多少个缓冲区?
答案是10个!每个文件都有它对应的缓冲区!(10个文件描述符!)
问题:这个FILE对象属于用户还是属于操作系统?这个缓冲区是不是属于用户级别的缓冲?
是用户的,且这个缓冲区属于用户级缓冲区!实际上就是malloc的一段空间!
像文件中打印写入的时候,此时的缓冲策略变成了全缓冲!
如何证明上面这一点?
加入当前我们有如下所示的代码:
printf,fprintf,fwrite三个C提供的库函数都是像缓冲区内打印的,write是直接向内核打印的;
因此我们可以看到下面的结果:
此时write是直接向内核写入的,因此我们可以看到其结果;
但是printf,fprintf,fwrite三个函数是向C的缓冲区写入的,当程序结束的时候才进行刷新!
此时虽然有\n,但是刷新策略是全缓冲,因此\n不起作用!
对试验二现象的解释:
因此此时我们可以解释下对比实验二中的结果:
对于打印在显示器中的程序:
fork()之后,子进程只会继承fork()之后的代码,而当前因为我们是想往显示器中打印程序,刷新策略是行刷新,因此此时fork()之前,父进程由于存在\n已经刷新打印在了显示器中,子进程此时进程父进程的缓冲区的状态,由于缓冲区已经被刷新出来,所以子进程不会打印东西!
对于打印在文本中的程序:
fork()之后,子进程只会继承fork()之后的代码,而当前因为我们是想往显示器中打印程序,刷新策略是全刷新,或者是进程结束的时候才进行刷新;此时父进程缓冲区还未被刷新到内核当中!子进程继承父进程的缓冲区状态(3条C接口的信息未被刷新),所以此时子进程也会打印3条信息!
对于C语言,无论是在mac,windows,还是Linux,上层提供的接口都是一样的,但是底层调用的系统调用接口是不一样的!因此可以这样认为C语言是具有跨平台属性的!
scanf对应的也有输入缓冲区!
系统的缓冲区是不是直接刷新到磁盘?
答案:不是的!也是和语言缓冲区到内核缓冲区类似,有对应的刷新策略!(无缓冲或者全缓冲!)
close本身不涉及刷新缓存,fclose才会在进程退出的时候进行缓存刷新!