
2026/10/28 18:38
読み書きの非対称性はあなたが(おそらく)想像しているほど大きい
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
POSIX システムにおける重要な教訓は、成功した
write(2) システムコールがデータを永続ストレージに到達したことを保証しないことであり、電力喪失やクラッシュ時にアプリケーションが破損するリスクを残すことです。このリスクは、複数のバッファリングレイヤーが存在することから生じます:ユーザープログラムは通常、バッファ付き I/O(例えば C stdio の FILE*、Go の bufio、Java の BufferedReader)を使用し、システムコールを呼び出す前に書き込みをバッファリングします。その上で、カーネルはバイト単位で揃った要求をブロック単位で揃った操作(ブロックは歴史的には 512 バイト、現代的なストレージでは後期にしばしば 4k)に変換し、ブロック単位でないデータをキャッシュします。write システムコールは、データがユーザー空間のバッファにコピーされたかカーネルキャッシュに引き渡された時点で即時リターンしますが、物理的なコミット後にではありません。対照的に、read(2) はリターンする前にストレージからバッファを充填するため、エラーは呼び出しが完了する前に発生します。したがって、成功した write() の後でも、データは揮発性キャッシュ(ユーザー空間と/またはカーネル)のみ、あるいは RAID コントローラに存在するだけであり、システムクラッシュやデバイスエラーが発生した場合の失うリスクがあります。NFS や SMB/CIFS などのネットワークファイルシステムでは、書き込みエラーは即時報告されず、返りコードが無視された場合はクローズ時にのみ報告されることがあります。データの耐久性を確保するためには、開発者はエラーコードを確認し、ファイル記述子をクローズする前に明示的に fsync()(Windows 上では FlushFileBuffers)を呼び出す必要があります。ディレクトリメタデータの変更がある場合、その更新の可視性を保証するには、別途ディレクトリのファイル記述子に対する fsync() を呼び出す必要があることもあります。したがって、デフォルトのバッファ付き I/O に依存するだけで、ハイリスクなプロダクション環境において重大な破損または重要な情報の完全な損失につながることがあります。本文
コンピューティングにおける入出力(I/O)操作の非対称性とリスク
コンピューティングにおける 入出力(I/O)操作 の実装と公開について解説します。本記事では、特に読み込みと書き込みにおける振る舞いの違い(非対称性)、その原因であるバッファリング機構、そして開発者が見落としがちな重要な注意点に触れます。
1. システムコールとバッファ付き I/O の基本
POSIX システムコール
read(2) と write(2) は、入出力処理の基本単位となります。
システムコールの定義
以下の C 関数型定義は、生(raw)のシステムコールを表します。ユーザ空間プログラムが直接使用可能ですが、実際には高層階級のレイヤーを経由するのが一般的です。
ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count);
C スタンダードライブラリの役割
プログラムは通常、
FILE* タイプおよび fread / fwrite 関数を通じて I/O を実行します。これらは内部で count の値を分割処理しますが、本質的にはシステムコールと同様の動作を行います。
基本概念の対称性と非対称性
- 概念的な対称性:
系:ディスク上のファイルを参照し、バイトをメモリバッファへコピー。read
系:メモリバッファ内のデータを格納先(ストレージ)へ書き込み。write
- 実装的な非対称性:
- エラー発生時や内部機構(バッファリング)により、両者の挙動に重要な違いが生じます。
2. バッファ付き I/O の仕組み
プログラム内のファイル I/O はストレージデバイスへ直接到達せず、複数の バッファレイヤー を通過して伝達されます。これらは技術的な動機に基づき有効になっています。
カーネル空間側のバッファキャッシュ
- デバイスは「ブロックデバイス」として動作し、固定サイズ(例:4 KB)のブロック単位でデータを格納・処理します。
- ランダムな I/O リクエストの変換:
- バイト境界に揃っていないリクエスト(例:オフセット 1234567 バイトから 100 バイト読み取り)も、ブロック境界に揃った操作に変換して実行されます。
- 書き込み時、既存ブロックの内容を読み取る必要がある場合も同様にバッファキャッシュを経由します。
- 役割: デバイスの理解するブロック境界への要求変換とデータの一時的な保存(キャッシュ)。
ユーザ空間側のバッファリング
システムコールの呼び出しにはオーバーヘッド(数 hundred サイクル)が発生するため、一度に処理するデータ量を増やす必要があります。
| バッファサイズ | 例(C スタンダードライブラリ・言語別) |
|---|---|
| 4096 バイト | Visual C++ 2013 の 、Go (/) |
| 8192 バイト | GNU libc の 、Java (/) |
ハードディスクや RAID コントローラーにも独自のキャッシュが搭載されており、実装はさらに複雑になりますが、基本的な構造はこの 2 つのレイヤーで理解できます。
3. I/O 操作発行時の挙動と非対称性
読み込みと書き込みにおける「世界の状態」の変化に違いがあります。
read
系コール(読み込み)
read- バッファの充填: アプリケーションが要求したデータ量(例:100 バイト)よりも多いデータ量(例:4 KB)を一度に読み取ろうとします。
- データの確保: カーネルはファイルシステムに問いかけ、必要ならストレージから読み込み、バッファキャッシュへコピーします。
- 完了: データがメモリバッファに到達し、アプリケーションの指定した先頭にコピーされます。
エラー処理の特徴
- ファイルサイズ不足、読み取りエラー、接続切断などが発生した場合、システムコールを呼び出す直後にエラーを検知し、適切なエラーコードを返します。
- 結論: 読み込み失敗は即座に報告されるため、アプリケーション側で安全にエラーハンドリングが可能です。
write
系コール(書き込み)
write- 仮保存: アプリケーションのデータを受け取り、バッファへコピーしますが、直ちにシステムコールを介してストレージへ書き込むわけではありません。
- 即時的成功: ユーザ空間関数はすぐに「成功」として戻ってきます。
- 後置実行: 実際の書き込みは以下のいずれかのタイミングで実行されます。
- バッファが満杯になった時
、fflush()
,fseek()
を呼び出した時fclose()
エラー処理のリスク
- 「成功」を報告されたからといって書き込みに成功したとは限りません。
- 多くのプログラマは
の戻り値を確認しますが、fwrite
やfflush
のエラーチェックを行う人は少ないです。fclose - 結論: バッファ付き書き込みの本質は「実行の遅延」であり、エラー報告も同様に遅延されています。
4. その他のバッファリングとリスク
カーネル空間のバッファキャッシュ(write
システムコール)
writewrite システムコールが成功したとしても、データはまだ物理的なストレージデバイスには到達していません。
- リスク: デバイスエラーやシステムクラッシュにより、この段階で停電などが起こるとデータは失われます。
- 遅延書き込みの問題:
- ローカルファイルシステムでも、空き容量の確保処理が後から実行(delayed write-back)される場合があります。
- ネットワークファイルシステム(NFS, SMB/CIFS)では、
までの間にエラーが発生している場合でも通知されないことがあります。close()
- 対策:
の戻り値だけでなく、データ永続性を確保するにはclose()
を必要です。fsync
データの真正性とメタデータ
- バッファキャッシュに到達した=ストレージへ書き込まれたという保証はありません。
- 永続性の確保手順:
- 書き込みシステムコールを実行し、エラーを確認。
の戻り値をチェックする(Windows ではclose()
など)。FlushFileBuffers- 直前に
を呼び出し、エラーを確認する。fsync(2)
ディレクトリ記述子への注意
fsync() はそのファイルを含むディレクトリのメタデータまでディスクに到達することを保証しません。
- 正しい実装: ディレクトリに対応するファイル記述子上でも明示的に
を行う必要があります(多くのライブラリはこの点の保証を行っていません)。fsync()
脚注 [3]:
は C ライブラリの関数ではなく、正当なシステムコールです。fsync
5. 結論と概要
I/O 操作におけるリスクを要約します。
| 特性 | 読み込み () | 書き込み () |
|---|---|---|
| 非対称性 | なし(ほぼ透明) | 非常に高い(複雑) |
| 失敗時の挙動 | データを見る前にエラーが検知され、即座に報告される。 | 書き込みの成功を報告しても、実際には未実行またはエラー中の可能性がある。 |
| 開発者の課題 | エラーハンドリングが比較的容易。 | 実行の遅延とエラーの遅延を理解していないと深刻なデータ喪失を招く。 |
| OS の役割 | 抽象化により単純化する。 | 抽象化機構( など)自体が複雑で、正しく使う必要があります。 |
開発者への要約
- 書き込みは実際の実行とエラー報告が遅延していることを常に意識してください。
- データの永続性を保証するためには、以下の手順を踏む必要があります:
- 書き込み完了直前に
を実行する。fsync() - 関連するシステムコール(
,close
など)の 戻り値をチェックする。fflush
- 書き込み完了直前に
- 「ドラゴン」と呼ばれるこれらの難しい問題は、OS の抽象化が必ずしも状況を楽にしてくれないことに起因します。
6. 脚注
- [1] バッファモード: ここでの議論は「フルバッファモード」に関するものです(行バッファモードとは異なります)。
- [2] 遅延割り当てのリスク: 現代のファイルシステムではブロック割り当てを遅らせることは一般的ですが、空き容量確保処理も同様に後置されます。ユーザーが重要ドキュメントを保存している最中に OS が再起動したり、容量不足に気づいたのが数分後だったりすると、アプリケーションからはエラーが発生しないままクラッシュやデータ喪失が進む可能性があります。
- [3] fsync の性質: 名前の響きから C ライブラリの一部のように思えますが、実際にはカーネル空間で動作するシステムコールです。