Dev Study
OSと低レイヤのしくみ コース

19. write()が返ってもディスクには無い — fsyncと耐久性

write()が成功して返った瞬間、データがどこまで進んでいるのかを図で追っていきましょう。

① write()が実際にやっていること

write(fd, buf, n) 呼び出し時のデータの移動
ユーザ空間 bufアプリのメモリ上のデータ
write(2) システムコール
ページキャッシュのページ内容を上書きし dirty の印を立てる
カーネル空間のメモリ
ここで即座に return n
write() が成功を返す戻り値 = 書けたバイト数
この矢印はまだ発生していない
ブロックデバイスSSD / HDD
  • ・write(2) の NOTES には「A successful return from write() does not make any guarantee that data has been committed to disk.」と明記されています
  • ・同じ NOTES は「On some filesystems, including NFS, it does not even guarantee that space has successfully been reserved for the data.」とも続けています
  • ・つまり write() の戻り値はメモリへのコピーが終わったことしか意味しません

write()はページキャッシュ(カーネルがファイル内容を載せておくメモリ領域)上のページを書き換え、そのページに「dirty(書き換え済み・未反映)」の印を付けて返ります。デバイスへの転送はこの時点では一度も起きていません。

② だから電源断でデータは消える

write() 直後に電源断が起きた場合
アプリwrite() は成功したと認識
データの所在
ページキャッシュ上の dirty ページ揮発性メモリ(DRAM)
電源断
メモリ内容は消滅dirty ページごと失われる
デバイス上の内容書き込み前の古いまま
再起動後
ファイルは write() 前の状態成功したはずの書き込みが無い
  • ・write(2) は「The only way to be sure is to call fsync(2) after you are done writing all your data.」と述べています
  • ・通常のシャットダウンではカーネルが dirty ページを書き出すため消えません
  • ・カーネルパニックでもデータは失われますが、理由は異なります。メモリが消えるのではなく、writeback を実行するカーネル自身が動けなくなるためです

dirtyページはメモリ上にしか存在しません。デバイスへ届く前に電源が落ちれば、write()が成功を返していたとしてもその内容は失われます。

③ 誰が後から書き出すのか — writeback

flusher スレッドによる遅延書き出し
dirty ページが溜まる複数の write() の結果
flusher スレッドが起きる間隔
周期的な起床dirty_writeback_centisecs
100分の1秒単位
起床時に何を書き出すか
古くなった dirty を対象にするdirty_expire_centisecs 超過分
それ自体は起床の契機ではない
閾値を超えると前倒しで起動
背景 writeback の開始dirty_background_ratio 超過
書き手自身が writebackdirty_ratio 超過
デバイスへ転送されると
dirty の印が落ちる以後はクリーンなページ
  • ・カーネル文書 admin-guide/sysctl/vm は dirty_writeback_centisecs を「The kernel flusher threads will periodically wake up and write old data out to disk.」と説明しています
  • ・dirty_expire_centisecs は「Data which has been dirty in-memory for longer than this interval will be written out next time a flusher thread wakes up.」です。期限切れは書き出しの対象条件であって、起床そのものを引き起こすわけではありません
  • ・dirty_ratio は「the number of pages at which a process which is generating disk writes will itself start writing out dirty data」と定義されています
  • ・いずれもアプリが指定できるタイミングではありません

dirtyページはカーネルのflusherスレッドが後からまとめてデバイスへ書き出します。これをwritebackと呼びます。まとめて書くことでI/Oを効率化できる一方、書き出しの時刻はアプリから見えません。

④ fsync()はデバイスまで届けてから返る

fsync(fd) の同期的な書き出し
fsync(fd) 呼び出し呼び出し元はここでブロック
対象ファイルの dirty をすべて集める
データのページファイル内容
メタデータサイズ・タイムスタンプ等
デバイスへ転送 + ディスクキャッシュのフラッシュ
永続ストレージに到達クラッシュしても復元可能
到達を確認してから
fsync() が 0 を返す失敗時は EIO 等
  • ・fsync(2) は「transfers ('flushes') all modified in-core data of the file referred to by the file descriptor fd to the disk device ... so that all changed information can be retrieved even if the system crashes or is rebooted」と定義しています
  • ・「This includes writing through or flushing a disk cache if present.」— デバイス内蔵の書き込みキャッシュもフラッシュ対象です
  • ・ただし対象はそのファイルだけです。fsync(2) は「Calling fsync() does not necessarily ensure that the entry in the directory containing the file has also reached disk. For that an explicit fsync() on a file descriptor for the directory is also needed.」と注意しています。新規作成したファイルは親ディレクトリの fsync も必要です
  • ・戻り値は必ず確認してください。ENOSPC や EIO で失敗しうるためです

fsync()はそのファイルのdirtyページとメタデータをデバイスへ転送し、完了してから返ります。write()と違い、戻ってきた時点でそのファイルの内容は永続化されています。

⑤ fdatasync()は何を省くのか

fsync() と fdatasync() のメタデータ扱いの違い
判定基準そのメタデータは読み出しに必要か
必要 → 両者とも書き出す
ファイルサイズ追記でサイズが伸びた場合
知らないと読めない
不要 → fdatasync は省略できる
最終更新時刻 mtime読み出しには影響しない
最終状態変更時刻 ctime同上
結果
fsync: データ + 全メタデータ
fdatasync: データ + 必要なメタデータのみ
  • ・fsync(2) は fdatasync について「does not flush modified metadata unless that metadata is needed in order to allow a subsequent data retrieval to be correctly handled」と説明しています
  • ・同じ man ページは、mtime の変更だけならフラッシュ不要だがファイルサイズの変更は必要、という例を挙げています
  • ・既存領域の上書きのようにサイズが変わらない更新では、fdatasync のほうが有利になりえます
  • ・データ自体の耐久性はどちらも同じです。違うのはメタデータの扱いだけです

fdatasync()は、後続の読み出しに必要ないメタデータの書き出しを省きます。タイムスタンプだけが変わった場合、メタデータ用のI/Oを一回減らせます。

⑥ O_SYNC / O_DSYNC — write毎に同じ保証を与える

open(2) のフラグと write() の返るタイミング
open(path, O_WRONLY)フラグ無し
write はページキャッシュで返る
O_DSYNC を付けると
write ごとに data integrity completionデータ + 読み出しに必要なメタデータ
fdatasync 相当
O_SYNC を付けると
write ごとに file integrity completionデータ + 全メタデータ
fsync 相当
どちらも
write() の戻り = 永続化完了その分 write() は遅くなる
  • ・open(2) の Synchronized I/O 節は O_SYNC を「write operations will flush data and all associated metadata to the underlying hardware」と説明しています
  • ・同じ節で O_DSYNC は「will flush data to the underlying hardware, but will only flush metadata updates that are required to allow a subsequent read operation to complete successfully」とされています
  • ・open(2) は「Linux implements O_SYNC and O_DSYNC, but not O_RSYNC.」と述べ、glibc が O_RSYNC を O_SYNC と同じ値で定義していることを somewhat incorrectly と評しています

open()時にO_SYNC/O_DSYNCを付けると、個々のwrite()がデバイス到達まで待ってから返るようになります。fsync()を明示的に呼ばなくても同じ保証が得られます。

⑦ なぜカーネルは最初からデバイスへ書かないのか

遅延書き出しが得ているもの
設計上の問いwrite ごとにデバイスを待つべきか
待たないことで得られる利点
まとめ書き小さな write を1回のI/Oに集約
上書きの相殺同じページへの再書き込みはI/O不要
順序の最適化I/Oスケジューラが並べ替え可能
代償として発生するもの
クラッシュ時にデータを失う窓write 完了から writeback までの間
アプリ側の選択肢として用意された
速度優先: write() のみ
耐久性優先: fsync / fdatasync / O_SYNC
  • ・カーネルは一律にどちらかを選ばず、既定を高速側に置いた上で明示的な同期手段を提供しています
  • ・そのため「いつ fsync するか」はアプリの責務です。DBやエディタが保存時に fsync を呼ぶのはこのためです

毎回デバイスへ書くと、書き込みのたびにデバイスの応答を待つことになります。ページキャッシュを挟むことで、まとめ書き・上書きの相殺・順序の最適化ができます。耐久性を選ぶ権利をアプリ側に渡した設計です。

公式ドキュメントで詳しく ↗