缓存分类
-
块缓存
一般用于访问真正的磁盘文件。C库会为文件访问申请一块内存,只有当文件内容将缓存块填满或执行冲刷函数flush时,C库才会将缓存内容写入内核中。
-
行缓存
一般用于访问终端。当遇到一个换行符时,就会引发真正的I/O操作。需要注意的是,C库的行缓存也是固定大小的。因此,当缓存已满,即使没有换行符时也会引发I/O操作。
-
无缓存
C库没有进行任何的缓存。任何C库的I/O调用都会引发实际的I/O操作。
标准输入输出的默认缓存机制
stdio.h 中声明了 stdin 、stdout 和 stderr 的全局变量以及对应的宏:
|
|
可以看出 stdin stdout stderr 其实就是文件指针, 它们的定义代码如下:
|
|
|
|
DEF_STDFILE
是一个宏定义,用于初始化 C 库中的 FILE 结构。从源码上就可以看到3个标准输入输出的差异:
- stdin: 文件描述符为
0
, 不可写(_IO_NO_WRITES) - stdout: 文件描述符为
1
, 不可读(_IO_NO_READS) - stderr: 文件描述符为
2
, 不可读(_IO_NO_READS)
通常,所有文件都是块缓冲的。当文件上发生第一个I/O操作时,将调用
malloc
并获得一个最优大小的缓冲区。
如果一个流指向一个终端(比如通常的 stdout),那么它就是行缓冲的。标准错误流 stderr 总是未缓冲的。
从源码中的定义也能看出, stderr 在定义时还追加了 IO_UNBUFFERED,表示无缓冲。
C库接口
C库提供了接口,用于修改默认的缓存行为:
|
|
前3个接口内部都调用了 setvbuf
接口, 所以主要看 setvbuf
接口就行。
- 当
size
参数设为 0 时,代表使用默认的最优大小缓冲区分配 - 当
size
不为 0 时,除了未缓冲的文件,buf
参数应该指向一个至少有size
大小的缓冲区; 如果buf
不为空,则调用方必须在流关闭后自己释放该缓冲区 - 当
size
不为 0,但buf
为NULL
时,则库会自动分配给定大小的缓冲区,并且自动在流关闭时释放
例子
|
|
上述代码执行后:
▶ gcc c_lib_io_cache.c -o main.o && ./main.o
Hello parent
Hello child
因为 stdout 在终端默认为行缓存,所以最开始执行 printf("Hello ")
时并没有触发真正的输出, Hello
只被写到了 父进程的 stdout 的内存缓存中,当父进程通过 fork 创建子进程之后, 子进程的内存空间也拥有和父进程一样的内容,所以子进程调用 printf("child\n")
时,连带自己 stdout 缓存空间中的 Hello
一起输出了。
下面通过两种方式去避免上述问题的出现:
-
强制立即输出到 stdout
在父进程
printf("Hello ")
后调用fflush(stdout)
强制立即输出到 stdout1 2 3 4
//... printf("Hello "); fflush(stdout); //...
▶ gcc c_lib_io_cache.c -o main.o && ./main.o Hello parent child
-
修改 stdout 的缓存大小
在
printf("Hello ")
前调用setbuffer
将 stdout 的缓存大小设为 1 个字节1 2 3 4
//... setbuffer(stdout, NULL, 1); printf("Hello "); //...
▶ gcc c_lib_io_cache.c -o main.o && ./main.o Hello parent child
虽然第2种方式也实现了目的,但是将行缓存大小设为1个字节,终究不是最优方法,没有利用到缓存机制。所以最佳应该方法应该是第一种利用 fflush
强制 IO 同步。