Linux文件:缓冲区、缓冲区刷新机制 | C库模拟实现
一、缓冲区的作用
缓冲区本质上就是一部分内存,用于提高效率!!
对于文件的IO等操作,用户可以直接通过系统调用直接向操作系统进行读操作和写操作。但这样时间开销较大。所以在语言层面一般会维护一段语言级别的缓冲区,用于暂存数据。我们可以快速向缓冲区中写入数据,然后通过一定的刷新方式。将数据从语言级别的缓冲区中拷贝到内核缓冲区。大大提高使用者的效率!!
&mesp;同时由于缓冲区的存在,我们可以积累一定的数据后在统一发生,提高发送的效率!!
二、缓冲区的刷新机制
缓冲区可以暂存数据,必定存在一定的刷新机制。常见的刷新机制由以下3种:
- 无缓冲(立即刷新)
- 行缓冲(行刷新)
- 全缓冲(缓冲区全部写满后在刷新)
其中显示器文件的刷新机制就是行刷新;对于磁盘上的文件则是全缓冲!!
除此之外,还存在一些特殊的刷新机制:
- 强制刷新。比如调用
flush
函数,以及printf
在执行时,碰到/n
时强制刷新! - 进程退出时,一般会刷新缓冲区!
三、测试样例解析
3.1 测试样例和运行结果
下面我们调用3个常见的C库函数和一个系统调用,都向显示器文件中进行写入。当写入操作完毕后创建子进程,我们来看看分别向显示器文件
和磁盘文件
中进行写入会发生什么?
int main()
{
fprintf(stdout, "C: hello fprintf\n");
printf("C: hello printf\n");
fputs("C: hello fputs\n", stdout);
const char* buf = "system call: hello write\n";
write(1, buf, strlen(buf));
fork();
return 0;
}
我们编译后,直接运行向显示器写入
。然后通过输出重定向向log.txt磁盘文件
进行写入:
【运行结果】:
3.2 结果分析
1、向显示器文件写入:
当我们直接向显示器打印消息时,由于显示器的刷新机制是行刷新;并且我们所打印的字符串都带了‘\n
(在printf
中,'\n'
是一种强制刷新的触发机制)。所以在fork()
创建子进程前,数据已经全部被刷新!!
2、向磁盘文件进行写入:
当我们重定向向磁盘文件写入数据时,缓冲区的刷新机制由行缓冲变成全缓冲。全缓冲也就意味着缓冲区变大,简单的几个字符串不足以将缓冲区写满,无法触发刷新机制。
此时缓冲数据还在缓冲区。但当前缓冲区为C语言所提供的缓冲区,和操作系统无关。所以当前缓冲区中的数据依然属于进程。当fork()创建完子进程退出时,一般会刷新缓冲区。
而刷新缓冲区本质上也是一种清空或者写入操作。所以父进程和子进程在退出前数据共享同一份;当退出时刷新缓冲区,对数据进行修改会发生写时拷贝,父、子进程各自私有一份。所以最后对于C函数调用会存在两份!!
至于系统调用数据只打印一份的原因在于:系统调用在语言之下,数据不是向语言基本的缓冲区中写入;而是直接向操作系统内核缓冲区中写入。此时数据属于操作系统,不在属于进程!!
四、语言级别的缓冲区究竟在哪?
下面我们以C为例。
在此时样例中,我们已经知道对于库函数printf、fprintf
自带缓冲区,而系统调用write
则没有。(内核提提供的缓冲区此处不考虑)库函数时对系统调用的封装,这也意味着C所提供的缓冲区是二次加上的,由C本身所提供!!
在C中,所有的IO函数都存在一个FILE
指针,或者底层会封装FILE
指针。而C的缓冲区的相关信息则保存在FILE
结构体中,由FILE
结构体来维护!!
【FILE
结构体内容】:
在/usr/include/stdio.h
路径下,存在这样一段代码typedef struct _IO_FILE FILE
。所以可以通过下面操作查找FILE:
【FILE
结构体】:
在/usr/include/libio.h
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags