ファイルから同じ0が読めても、穴と確保済み領域は違う

緑の浅い器には中央の穴があり、隣の桃色の器には白い円盤が収まっている。

ファイルの一部分を読むと、全て0だった。その範囲は、ディスク上でも同じように保存されているのでしょうか。Linuxのfallocateには、領域を解放して穴を作る操作と、領域を確保しながら0として読める状態にする操作があります。読み取れる内容が同じでも、領域の扱いは異なります。

ここではPUNCH_HOLEとZERO_RANGEを比べます。どちらも対応するファイルシステムで操作が成功した場合を前提にし、ファイルの長さ、読み取れるバイト、領域の割当てを別々に追います。

簡単にまとめると?

  • PUNCH_HOLEは指定範囲に穴を作り、範囲内の完全なファイルシステムブロックを解放します。KEEP_SIZEとの併用が必須で、ファイルの長さは保ちます。
  • ZERO_RANGEでもその範囲を読むと0になりますが、穴にかかる領域は事前確保されます。0が読めることだけでは、領域が解放されたとは判断できません。
  • 部分ブロック、ファイルシステムの対応、戻り値を確認します。穴の検出結果も、実際の割当てを必ず完全に示すとは限りません。

長さ、内容、割当ての三つを分ける

ファイルの長さは、先頭からどこまでバイトを読めるかという論理的な範囲です。一方、ファイルシステムがその内容のために確保している領域は、長さと同じ観点の数ではありません。穴を含むファイルでは、ある範囲が0として読めても、その範囲全体のデータ領域が同じ形で割り当てられているとは限りません。[1][2]

たとえば、長さ16 KiBのファイルの中央8 KiBが不要になったとします。末尾を切り詰めると後ろのデータまで読めなくなるため、長さを保ったまま中央を扱いたい場合があります。そこで必要なのが、不要部分の領域を手放したいのか、後で書く場所を確保しながら0として扱いたいのか、という目的の区別です。

PUNCH_HOLEは、長さを保って穴を作る

FALLOC_FL_PUNCH_HOLEは、指定した範囲で領域を解放する操作です。範囲内に完全に含まれるファイルシステムブロックは取り除かれ、境界にかかる部分ブロックは0にされます。成功後、その範囲を読むと0が返ります。[1]

このフラグはFALLOC_FL_KEEP_SIZEと一緒に指定する必要があります。したがって、末尾にかかる範囲を指定したとしても、ファイルの長さ自体を縮める操作にはなりません。「中央に穴を空ける」ことと、「後ろの内容を前へ詰める」ことも異なります。後者はCOLLAPSE_RANGEという別の操作です。

ZERO_RANGEは、0に見える領域を確保する

FALLOC_FL_ZERO_RANGEも、成功後に指定範囲から0を読める状態にします。ただし、穴にかかる部分については領域を事前確保するため、PUNCH_HOLEと同じ領域解放ではありません。ファイルシステムは、可能なら未書込みのエクステント、つまり領域は確保されているが内容を未書込みとして扱うまとまりへ変える方法を用います。[1]

この方法では、必ずしも全バイトへ物理的な0の書込みを行うわけではありません。読み手に0を返す状態を、ファイルシステムの管理情報で表せるためです。したがってZERO_RANGEを、保存媒体の過去の内容を確実に消去した証拠として扱うこともできません。

16KiBのファイルの中央8KiBを対象に、穴を作る場合と未書込み領域を確保する場合を比較。どちらも長さ16KiBで中央から0を読む。
独自の概念モデル。ブロックを4 KiBと仮定し、中央の完全な2ブロックを対象とした。実機の物理配置や削減量の測定ではない。ZERO_RANGEにもKEEP_SIZEを指定した比較。

ZERO_RANGEではKEEP_SIZEを追加指定できます。これを指定せず、対象が現在の末尾を超える場合は、長さも変わり得ます。図では違いを領域の扱いへ絞るため、両方とも長さを保つ条件を選びました。実際の呼び出しを比べる際も、主となるフラグだけでなく併用するフラグまで確認します。

独自モデルでは、同じ0でも二通りに分かれる

図の独自例では、最初は4 KiBのブロックが四つあり、先頭と末尾の内容を残します。中央二つへPUNCH_HOLEとKEEP_SIZEを適用したモデルでは、中央のデータブロックを穴として表します。ZERO_RANGEとKEEP_SIZEのモデルでは、中央も割当てを維持する未書込み領域として表します。読み出しを模した計算では、どちらも中央8,192バイトが0になり、全体の長さ16,384バイトは変わりません。

この一致は、読み出し結果だけを比較しても、二つの操作を区別できない例になっています。一方で、モデル内の割当て済みブロックは、前者が二つ、後者が四つです。これは説明用の管理状態であり、実際のディスク使用量が必ず半分になるという性能・容量の測定ではありません。管理情報や共有領域などの条件はモデルに含めていません。

対象の開始位置や長さがブロック境界にそろわなければ、話はさらに変わります。PUNCH_HOLEでも部分ブロックは丸ごと解放する対象ではないため、指定したバイト数をそのまま解放容量へ読み替えないようにします。ファイルシステムのブロックと、アプリが読み書きする任意のバイト範囲を分けることが必要です。

穴を調べる結果にも、読み方の条件がある

lseekのSEEK_DATAとSEEK_HOLEは、データや穴として扱われる次の位置を探すために使えます。バックアップなどで穴を保つ際に役立ちますが、ファイルシステムには全ての穴を細かく報告する義務がありません。SEEK_HOLEが常に末尾を返すような実装も許されるため、これだけで実際の割当てを完全に地図にできるとは限りません。[2]

また、lseekで末尾より先へ位置を動かすだけでは、ファイルの長さは変わりません。その先へ実際に書いたときにできる隙間が、後の読み出しで0を返します。位置を動かした、書き込んだ、長さが変わった、穴を観測した、という出来事も別々に記録します。

実装を選ぶときは、領域を回収したいのか、後の書込み用に確保したいのかを先に決めます。そのうえでファイルシステムの対応と制約、呼び出しの戻り値、長さ、必要な内容、割当て情報を確認します。本稿はLinux man-pages 6.19と独自モデルによる説明であり、Linuxカーネル上でfallocateを実行した試験ではありません。成功を確認できない操作を、0が読めるという一つの結果から成功扱いにはしません。

出典

  1. Linux man-pages 6.19、fallocate(2)、Deallocating file space、Zeroing file space、RETURN VALUE、ERRORS。
  2. 同、lseek(2)、DESCRIPTIONとSeeking file data and holes。4 KiBブロックの図と計算例は独自の概念モデル。