---
title: 缓冲 I/O (stdio)
url: https://doc.liz6.com/systems-programming/05-io-and-file-systems/01-buffered-i-o-stdio
locale: zh
area: systems-programming
tags:
- systems-programming
- IO与文件系统
date: 2026-06-30
modified: 2026-06-30
description: '覆盖: setvbuf 三种策略 → fread/fwrite 缓冲机制 → 用户态缓冲 vs 内核 page cache → 行缓冲与终端 → 自定义缓冲 适用: glibc stdio, musl'
---

# 缓冲 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: 三种缓冲模式

```c
#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 简化)

```c
// 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 数)
```

## 行缓冲的陷阱

```c
// 终端输出 (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 与缓冲

```c
// 经典坑:
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
```

## 自定义缓冲策略

```c
// 提供自己的缓冲区 (非 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, 行缓冲*
