C++与Linux基础:文件篇(5)文件管理系统与缓冲区实现
1. 知识回顾和新的内容介绍
在上一篇文章中我们详细谈了内存中的文件是如何操作的,这篇文章将会用到前面的知识,我们将自己实现用系统的接口封装一个 fread 和 fwrite 的接口。
在完成这个任务之前,我们必须先讲解两个缓冲区:一个是系统的缓冲区,一个是语言比如 C 语言的缓冲区。只有这样,我们才能理解后面是如何封装的。
接下来会从下面几个点来完成今天的内容:
- 什么是缓冲区?
- 为什么要引入缓冲区
- 实现 fwrite 和 fopen
2. 缓冲区的概念和作用
2-1 缓冲区的概念
缓冲区的本质其实很简单:就是一块在堆上(或栈上)动态开辟的、用于暂存数据的内存空间,其底层实现通常是一个字符数组(char 数组)。
缓冲区是内存中预留的一部分空间,用于临时存储输入或输出的数据,以协调不同速度的设备或操作之间的数据流。在计算机系统中,缓冲区常见于文件 I/O、网络通信等场景。
它只是一个用来临时存储数据的地方,有它自己的刷新方式,在后面我们会讲它是行刷新还是满了才刷新进入文件中。
2-2 缓冲区的作用
在深入之前,我们回顾一下 Linux 是如何通过系统接口打开文件的。
当我们的程序跑起来时,内核创建了一个进程控制块(PCB),也就是 task_struct。在这个巨大的结构体中,有一个极其关键的指针 struct files_struct *files。这个指针指向了一张文件描述符表。
我们可以把这张表想象成一个指针数组(fd_array),数组的下标就是我们常说的文件描述符(fd)。而数组的内容,则是指向 打开文件结构体(struct file) 的指针。(默认情况下,进程启动时已经填充了前三个位置(0, 1, 2),分别对应标准输入、标准输出和标准错误)
当我们调用 write 接口时,内核通过 fd 索引找到对应的 struct file,进而通过它找到文件对应的 inode(索引节点),从而定位到文件在磁盘上的位置。
但是!这里有一个关键点: 如果我们每次调用 write 都直接把数据往慢速的磁盘上刻,那系统的效率将惨不忍睹。因此,Linux 引入了 页缓存(Page Cache)。我们的 write 操作通常只是将数据从用户态拷贝到了内核态的内存(Cache)中就直接返回了,真正的'落盘'操作则由操作系统在后台择机完成。这才是 Linux 高效 IO 的秘诀。
简单来说,就是为了满足 CPU 和磁盘速度不匹配而导致的效率问题。
总结一下就是:引入缓冲区的主要原因包括:
- 减少系统调用开销:每次直接读写磁盘或外设都需要通过系统调用,导致 CPU 在用户态和内核态之间切换,消耗资源。缓冲区可以累积多次操作的数据,一次性进行系统调用,从而降低切换频率,提升效率。
- 平衡速度差异:CPU 处理速度远高于磁盘、打印机等 I/O 设备。缓冲区作为中间层,允许 CPU 快速将数据暂存后继续执行其他任务,而 I/O 设备可以慢慢处理数据,避免 CPU 等待。
- 优化数据组织:例如行缓冲区(用于终端输入输出)可以在遇到换行符时刷新,提升交互体验;全缓冲区(用于磁盘文件)则填满后才写入,减少磁盘访问次数。
C 语言在封装
FILE这个结构体的时候其实也给了缓冲区,你知道是为什么吗? C 语言缓冲区的目的,不是为了减少磁盘 IO(那是内核 Page Cache 的工作),而是为了减少系统调用的次数。因为调用系统调用的'价格'实在是太昂贵了。每次调用都要向内核反映问题。相当于每次过高速路都要收取高速费用。 为了解决这个问题,我们不得不一起打包好,在一起送到系统的调用中。这就是为什么我们在写高性能 C/C++ 服务时,有时候甚至觉得 C 库的 buffer 都不够好,还要自己在业务层再写一个 Buffer(比如 Log 系统里的双缓冲区),目的都是一样的:




