このページの目次
バッファリング I/O (stdio)
カバー範囲: setvbuf の3つのポリシー → fread/fwrite のバッファリング機構 → ユーザー空間バッファ vs カーネルページキャッシュ → 行バッファリングと端末 → カスタムバッファ 対象: glibc stdio, musl
概要
fread/fwrite は直接 read/write システムコールを呼び出しません。ユーザー空間でバッファを管理しています。そのため、fwrite(buf, 1, 1, fp) を実行しても(fflush が呼ばれるか、バッファがいっぱいになるまで)システムコールは発生しません。この仕組みを理解することは、「なぜ printf の出力が表示されないのか」「なぜ fwrite が write よりも高速なのか」といった問題のトラブルシューティングの基礎となります。
setvbuf: 3つのバッファリングモード
// _IONBF: バッファなし → 各 fwrite で直接 syscall
;
// _IOLBF: 行バッファ → '\n' またはバッファがいっぱいになると flush
; // BUFSIZ = 8192 (glibc)
// _IOFBF: 全バッファ → バッファがいっぱいになると 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 → _IO_write_ptr += n → n を返す
// (非常に高速! ユーザー空間のみで完結し、syscall は発生しない)
// 2. それ以外の場合: まず flush (write(fd, buffer, ...)) → その後 memcpy
// 3. 行バッファモードで buf に '\n' が含まれている場合: '\n' の位置まで flush
ユーザー空間バッファ vs カーネルページキャッシュ
fwrite で 1 バイト × 1000 回書き込み:
全バッファモード: 1 回の write syscall (バッファ flush) → 約 1 回のカーネル呼び出し
バッファなしモード: 1000 回の write syscall → 約 1000 回のカーネル呼び出し
パフォーマンス差: 約 100 倍
カーネル write で 1 バイト × 1000 回ファイルに書き込み:
ページキャッシュがマージされる (同じページ内であれば) → 1 回のディスク IO
ただし、各 write() には syscall のオーバーヘッドがかかる (それぞれ約 200ns)
典型的な組み合わせ:
fwrite: ユーザー空間でのバッチ処理 (syscall 回数の削減)
page cache: カーネル空間でのバッチ処理 (ディスク IO 回数の削減)
行バッファリングの落とし穴
// 端末出力 (stdout はデフォルトで行バッファ):
; // バッファに入るが、出力されない ('\n' がないため)
; // stderr はバッファなし → "!" が即座に出力
; // バッファに入る + '\n' → flush → "hello world" が出力
// 出力結果: "!hello world" (意図した順序ではない!)
// 修正方法:
;
; // 強制的に flush
;
;
// または: 最初から stdout のバッファリングを無効にする:
;
fork とバッファ
// 古典的な罠:
; // バッファにデータがあるが '\n' もある? → '\n' があるため flush される
;
// OK: fork の前に flush されている
; // バッファにデータがあるが '\n' がない
;
// 子プロセスは FILE 構造全体 (バッファ含む) をコピーする
// → 親プロセスと子プロセスの両方が、後の flush 時に "before fork" を出力する
// → 2 回出力される!
// ルール: fork の前に常に fflush(NULL) を呼び出すか、行バッファを使用して確実に flush する
カスタムバッファリングポリシー
// 独自のバッファを提供する (glibc デフォルトの BUFSIZ 割り当てではない):
char mybuf;
;
// 注意: mybuf の生存期間は fp の使用期間以上でなければならない
// (glibc はバッファをコピーせず、ポインタのみを保持する)
参考
- 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, 行バッファリング