🌟hello,各位读者大大们你们好呀🌟
🍭🍭系列专栏:【Linux初阶】
✒️✒️本篇内容:库函数和系统函数的刷新差异,缓冲区的意义、刷新策略、存在位置、文件写入的逻辑,缓冲区的代码实现,缓冲区和OS的关系
🚢🚢作者简介:计算机海洋的新进船长一枚,请多多指教( •̀֊•́ ) ̖́-
文章目录
一、为什么我的库函数输出两次,系统函数只输出一次?
在正式学习缓冲区知识前,我们用一个代码问题作为引入
#include <stdio.h>
#include <string.h>
int main()
{
const char* msg0 = "hello printf\n";
const char* msg1 = "hello fwrite\n";
const char* msg2 = "hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
运行出结果:
hello printf
hello fwrite
hello write
但如果对进程实现输出重定向呢?./hello > file
, 我们发现结果变成了:
hello write
hello printf
hello fwrite
hello printf
hello fwrite
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。
假如我们将 fork()代码注释掉,再运行上面的重定向指令,我们可以发现它会正常将 write、printf、fwrite三个信息重定向到 file文件中,因此我们可以推断出问题肯定和fork有关!
要理解上面问题出现的原因,就要涉及我们接下来要学习的缓冲区的知识了。
———— 我是一条知识分割线 ————
二、理解缓冲区问题
1.缓冲区存在的意义
在这里我先给出一条结论:缓冲区实际上就是一段内存
。
那么缓冲区是什么?谁提供的?为什么要存在呢?
内存的进程要将数据写入到磁盘的文件,这个动作的实现,首先需要在内存中开辟一块缓冲区,进程将数据拷贝到我们的缓冲区中,然后进程对应的拷贝函数就直接返回了,返回之后,进程就可以继续向后执行自己的代码了。
在进程执行代码期间,缓冲区中的数据会定期的发送到磁盘的文件中。
为什么进程不直接自己将数据写入到文件中呢?这是因为对于进程来说太消耗时间了,我们知道,进程写入磁盘文件的这个动作属于IO,涉及到外设,因此相对比较慢,如果进程自己执行这个动作,效率会很低。
至此,我们就上面的三个问题先做一个回答:
缓冲区实际上就是一段内存
;缓冲区是用户级语言层面(语言库)给我们提供的
(后面会将讲解);- 缓冲区的意义是什么呢?
节省进程进行数据 IO的时间
。
那么问题又来了:你说我将信息拷贝给了缓冲区,我哪里有拷贝呢?
以 fwrite函数为例,与其我们将 fwrite理解为写入文件的函数,倒不如将 fwrite理解为拷贝函数!将数据从进程,拷贝到缓冲区或外设中。
———— 我是一条知识分割线 ————
2.缓冲区的刷新策略
假设我们有一块数据,我们是应该一次性写入到外设 还是 少量多次的写入到外设呢?
实际上,一次性写入到外设是效率最高的。在我们 IO的过程中,最耗费时间的并不是数据的拷贝时间,以 1秒为例,有可能 990毫秒都在等待外设准备就绪,10毫秒完成拷贝。因此,对于计算机来说,一次性写入效率是最高的。
缓冲区一定会根据具体的设备,定制自己的刷新策略。一般而言,我们有三种策略
和 两种特殊情况
:
三种策略
- 立即刷新 - 无缓冲(不常见);
- 行刷新 - 行缓冲(显示器就是使用行缓存);
- 缓冲区满 - 全缓冲(磁盘文件就是全缓冲);
以显示器为例,显示器采用行缓冲,因为显示器需要满足人的阅读需求(及时高效反馈),使用行缓存可以在满足人的需求的同时,保证缓冲效率不会太低。
以磁盘文件为例,IO时采用全缓冲,等待磁盘响应的次数最少,可以保障最大的效率。
两种特殊情况
- 用户强制刷新;
- 进程退出 - 一般都要进行缓冲区刷新;
为什么有些程序在没有强制刷新要求的情况下,数据迟迟不刷新到显示器上?很大可能就是这个程序的进程还没有退出,缓冲区的数据还没有刷新。
———— 我是一条知识分割线 ————
3.缓冲区在哪里?
再次回到我们一开始的那个代码问题的运行结果:
hello write
hello printf
hello fwrite
hello printf
hello fwrite
告知:上面这个结果的出现,一定和缓冲区有关。
那么这个缓冲区是谁提供的呢?
通过对我们一开始的那个代码运行结果的分析,我们可以得出,缓冲区不在内核中,因为如果缓冲区在内核中,write也应该要被打印两次。
结论:我们之前谈论的缓冲区,都是
用户语言层面(语言库)给我们提供的缓冲区
。C语言的缓冲区是C标准库提供的。
缓冲区在哪里?
在我们进行文件读写过程中,涉及到的调用接口(stdout、stdin、stderr),都是 *FILE 类型的,*FILE会被 FILE结构体管理,在这个结构体中,就包含了 fd(文件描述符) & 一个缓冲区
。
总结:这个缓冲区,在 stdout、stdin、stderr -> *FILE -> FILE结构体 -> fd & 一个缓冲区
因此,我们强制刷新时,需要使用 fflush(文件指针)
,关闭文件时,需要 fclose(文件指针)
。
———— 我是一条知识分割线 ————
4.文件写入的底层逻辑(1)
如果有兴趣,可以看看FILE结构体:
typedef struct _IO_FILE FILE; 在/usr/include/stdio.h
在 / usr / include / libio.h
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base;