
🎬 个人主页:HABuo
📖 个人专栏:《C++系列》《Linux系列》《数据结构》《C语言系列》《Python系列》《YOLO系列》
⛰️ 如果再也不能见到你,祝你早安,午安,晚安

目录
📚一、文件缓冲区
📖1.1 用户级缓冲区
📖1.2 内核缓冲区
📚二、总结
前言:
上篇博客我们谈论了重定向的问题,今天我们来学习了解我们在学习编程语言当中时常遇到的一个问题:缓冲区,但是没人给我们讲清楚,缓冲区是什么?缓冲区在哪?缓冲区是用来做什么的?那今天就让我们一举搞定它!
本章重点:
本篇文章着重讲讲解缓冲区是什么,缓冲区的作用,语言层级缓冲区在哪,它的刷新策略是什么,内核缓冲区在哪,整体的流程是什么!
📚一、文件缓冲区
我们先看一个现象:
int main()
{
//C接口
printf("hello printf!\\n");
fprintf(stdout, "hello fprintf!\\n");
const char* fputsString = "hello fputs\\n";
fputs(fputsString, stdout);
//系统接口
const char* wstring = "hello write\\n";
write(1, wstring, strlen(wstring));
return 0;
}

int main()
{
//C接口
printf("hello printf!\\n");
fprintf(stdout, "hello fprintf!\\n");
const char* fputsString = "hello fputs\\n";
fputs(fputsString, stdout);
//系统接口
const char* wstring = "hello write\\n";
write(1, wstring, strlen(wstring));
fork();
return 0;
}

可以看到增加了fork之后C语言的接口往文件里写了两次,系统调用接口只写了一次,但是直接在显示器上打印的却又都只有一次,让人脑大!这其实就是缓冲区搞的鬼,让我们来一探究竟!
📖1.1 用户级缓冲区
事实上缓冲区的本质就是一段内存空间,那么为什么要有缓冲区?
首先我们来理解下缓冲区是什么,有什么作用:
你在浙江理工大学想要将一本书送给你在厦门大学的好友,你会亲自将书带过去给他然后再回杭州吗?显然不可能你会去楼下的顺丰快递将书籍让顺丰帮你寄到厦门,你就代表一个用户,而书籍就是你要发送给其他用户的数据,顺丰就是这个缓冲区。很明显缓冲区有以下性质:
- 顺丰拿到你的快递立刻发送(立刻刷新)
- 等累计快递达到一定数量统一发送(行刷新)
- 或者当快递站放满了再发送快递(满刷新)

所以我们清楚了,缓冲区就是为了提高效率而存在的,它其实就是一块内存!
并且我们也要清楚缓冲区的刷新策略:
- 立即刷新,无缓冲
- 行刷新,行缓存 —>显示器(给人看的)
- 缓冲区满,全缓冲 —>磁盘文件
- 两种特殊情况
1.用户强制刷新
2.进程退出–一般都要进行缓冲区刷新
那这个缓冲区在哪呢?
首先告知:上述的现象一定和缓冲区有关!
—> 缓冲区一定不在内核中!!因为如果在内核中,write也应该打印两次!
—> 我们之前谈论的所有的缓冲区,都指的是用户级语言层面给我们提供的缓冲区!!
—> 这个缓冲区,在stdout,stdin,stderr->FILE*->FILE结构体->fd&还包括了一个缓冲区!!!
这也就清楚了为什么上面重定向时,即使stdout的文件描述符1已经不指向原有的映射但是我们依然刷新
的是stdout,正是因为stdout的类型是FILE*即C语言封装的FILE里面包含了fd和缓冲区,无论你
文件描述符指向如何变化,但FILE里的内容依然是stdout的,你要想把内容刷新到其它地方,必须要刷新我!
那如何解释上述现象呢?
代码结束之前,进行创建子进程
1.如果我们没有进行〉,看到了4条消息
—> stdout默认使用的是行刷新,在进程fork之前,三条C函数已经将数据进行打印
—> 输出到显示器上(外设),你的FILE内部即缓冲区,进程内部不存在对应的数据了
2.如果我们进行了〉
—> 写入文件不再是显示器,而是普通文件,采用的刷新策略是全缓冲
—> 之前的3条c显示函数,虽然带了\\n,但是不足以将stout缓冲区写满! 数据并没有被刷新!!!
—> 执行fork的时候,stdout属于父进程,创建子进程时,紧接着就是进程退出!
—> 谁先退出,一定要进行缓冲区刷新(就是修改)
—> 也就要发生写时拷贝!!数据最终会显示两份
注:写时拷贝是修改什么就拷贝什么,要不要对整个FILE进行更改取决于整个FILE的内容是否发生改变
3.write为什么没有呢?
—> 上面的过程都和wirte无关,wirte没有FILE
—> 而用的是fd,就没有c提供的缓冲区
📖1.2 内核缓冲区
经过上面的学习你肯定了解了由C语言与系统调用接口打印的现象到用户级缓冲区理解的本质,但是随之而来的就会有一个问题,那它和OS有什么关系呢?整体的链条又是一个什么样的呢?请继续往下看:

所以一句话,就是之前所说的缓冲区是用户级缓冲区,即在操作系统之上存在的,那我们所说的行缓冲、全缓冲等都是C语言所给我们做的,但是操作系统与磁盘之间还存在一个内核缓冲区(这也就是我们在Linux源代码struct_file里面有一个缓冲区这就对的上了),它的缓冲策略完全由操作系统自主决定这个没法证明,只能看到接口,所以有个fsync接口就是让操作系统立马给我们缓冲到磁盘!

整体的框架可以见下:
┌─────────────────────────────────────────────────────────────┐
│ 用户空间 (User Space) │
├─────────────────────────────────────────────────────────────┤
│ 应用程序 (Application) |
│ ┌─────────────────────────────────────────────────────┐ │
│ │ FILE *fp = fopen("file.txt", "w"); │ │
│ │ fprintf(fp, "Hello World"); │ │
│ │ fclose(fp); │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ C标准库 (libc) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ struct _IO_FILE (FILE结构体) │ │
│ │ ├─ 用户态缓冲区 (User Buffer) │ │
│ │ ├─ 文件描述符 (fd) │ │
│ │ └─ 操作函数表 (vtable) │ │
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 系统调用接口 (System Call Interface) │
├─────────────────────────────────────────────────────────────┤
│ 内核空间 (Kernel Space) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 虚拟文件系统 (VFS) │ │
│ │ ├─ struct file (内核文件对象) │ │
│ │ ├─ struct inode (索引节点) │ │
│ │ └─ struct dentry (目录项) │ │
│ │ │ │
│ │ 页缓存 (Page Cache) │ │
│ │ ┌─────────────┬─────────────┬─────────────┐ │ │
│ │ │ 页帧 │ 页帧 │ 页帧 │ │ │
│ │ │ (脏页标记) │ (干净页) │ (脏页标记) │ │ │
│ │ └─────────────┴─────────────┴─────────────┘ │ │
│ │ │ │
│ │ 块设备层 (Block Layer) │ │
│ │ ┌─ I/O调度器 (I/O Scheduler) ─┐ │ │
│ │ │ CFQ/Deadline/NOOP │ │ │
│ │ └─────────────────────────────┘ │ │
│ │ │ │
│ │ 设备驱动层 (Device Driver Layer) │ │
│ │ ┌─ 通用块层 (Generic Block Layer) ─┐ │ │
│ │ │ struct bio (块I/O请求) │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 硬件抽象层 (Hardware Abstraction Layer) │
├─────────────────────────────────────────────────────────────┤
│ 物理设备 (Physical Device) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SATA/NVMe控制器 │ │
│ │ ┌─ 设备固件 (Firmware) ─┐ │ │
│ │ │ 闪存转换层 (FTL) │ │ │
│ │ └──────────────────────┘ │ │
│ │ │ │
│ │ NAND闪存芯片 │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │页 0 │ │页 1 │ │页 2 │ … │ │
│ │ └─────┘ └─────┘ └─────┘ │ │
│ │ ┌─────────┐ │ │
│ │ │坏块表 │ │ │
│ │ └─────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
📚二、总结
本篇博客我们了解了缓冲区的相关原理
小结一下:
语言层级缓冲区:
- 在哪:在FILE结构体里
- 刷新策略:①立马刷新。②行刷新–显示器。③全刷新–磁盘。特殊情况:①进程退出②主动强制刷新
语言层级缓冲区与OS关系:
- 程序调用 —> FILE(文件描述符和缓冲区) —> struct_file —> 内核缓冲区 —> 磁盘





