在标准的 C 库中,对 IO 有一定的缓存机制。理解这些机制或许能在分析某些问题时提供参考。
缓存分类
-
块缓存
一般用于访问真正的磁盘文件。C库会为文件访问申请一块内存,只有当文件内容将缓存块填满或执行冲刷函数flush时,C库才会将缓存内容写入内核中。
-
行缓存
一般用于访问终端。当遇到一个换行符时,就会引发真正的I/O操作。需要注意的是,C库的行缓存也是固定大小的。因此,当缓存已满,即使没有换行符时也会引发I/O操作。
-
无缓存
C库没有进行任何的缓存。任何C库的I/O调用都会引发实际的I/O操作。
标准输入输出的默认缓存机制
stdio.h 中声明了 stdin 、stdout 和 stderr 的全局变量以及对应的宏:
typedef struct _IO_FILE FILE;
/* Standard streams. */
extern struct _IO_FILE *stdin; /* Standard input stream. */
extern struct _IO_FILE *stdout; /* Standard output stream. */
extern struct _IO_FILE *stderr; /* Standard error output stream. */
#ifdef __STDC__
/* C89/C99 say they're macros. Make them happy. */
#define stdin stdin
#define stdout stdout
#define stderr stderr
#endif
可以看出 stdin stdout stderr 其实就是文件指针, 它们的定义代码如下:
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;
DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);
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库提供了接口,用于修改默认的缓存行为:
void setbuf(FILE *restrict stream, char *restrict buf);
void setbuffer(FILE *stream, char *buf, int size);
int setlinebuf(FILE *stream);
int setvbuf(FILE *restrict stream, char *restrict buf, int type, size_t size);
前3个接口内部都调用了 setvbuf
接口, 所以主要看 setvbuf
接口就行。
- 当
size
参数设为 0 时,代表使用默认的最优大小缓冲区分配 - 当
size
不为 0 时,除了未缓冲的文件,buf
参数应该指向一个至少有size
大小的缓冲区; 如果buf
不为空,则调用方必须在流关闭后自己释放该缓冲区 - 当
size
不为 0,但buf
为NULL
时,则库会自动分配给定大小的缓冲区,并且自动在流关闭时释放
例子
//c_lib_io_cache.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
printf("Hello ");
if(0 == fork()) {
printf("child\n");
return 0;
}
printf("parent\n");
return 0;
}
上述代码执行后:
▶ 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)
强制立即输出到 stdout//... printf("Hello "); fflush(stdout); //...
▶ gcc c_lib_io_cache.c -o main.o && ./main.o Hello parent child
-
修改 stdout 的缓存大小
在
printf("Hello ")
前调用setbuffer
将 stdout 的缓存大小设为 1 个字节//... setbuffer(stdout, NULL, 1); printf("Hello "); //...
▶ gcc c_lib_io_cache.c -o main.o && ./main.o Hello parent child
虽然第2种方式也实现了目的,但是将行缓存大小设为1个字节,终究不是最优方法,没有利用到缓存机制。所以最佳应该方法应该是第一种利用 fflush
强制 IO 同步。