mmapのPRIVATEとSHARED、メモリーへの変更はどこへ届くのか

左は浮いた薄紫の板だけに桃色の片があり、右は透明な柱でつながる緑の板二枚に同じ桃色の片がある。

ファイルをメモリーへ対応付けるmmapでは、配列へ値を入れるように内容を扱えます。ただし、その代入が元のファイルへ届くかどうかは、書き込みを許しただけでは決まりません。MAP_PRIVATEとMAP_SHAREDの選び方が、変更の行き先を分けます。

ここでは、同じ初期データへ一文字だけ代入する例から、読み書きの許可、変更の共有、保存の同期を分けて考えます。対象は通常のファイルを使ったマッピングです。匿名メモリー、デバイス固有の領域、性能比較は扱いません。

簡単にまとめると?

  • MAP_PRIVATEへの書き込みは、そのマッピングの私有の変更になります。元のファイルを編集するための指定ではありません。
  • MAP_SHAREDは変更を同じ領域の共有マッピングやファイルへ伝える方式です。ただし、複数の値を一括で更新する仕組みまでは用意しません。
  • 書けるかを指定するPROT_WRITE、変更の共有先、書き戻しを待つmsyncは別の役割です。用途に合わせて三つを確認します。

書き込み許可と、変更の行き先を分ける

mmapが作るのは、プロセスの仮想アドレス空間と、ファイルのある範囲との対応です。プログラムは返されたアドレスを通してその範囲を扱います。Linuxのmmapでは、prot引数が読み書きなどの保護を、flags引数がマッピングの共有方式を指定します。[1]

PROT_WRITEは、そのマッピングへ書き込めるよう求める指定です。一方、MAP_PRIVATEかMAP_SHAREDかは、その変更を誰と共有するかに関係します。「書き込める」という条件だけから、「元のファイルが変わる」と結論してはいけません。要求する保護と、ファイルを開いたときのモードも整合させる必要があります。

この二つを分けておくと、ファイルの内容を作業用に加工する処理と、ファイル自体を更新する処理を設計しやすくなります。同じ代入式でも、どのマッピングを通して実行するかで意味が変わるからです。

ABCDの二文字目を変える、小さなモデル

初期内容をABCDとするファイルを考えます。最初の実行では、そのファイルをMAP_PRIVATEで読み書きできるように対応付け、先頭から二文字目へXを代入します。変更後、そのマッピングを読むとAXCDになりますが、元のファイルへこのXを書き戻す操作にはなりません。

MAP_PRIVATEはコピーオンライト方式の私有マッピングです。変更を私有のものとして扱う仕組みであり、mmapを呼んだ瞬間にファイル全体を別の巨大な配列へコピーする、と説明する必要はありません。また、別の処理が後から元のファイルを変更した場合、それが私有マッピングに見えるかどうかは未規定です。「作成時点の内容を永久に固定したスナップショット」として使える保証もありません。[1]

二つ目の実行では、初期内容をABCDへ戻し、MAP_SHAREDのマッピングへ同じ代入をします。今度は変更を共有する方式なので、同じ領域を共有しているマッピングとファイルへ変更を伝える対象になります。図では二つの実行を混ぜず、それぞれの初期状態から比較しています。

別々に初期化したABCDの二文字目をXへ変更。PRIVATEでは私有表示AXCDとファイルABCD、SHAREDでは共有変更AXCDとファイルへの書戻し対象AXCDを分ける。
独自のデータ所有モデルによる比較。実際のLinux実行、ページフォルト数、書き戻し時刻の測定ではない。共有側の矢印は変更の対象を表し、電源断後の保存確認を意味しない。

この比較から選べるのは、まず変更の行き先です。元データを変えずに一時的な加工を行うなら私有方式、ファイルを更新したいなら共有方式が候補になります。読み取り中心か、書き換える範囲が広いかによるメモリー消費や速度の差は、この四文字のモデルからは判断できません。

共有できても、二つの代入が一組になるわけではない

共有する領域に、説明用の二つの値「個数」と「合計」を置く設計を考えます。初期値を個数1・合計10とし、別の値20を追加すると、更新後は個数2・合計30です。プログラムが個数を先に変更し、その次に合計を変更するなら、その二つの間を読む処理についても考えなければなりません。

説明用の逐次モデルで、その間に読取りを差し込むと、個数2・合計10という、更新の途中の組が見えます。MAP_SHAREDは同じ領域へ変更を伝える方式であり、この二項目をアプリの意味で一つの取引として更新する指定ではありません。実際の並行プログラムでは、言語のメモリーモデルや処理間の同期手段も関わるため、この単純なモデルをそのまま無同期の実装へ置き換えてはいけません。

したがって、共有領域を使う設計では、「どちらの処理がいつ変更し、いつ読んでよいか」を別に決めます。読み取り中の更新を排除するのか、変更完了を示す状態を同期して受け渡すのかなど、目的に合う手順が必要です。共有を選んだことと、途中状態を読まないことは、別々に検証する項目です。

見えた変更と、書き戻しの完了も別

MAP_SHAREDで変更を伝える先が決まっても、プログラムが代入を終えた時点を、そのまま保存完了時刻として扱うことはできません。マッピングした範囲の変更をファイルシステムへ書き戻すための呼出しがmsyncです。MS_SYNCでは更新を要求し、その完了を待ちます。戻り値も確認して、失敗を処理する必要があります。[2]

一方、msyncを呼べばMAP_PRIVATEで作った私有の変更が、共有の変更へ切り替わるわけではありません。保存したい内容をどの経路でファイルへ渡す設計なのかを先に揃えます。また、同期呼出しの成功と、複数項目をまとめたトランザクション、名前の置換、電源断試験は別の確認です。本稿の比較では、障害からの復旧や装置全体の耐久性までは実証していません。

用途を三つの問いへ分解する

mmapの指定を選ぶときは、最初に「この領域を読みたいのか、書きたいのか」を決めます。次に「その変更は私有の加工なのか、元のファイルや別の処理へ共有する更新なのか」を決めます。最後に、共有する場合は「いつ読んでよいか」「どの書き戻しの完了を確認するか」を決めます。

今回のモデルなら、PRIVATEでAXCDと読めたことは、私有の加工を確認した結果です。SHAREDで別の読取り側へ変更を伝えたいなら、共有方式に加えて読取りのタイミングを揃える必要があります。ファイルの保存まで要件に入るなら、その同期とエラー処理も確認に加えます。ひとつの「書けた」という観察を、これらすべての成功へ広げないことが、マッピングを扱うときの判断を明確にします。

出典

  1. Linux man-pages 6.19 — mmap(2)、DESCRIPTIONのprot、MAP_SHARED、MAP_PRIVATE。取得版は記事資料に保全。
  2. Linux man-pages 6.19 — msync(2)、DESCRIPTION、MS_SYNC、RETURN VALUE。