本页目录
缓冲 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: 三种缓冲模式
// _IONBF: 无缓冲 → 每次 fwrite 直接 syscall
;
// _IOLBF: 行缓冲 → 遇到 '\n' 或 buffer 满 → flush
; // BUFSIZ = 8192 (glibc)
// _IOFBF: 全缓冲 → buffer 满才 flush
;
// 默认策略:
// stderr: 无缓冲 (_IONBF)
// tty (stdout): 行缓冲 (_IOLBF)
// 普通文件: 全缓冲 (_IOFBF), 大小 = BUFSIZ
FILE 内部结构 (glibc 简化)
// glibc: libio/fileops.c, libio/genops.c
;
// 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 默认行缓冲):
; // 入 buffer,无输出 (无 '\n')
; // stderr 无缓冲 → 立即输出 "!"
; // 入 buffer + '\n' → flush → 输出 "hello world"
// 输出: "!hello world" (不是预期的顺序!)
// 修复:
;
; // 强制 flush
;
;
// 或: 刚开始就关 stdout 缓冲:
;
fork 与缓冲
// 经典坑:
; // buffer 中有数据但没 '\n'? → 但已有 '\n' → flush
;
// OK: fork 前被 flush 了
; // buffer 中有数据, 无 '\n'
;
// 子进程复制了整个 FILE 结构 (含 buffer!)
// → 父子进程都会在后续 flush 时输出 "before fork"
// → 输出两次!
// 规则: fork 前始终 fflush(NULL) 或用行缓冲确保已 flush
自定义缓冲策略
// 提供自己的缓冲区 (非 glibc 默认的 BUFSIZ allocation):
char 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, 行缓冲