stdio.h的缓冲机制解析

1. 令人迷惑的printf()

在C语言中,由于stdio.h中的缓冲机制,printf的输出通常会受到缓冲区的影响。

这种影响可能非常微妙,并常常令人疑惑,比如我们来看下面这段代码

c 复制代码
#include <stdio.h>
int main(void) {
  printf("Hello World");
  while(1);
}

在命令行中编译运行,发现他只是一味循环,输出不见了?!!

但是如果我们修改一下代码,添加一个换行符:

c 复制代码
printf("Hello World\n");

就可以看到Hello World被输出了?!

2. stdio的缓冲机制解析

根据标准I/O的缓冲方式,printf的输出主要有以下几种情况:

2.1. 行缓冲(Line Buffering)

  • 默认情况下,面向终端(标准输出/stdout是终端)的文件流 使用行缓冲
  • 缓冲区在以下情况下刷新
    1. 输出了一个换行符 \n
    2. 缓冲区被填满
    3. 主动调用刷新函数(如 fflush(stdout))。
    4. 程序正常结束,流被关闭(如 exit()return 导致流关闭)。

示例

c 复制代码
printf("Hello, ");    // 不会立即输出,因为没有换行
printf("World\n");    // 输出 "Hello, World",因为遇到换行符

2.2. 全缓冲(Full Buffering)

  • 默认情况下,面向文件的文件流(如写入文件的FILE* 使用全缓冲
  • 缓冲区在以下情况下刷新
    1. 缓冲区被填满
    2. 主动调用刷新函数(如 fflush(file_stream))。
    3. 程序正常结束,流被关闭(如 fclose()exit())。

示例

c 复制代码
FILE *fp = fopen("output.txt", "w");
fprintf(fp, "Buffered output");  // 不会立即写入文件
fflush(fp);                      // 主动刷新缓冲区,写入文件
fclose(fp);                      // 关闭文件时自动刷新缓冲区

2.3. 无缓冲(Unbuffered)

  • 默认情况下,标准错误流stderr无缓冲的(因为需要及时显示错误信息)。
  • 缓冲区在每次调用I/O操作时都会刷新,数据直接输出到目标设备。
  • 如果通过 setvbufsetbuf 将流设置为无缓冲,则每次调用printf都会立即输出。

示例

c 复制代码
fprintf(stderr, "This is an error message\n"); // 立即输出,不受缓冲机制影响

设置无缓冲流

c 复制代码
setvbuf(stdout, NULL, _IONBF, 0); // 将 stdout 设置为无缓冲
printf("Immediate output");      // 每次调用都会直接输出

2.4. 缓冲区溢出或关闭时刷新

  • 如果缓冲区被填满,stdio会自动刷新。
  • 当程序结束或流关闭时(如 fclose()),缓冲区中的内容会被自动刷新。

stdio缓冲机制总结

缓冲模式 使用场景 刷新条件
行缓冲 stdout面向终端 换行符、缓冲区满、调用fflush、流关闭或程序退出
全缓冲 stdout面向文件或其他设备 缓冲区满、调用fflush、流关闭或程序退出
无缓冲 stderr或主动设置无缓冲流 每次调用printffprintf直接输出

缓冲模式可以通过 setvbufsetbuf 自定义,这在调试或控制输出行为时非常有用。

3. 并发场景下的stdio缓冲

在并发场景下,stdio的缓冲机制可能会更令人迷惑一点,不过机制是相通的。

bash 复制代码
$ cat fork_printf.c
#include <stdio.h>
#include <unistd.h>

int main(void) {
  for (int i = 0; i < 2; i++) {
    fork();
    printf("Hello\n");
  }
  return 0;
}
$ gcc fork_printf.c
$ ./a.out
Hello
Hello
Hello
Hello
Hello
Hello
$ ./a.out | cat
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
$ # ??? 为什么两次输出内容不一样?是魔法么??!

这就是因为./a.out面向的输出的使用的缓冲方式不同:

  • 面向标准输出stdout时,使用行缓冲机制,Hello\n不存放在stdio的缓冲区(内存中),而是直接输出了
  • 面向管道输出时, 则使用全缓冲机制,因此第一个Hello\n会存放在缓冲区中,并随着fork一并复制,并再最后程序退出时输出。

也许你觉得我在胡说八道,但是根据计算机中没有魔法的观点,我们一定是有办法验证我们的猜想的。

没错,我们可以使用strace来看到程序的write系统调用,从而验证上述观点。

下一篇将以此为例介绍linux神器之strace的应用场景与使用方式。