Python でファイルに write() を呼んでも、データがすぐにディスクに書き込まれるわけではない。実際には 3 つの層でバッファリングが行われている。
3 層のバッファ構造
ファイルに書き込んだデータは、以下の順序で移動する。
Python のバッファ(io モジュール)
C ライブラリのバッファ(stdio)
カーネルのページキャッシュ
ディスク(最終目的地)
それぞれの層がバッファリングを行うため、write() の呼び出しがディスク書き込みを意味するわけではない。
第 1 層:Python のバッファ
Python の open() が返すファイルオブジェクトは、内部にバッファを持っている。
# バッファリングモードを指定できる
f = open("data.txt", "w", buffering=1) # 行バッファリング
f = open("data.txt", "w", buffering=8192) # 8KB バッファ
f = open("data.txt", "w", buffering=0) # バッファなし(バイナリのみ)
行バッファリング(対話的)または固定サイズバッファ(非対話的)。
io.DEFAULT_BUFFER_SIZE(通常 8192 バイト)のバッファ。
バッファなし。バイナリモードでのみ使用可能。
import io
print(io.DEFAULT_BUFFER_SIZE) # 8192
第 2 層:C ライブラリのバッファ
CPython は内部で C の標準ライブラリ(stdio)を使用しており、FILE* のバッファが存在する。ただし、Python 3 の io モジュールは多くの場合このレイヤーをバイパスする。
C の stdio を直接使用。setvbuf() でバッファリングを制御
io モジュールが独自にバッファリング。C レベルのバッファはほぼ使われない
第 3 層:カーネルのページキャッシュ
Python の flush() を呼んでも、データはカーネルのページキャッシュに留まる。ディスクへの実際の書き込みはカーネルが非同期で行う。
f = open("data.txt", "w")
f.write("hello")
# flush() は Python バッファ → カーネルバッファへの転送
f.flush()
# この時点ではまだディスクに書き込まれていない可能性がある
# fsync() でカーネルバッファ → ディスクへの書き込みを強制
import os
os.fsync(f.fileno())
# これでディスクに到達したことが保証される
f.close()
各関数の役割
| 関数 | 動作 | データの到達先 |
|---|---|---|
| write() | Python バッファに書く | Python バッファ |
| flush() | カーネルに渡す | カーネルバッファ |
| fsync() | ディスクに書く | ディスク |
実験:バッファリングの確認
バッファリングの動作を確認してみよう。
import os
import time
# バッファありで書き込み
f = open("test.txt", "w")
f.write("hello")
print(f"書き込み直後: {os.path.getsize('test.txt')} bytes") # 0 bytes
f.flush()
print(f"flush後: {os.path.getsize('test.txt')} bytes") # 5 bytes
f.close()
write() 直後はファイルサイズがゼロ。flush() して初めてカーネルに渡され、ファイルシステムに反映される。
バッファなしモード
リアルタイム性が必要な場合はバッファなしモードを使う。
# バイナリモードでのみ buffering=0 が使える
f = open("realtime.bin", "wb", buffering=0)
f.write(b"data") # 即座にカーネルバッファへ
f.close()
# テキストモードでバッファなしはエラー
try:
f = open("test.txt", "w", buffering=0)
except ValueError as e:
print(e) # can't have unbuffered text I/O
テキストモードでリアルタイム書き込みが必要なら、毎回 flush() を呼ぶか、行バッファリングを使う。
# 行バッファリング(改行で自動 flush)
f = open("log.txt", "w", buffering=1)
f.write("line 1\n") # 改行で自動的に flush される
読み取り時のバッファリング
読み取りにもバッファリングが効く。小さな read() を繰り返すと、実際にはまとめて読み込まれる。
# 1 バイトずつ読んでも、内部では 8KB 単位で読み込まれる
with open("large.txt", "rb") as f:
while True:
byte = f.read(1) # 見かけ上 1 バイト読み取り
if not byte:
break
# 実際は最初の read(1) で 8KB 読み込まれ、
# 以降はバッファから返される
mmap によるバッファリング回避
メモリマップドファイルを使うと、Python と C のバッファリング層をスキップしてカーネルのページキャッシュに直接アクセスできる。
import mmap
import os
with open("data.bin", "r+b") as f:
# ファイルをメモリにマップ
mm = mmap.mmap(f.fileno(), 0)
# 配列のようにアクセス
mm[0:5] = b"hello"
# 変更をディスクに同期
mm.flush()
mm.close()
まとめ
システムコールの回数を減らしてパフォーマンスを向上させる。1 バイトずつ write() しても、まとめてディスクに書かれる。
flush() だけでは不十分。fsync() を呼んで初めてディスク書き込みが保証される。
buffering=0(バイナリ)、buffering=1(テキスト、行単位)、または毎回 flush() を呼ぶ。