本页目录

缓冲 I/O (stdio)

覆盖: setvbuf 三种策略 → fread/fwrite 缓冲机制 → 用户态缓冲 vs 内核 page cache → 行缓冲与终端 → 自定义缓冲 适用: glibc stdio, musl

概述

fread/fwrite 不会直接调 read/write 系统调用——它们在用户态维护缓冲区。这就是为什么 fwrite(buf, 1, 1, fp) 不会产生系统调用(直到 fflush 或 buffer 满了)。理解这一层是排查"为什么 printf 输出没出现"、"为什么 fwrite 比 write 快"的基础。

setvbuf: 三种缓冲模式

#include <stdio.h>

// _IONBF: 无缓冲 → 每次 fwrite 直接 syscall
setvbuf(fp, NULL, _IONBF, 0);

// _IOLBF: 行缓冲 → 遇到 '\n' 或 buffer 满 → flush
setvbuf(fp, NULL, _IOLBF, BUFSIZ);  // BUFSIZ = 8192 (glibc)

// _IOFBF: 全缓冲 → buffer 满才 flush
setvbuf(fp, NULL, _IOFBF, BUFSIZ);

// 默认策略:
//   stderr:      无缓冲 (_IONBF)
//   tty (stdout): 行缓冲 (_IOLBF)
//   普通文件:     全缓冲 (_IOFBF), 大小 = BUFSIZ

FILE 内部结构 (glibc 简化)

// glibc: libio/fileops.c, libio/genops.c
struct _IO_FILE {
    int     _flags;         // _IO_UNBUFFERED, _IO_LINE_BUF, ...
    char    *_IO_read_ptr;  // 当前读指针
    char    *_IO_read_end;  // 读缓冲区末尾 (buffer + bytes_read)
    char    *_IO_write_ptr; // 当前写指针
    char    *_IO_write_end; // 写缓冲区末尾 (buffer + BUFSIZ)
    char    *_IO_buf_base;  // 缓冲区起始
    char    *_IO_buf_end;   // 缓冲区末尾

    int     _fileno;        // 对应的文件描述符
    // ... 锁, wide char, 回退, ...
};

// fwrite(buf, 1, n, fp):
//   1. 如果 n <= (fp->_IO_write_end - fp->_IO_write_ptr):
//      → memcpy 到 buffer → _IO_write_ptr += n → return n
//      (极快! 纯用户态, 无 syscall)
//   2. 否则: 先 flush (write(fd, buffer, ...)) → 再 memcpy
//   3. 如果是行缓冲且 buf 中有 '\n': flush 到 '\n' 位置

用户态缓冲 vs 内核 Page Cache

fwrite 写 1 byte × 1000 次:
  全缓冲模式:    1 次 write syscall (flush buffer) → ~1 次内核调用
  无缓冲模式:     1000 次 write syscall → ~1000 次内核调用
  性能差:         ~100x

内核 write 写 1 byte × 1000 次到文件:
  page cache 会合并 (如果同一页) → 1 次磁盘 IO
  但每次 write() 有 syscall 开销 (~200ns each)

典型组合:
  fwrite: 用户态批量 (减少 syscall 数)
  page cache: 内核批量 (减少磁盘 IO 数)

行缓冲的陷阱

// 终端输出 (stdout 默认行缓冲):
printf("hello");        // 入 buffer,无输出 (无 '\n')
fprintf(stderr, "!");   // stderr 无缓冲 → 立即输出 "!"
printf(" world\n");     // 入 buffer + '\n' → flush → 输出 "hello world"

// 输出: "!hello world" (不是预期的顺序!)

// 修复:
printf("hello");
fflush(stdout);         // 强制 flush
fprintf(stderr, "!");
printf(" world\n");

// 或: 刚开始就关 stdout 缓冲:
setvbuf(stdout, NULL, _IONBF, 0);

fork 与缓冲

// 经典坑:
printf("before fork\n");  // buffer 中有数据但没 '\n'? → 但已有 '\n' → flush
fork();
// OK: fork 前被 flush 了

printf("before fork");    // buffer 中有数据, 无 '\n'
fork();
// 子进程复制了整个 FILE 结构 (含 buffer!)
// → 父子进程都会在后续 flush 时输出 "before fork"
// → 输出两次!

// 规则: fork 前始终 fflush(NULL) 或用行缓冲确保已 flush

自定义缓冲策略

// 提供自己的缓冲区 (非 glibc 默认的 BUFSIZ allocation):
char mybuf[4096];
setvbuf(fp, mybuf, _IOFBF, sizeof(mybuf));

// 注意: mybuf 的生命期必须 ≥ fp 的使用期
// (glibc 不复制 buffer, 只存指针)

参考

  • man: setvbuf(3), fflush(3), fwrite(3)
  • glibc 源码⁠: libio/fileops.c, libio/genops.c
  • LWN: "The GNU C Library's stdio implementation"

关键词: setvbuf, _IONBF, _IOLBF, _IOFBF, fflush, BUFSIZ, FILE, fork buffer, 行缓冲