安室 浩和(やすむろ ひろかず)

 大手コンピュータ・メーカー勤務。入社以来10数年をソフトウェア開発の現場で過ごし,その後ソフトウェア品質部へ異動。現場への技術支援や品質教育開発などを主に行っている。「APIで学ぶWindows徹底理解」(日経BP社)などを執筆。

 前回は,仮想メモリーの基本的な仕組みと,VirtualAlloc APIを使って,メモリーを動的に割り当てる方法について解説した。ページングを使用して,物理メモリーをプロセスの仮想メモリー空間にマップしている仕組みについては理解していただけたものと思う。今回は,この同じ仕組みを利用した,もう一つの重要な機能について説明する。それが「メモリー・マップト・ファイル」である。

ファイルを仮想メモリーにマッピングして読み書きする

 メモリー・マップト・ファイル(Memory-Mapped File)とは,名前からは想像しにくいかもしれないが,ディスク上のファイルのデータを,仮想メモリーに対応(ファイル・マッピング)させる機能である。

 通常,ファイルのデータを読み書きする場合は,Win32 APIや標準ライブラリ関数などを使ってファイルをオープンし,メモリー上のバッファに読み込み,そこでデータを読み書きした後,再びファイルへ書き戻す,という手順になる。ファイル・サイズとメモリー空間の空き状況によっては,一気にファイル全体をメモリーに読み込んで操作することも可能だろう。

 しかし,面倒なのは,更新したデータを最終的にファイルに書き戻す必要があることだ。データをほんの一部変更しただけなのに,バッファ全体を書き戻していたのでは非効率的だが,変更した個所だけを書き戻すには,変更履歴の管理が煩雑になる。かといって,変更する都度ディスクに書き込みに行くのもパフォーマンス上影響があるだろう。

 こうした問題をなんとか簡単に解決できないか,という問いへの答えになるのが,メモリー・マップト・ファイルだ。いったんファイル・マッピングを行えば,仮想メモリーに対する読み書きが,システムによってそのままファイルの読み書きに翻訳されて実行されるのである。プログラム上は,ファイルのデータを配列や構造体を扱うように処理できるようになる。なんとも便利そうな機能ではないか。

 メモリー・マップト・ファイルを使うためには,図1のような3段階の手順を踏む必要がある。

図1●メモリ・マップト・ファイルを使用する準備手順
図1●メモリ・マップト・ファイルを使用する準備手順

 手順の最初は,メモリーにマップするファイルを,ファイル・オブジェクトとして準備するものだ。このとき注意すべき点は,アクセス属性である。マップしたメモリーに読み書きを行うということは,結果的にマップされたファイル・オブジェクトを読み書きすることになるため,必要な操作に合わせたアクセス属性になっていなければならない。つまり,読み込みだけを行うなら「GENERIC_READ」でよいが,読み書き両方を行うなら「GENERIC_READ|GENERIC_WRITE」を指定する必要がある。また,保護属性は,メモリーにマップされているファイルを,別のプロセスから勝手に書き換えられないように指定する。これは,通常共有禁止を指定しておけばよい。

 次に生成する「ファイル・マッピング・オブジェクト」が,メモリー・マップト・ファイルを実現するオブジェクトだ。Create...とOpen...という2種類のAPIが書いてあるのは,ファイル・オブジェクトや同期オブジェクトなどと同様の理由である。Open...は既存のオブジェクトをオープンする専用のAPIだが,Create...は作成とオープンとどちらにも対応する。ここでは,CreateFileMappingのプロトタイプ定義をお見せしよう。


HANDLE CreateFileMapping(  // 戻り値:オブジェクトのハンドル
  HANDLE hFile,            // マップするファイル・オブジェクト
  LPSECURITY_ATTRIBUTES lpAttributes,  // セキュリティ属性
  DWORD flProtect,         // 保護属性
  DWORD dwMaximumSizeHigh, // マッピングのサイズ (上位32ビット)
  DWORD dwMaximumSizeLow,  // マッピングのサイズ (下位32ビット)
  LPCTSTR lpName           // オブジェクト名
);

 このAPIでも,第3引数に保護属性を指定するようになっているが,こちらは仮想メモリーにマップする際の,ページの保護属性に対応するものだ。VirtualAllocの4番目の引数に指定できるものとほぼ同じだが,「実行可能」属性については単独で指定することはできない(表1)。

表1●CreateFileMappingに指定可能な保護属性
マクロ名 説明
PAGE_READONLY 0x02 読み込みのみ可
PAGE_READWRITE 0x04 読み書き可
PAGE_WRITECOPY 0x08 読み書き可および書き込み時にコピー作成
PAGE_EXECUTE_READ 0x20 実行および読み込みのみ可
PAGE_EXECUTE_READWRITE 0x40 実行および読み書き可

 マッピングするサイズは,二つの引数を使用して,64ビットで指定できるようになっている。これは,Windowsのファイル・システムが,32ビットでは表せない4Gバイト以上の大きさのファイルをサポートしているためだ。

 マップするファイル全体にアクセスするのであれば,ファイル・サイズではなく0を指定してもよい。ただし,実際のファイル・サイズが本当に0の場合には,読むことも書くこともできないのでエラーになってしまう。

 ファイル・サイズより大きな値を指定してもよいが,その場合は,ファイル・マッピング・オブジェクトが生成されたタイミングで,ファイルが指定されたサイズまで自動拡張される。また,逆にファイル・サイズより小さい値を指定すると,ファイルの先頭から指定サイズまでのデータにしかアクセスできないオブジェクトが生成される(図2の右側部分)。

図2●メモリ・マップト・ファイルの利用イメージ
図2●メモリ・マップト・ファイルの利用イメージ

 最後の手順は,いよいよ仮想メモリーへのマッピングである。「ビュー」というのは,仮想メモリー空間に窓を開けて,そこからファイル・マッピング・オブジェクトをのぞき見るイメージだと考えてもらえばよいだろう(図2の左側部分)。窓を開けるためのAPI,MapViewOfFileExのプロトタイプは次のようになっている。


LPVOID MapViewOfFileEx(     // 戻り値:マップされた先頭アドレス
  HANDLE hFileMappingObject, // ファイル・マッピング・オブジェクト
  DWORD dwDesiredAccess,    // アクセス属性
  DWORD dwFileOffsetHigh,
         // マップするファイル・データのオフセット(上位32ビット)
  DWORD dwFileOffsetLow,
         // マップするファイル・データのオフセット(下位32ビット)
  SIZE_T dwNumberOfBytesToMap,  // マップするサイズ(32ビット)
  LPVOID lpBaseAddress          // マップしたい仮想アドレス
);

 最後の「lpBaseAddress」という引数が,窓を開ける位置(仮想アドレス)を指定するものだ。これにNULLを指定した場合は,システムが自動的に決めてくれる。窓を開けるAPIには,ほかにMapViewOfFileがあるが,MapViewOfFileExから最後の引数を省いただけのものだ。動作としては,省略された引数にNULLを指定したMapViewOfFileExと全く同じになる。

 ビューにファイルがマップされると,その仮想メモリー領域にはファイルのデータが現れる。つまり,VirtualAlloc APIで予約とコミットを同時に実行したのと同じ状態だ。したがって,この領域は「最小予約単位」の境界にフィットしていなければならない。VirtualAllocのときとは異なり,システム側で自動的に調整してくれないので注意が必要だ(ずれているとエラーになる)。

 第2引数には,またまたアクセス属性を指定するようになっている。ファイル・オブジェクトのアクセス属性,ファイル・マッピング・オブジェクトの保護属性に対応するものだ。これら三つは,どんな組み合わせでもOKというわけではない。わかりづらいので,それぞれの対応を表2にまとめる。

表2●ビュー,メモリ・マップト・ファイル,ファイルのアクセス属性と保護属性の対応
表2●ビュー,メモリ・マップト・ファイル,ファイルのアクセス属性と保護属性の対応

 一方,ファイルのどの部分をマップするのかを指定するのが,第3,4引数のペア(オフセット)と第5引数(サイズ)である。

 32ビットWindowsでは,プロセスのメモリー空間は4Gバイトだが,前回説明したように,XPなどでは通常2Gバイト,拡張しても3Gバイトの領域しかプログラムからは使用できない(仮想メモリーのユーザー領域)。したがって,それより大きいファイル全体をマップするのは不可能である。そこで,サイズは32ビット値で十分となり,オフセットと組み合わせて,巨大ファイルでも部分的にマップできるようになっているというわけだ。オフセットは,やはり「最小予約単位」の境界に合わせる必要がある。