IO 缓冲区
认识
首先我们要明白一些概念,用户级和内核级,可以简单的理解为,我们平时的编程就是在用户级干活,而内核级就是操作系统的地盘,当我们需要操作系统帮忙的时候,就需要调用操作系统提供的一些接口,也就是系统调用(其实就是些函数),这时候就是由用户级进入到内核级执行
然后,我们还要理解一下缓冲区的概念,其实你可以理解为过渡,举个例子,有两个岛,中间是一片海,游客想从A岛到B岛,就得坐船,对船夫来说,当人都坐满的时候,在划船最划算(不然一个游客就划一个来回,都累死了),其次,如果船够大,一次能够运送的游客就越多,也更划算
ok,以上这个例子其实就是缓冲区,岛A是用户级,岛B是内核级,这个船就是缓冲区了,游客就是传递的数据,缓冲区其实就是暂存数据,等缓冲区满了在传递给内核,这样只需一次系统调用就能传递许多数据,非常划算,其次缓冲区的大小也很重要
有了这些概念后,直接看下面这个图,分为用户态和内核态(其实就是用户级和内核级)
ok,我们看到用户态和内核态之间是隔着 I/O系统调用 的,这个就是二者交互的接口啦(或者说分界线),现在让我们按顺序来讲讲数据是怎么传递的吧
首先,这是我们的用户数据,它经过一些C库函数(stdio库)的操作进入到 stdio缓冲区(用户态缓冲区),然后等到缓冲区满或者其他一些条件满足,经由 I/O系统调用 进入到 内核缓冲区,然后在等缓冲区满或者其他一些条件满足,经由 内核发起的写操作 写入到磁盘中,这是写操作的,读操作其实就是逆向啦
然后呢,以上是正常的步骤,就是图中实箭头的部分,虚箭头是通过其他的一些手段 加快缓冲区的刷新(即数据的传递,比如说不用等缓冲区满等等)
stdio库的缓冲
设置一个 stdio 流的缓冲模式
调用 setvbuf()函数,可以控制 stdio 库使用缓冲的形式
-
参数介绍
- FILE *stream: 文件流,即代表要操作的文件,打开流后,必须在调用任何其他 stdio 函数之前先调用 setvbuf()
- char *buf: 缓冲区
- buf 不为 NULL,那么其指向 size 大小的内存块以作为 stream 的缓冲区因 为 stdio 库将要使用 buf 指向的缓冲区,所以应该以动态或静态在堆中为该缓冲区 分配一块空间(使用 malloc()或类似函数)
- 若 buf 为 NULL,那么 stdio 库会为 stream 自动分配一个缓冲区(除非选择非缓冲的I/O)
- size_t size: 指定缓冲区的大小
- int mode: 指定缓冲类型
这里我认为需要注意的是,行缓冲模式下,换行符 可以理解为缓冲的结束标志
还有一些类似功能的函数,比如 setbuf, setbuffer,这里就不展开了
刷新 stdio 缓冲区
flush 的作用是将 指定文件流中的所有未写入的数据立即写入到文件中。它确保缓冲区的数据被实际输出到文件,以便用户能看到最新的内容。通常用于处理输出流(如标准输出)时,确保数据及时可用
stdin 和 stdout 的行为影响输入输出的顺序和刷新机制。若 stdin 和 stdout 指向终端,读取 stdin 时会隐式刷新 stdout,但这不包括换行符。这意味着你应该手动调用 fflush(stdout) 来确保提示信息立即显示,以提高可移植性
换句话说就是,一般我们在让用户输入时,都会有个提示符,可能使用printf或者其他的,这里的意思就是让我们在printf后接上 fflush(stdout),确保在输入前,提示信息能够立即显示,然后不包括换行符是因为 换行符会自动触发缓冲区刷新(行缓冲模式)
若打开一个流同时用于输入和输出,则 C99 标准中提出了两项要求。首先,一个输出操作不能紧跟一个输入操作,必须在二者之间调用 fflush()函数或是一个文件定位函数(fseek()、fsetpos()或者 rewind())。其次,一个输入操作不能紧跟一个输出操作,必须在二者之间调用一个文件定位函数,除非输入操作遭遇文件结尾
-
输出后接输入:如果你先执行了输出操作,比如 printf,然后直接进行输入操作,比如 scanf,可能会出现输入读取错误。因此,必须在这两者之间调用 fflush(stdout) 或者文件定位函数,如 fseek()
-
输入后接输出:如果你先进行了输入操作,然后立刻执行输出操作,可能会导致未定义行为。这种情况下,也需要调用文件定位函数
控制文件 I/O 的内核缓冲
- 同步 I/O 数据完整性:强调读取输出的数据时一致的
- 同步 I/O 文件完整性:同步I/O数据完整性+更新文件的元数据(比如修改的时间戳等等)
系统调用
- 影响单个文件
- int fsync(int fd): 对应同步 I/O 文件完整性
- int fdatasync(int fd): 对应同步 I/O 数据完整性
- 影响所有文件
- void sync(void): 同步系统中所有的文件数据和元数据
open函数标志的扩展
- O_SYNC:按照同步 I/O 文件完整性的要求执行写操作
- O_DSYNC:按照同步 I/O 数据完整性的要求执行写操作
- O_RSYNC: 与 O_SYNC 标志或 O_DSYNC 标志配合一起使用的,先执行对应的写操作在读
- O_DIRECT: 直接 I/O,绕过缓冲区高速缓存
混合使用库函数和系统调用进行文件 I/O
以上两个函数适用于文件流和文件描述符之间的转换的,需要注意的是mode参数要与文件描述符 fd 的访问模式一致
I/O 系统调用会直接将数据传递到内核缓冲区高速缓存,而 stdio 库函数会等
到用户空间的流缓冲区填满,再调用 write()将其传递到内核缓冲区高速缓存,通常情况下,printf()函数的输出往往在 write()函数的输出之后出现(特殊情况:fflush或者换行符刷新)
再探
虚线:左侧所示为可于任何时刻显式强制刷新各类缓冲区的调用。图右侧所示为促使刷新自动化的调用:一是通过禁用 stdio 库的缓冲,二是在文件输出类的系统调用中用同步,从而使每个 write()调用立刻刷新到磁盘
补充
// fp表示一个文件流
fflush(fp);
fsync(fileno(fp));
该组合用于确保文件的数据被安全地写入磁盘,具体效果如下:
-
fflush(fp);
- 作用:将缓冲区中的数据立即写入到文件中。这意味着,任何在 fp 指向的文件流中尚未写入磁盘的数据都会被写入
- 场景:常用于确保用户看到的数据与实际文件内容一致,尤其在输出后需要马上看到结果时
-
fsync(fileno(fp));
- 作用:将与 fp 文件流对应的文件描述符的所有数据和元数据同步到磁盘。这是一个更底层的操作,确保数据在物理磁盘上持久化
- 场景:用于需要极高数据安全性的场合,如数据库系统,确保数据不会因系统崩溃而丢失
-
整体效果
将这两个函数结合使用,可以保证:
数据首先从用户空间的缓冲区写入到操作系统的缓冲区
然后,再将操作系统的缓冲区中的数据同步到物理磁盘这可以大幅降低数据丢失的风险,特别是在进行重要的文件写入操作后