共有メモリを使ってみる ACT-1

さて、前回のメッセージボックスのように、本来HSPでできることをわざわざWin32 APIを使って実現させる必要などないので、どうせやるならHSPの標準機能ではできないことをやってみたいと思います。で、今回の目的は「プロセス間のメモリの共有」です。これを実現させるには、Windowsが持つメモリマップトファイルの機能を使います。

メモリの共有

Windowsでは、すべてのプロセスはそれぞれ自分専用の仮想アドレス空間を持っています。32ビットアプリケーションなら、アドレス0x000000000xFFFFFFFFの4Gバイトの仮想アドレス空間を持ちます。このアドレス空間はそれぞれのプロセスで独立したものであり、あるプロセスからは別のプロセスのアドレス空間にはアクセスできないようになっています。

例として、プロセスAがメモリ上にあるデータを格納していて、そのアドレスが例えば0x44444444であったとしましょう。別のプロセスBが同じアドレス0x44444444に格納されているデータを読み出しても、そのデータはプロセスAのものと同じではありません。あるいは、プロセスBのそのアドレスにはデータが存在せず、アドレス空間が破壊されたり、アクセス保護エラーが起こったりするかも知れません。このように、仮想アドレス空間ではメモリのデータを別のプロセスと共有することはできないのです。

そこで、メモリマップトファイルを使います。メモリマップトファイルの機能によって作成されるファイルマッピングオブジェクトを使えば、異なるプロセス間でも同一のデータ領域にアクセスすることができるのです。また、実際にディスク上にファイルを作らなくても、メモリ上に仮想的なファイルがあるとみなして、ファイルマッピングオブジェクトを作ることができます。

メモリマップトファイルとは

先ほどから何度か出てきている『メモリマップトファイル』について説明しておきましょう。

メモリマップトファイルとは、ディスク上のファイルをアドレス空間に割り当てて、ファイルの内容(データ)があたかもメモリに存在するかのようにそれにアクセスすることができる機能のことを言います。

ちょっと難しいかもしれませんが、ファイルの内容がそのままメモリ(アドレス空間)に反映(ミラーリング)されている状態というものを考えてみてください。この状態では、ファイルの内容が書き換えられると、同時にメモリの内容も同じものになります。そして、メモリ上のデータを書き換えたら、同時にファイルの内容も書き換わるという状態です。メモリマップトファイルは、ちょうどこのような状態が再現されているとみなすことができます。このような、仮想アドレス空間にファイル内容を反映させることを、「ファイルを仮想アドレス空間にマッピングする(マップする)」と言います。

気づいたかもしれませんが、その名が示す通り「メモリマップトファイル(Memory Mapped File)」とは「メモリ上にマップされたファイル」のことを指しているのです。ただし、このページでは、その「機能」についても同じく「メモリマップトファイル」と呼ぶことにします。

このように考えると、複数のプロセス間で同じファイルをマッピングすれば、事実上、それらのプロセスでメモリの共有が行なわれることが分かりますね。つまり、プロセスAのメモリを書き換えると、同時にメモリマップトファイルの内容が書き換えられ、プロセスBのメモリの内容も書き換えられる、ということが起こるのです。これが今回行なおうとしているメモリ共有のメカニズムです。


ところで、Windowsでは、メモリマップトファイルはメモリ共有のためだけに使われているのではありません。この機能は、主に次の3通りの方法で使われます。

まず第1に、メモリマップトファイルはWindowsが実行可能ファイル(EXEやDLL)をロード、実行するときに使われています。アプリケーションプログラムを実行するためには、その実行可能ファイルに含まれているプログラムコードがメモリ上に存在しなければなりませんよね(ここでいう「メモリ」とは、プロセスの仮想アドレス空間のこと)。この、「プログラムコードをメモリ上に読み込む」という動作を、メモリマップトファイルによって行なっているのです。(あくまで内部的に行なわれていることなので、ユーザーやプログラマからは見えませんけど。)

第2に、アプリケーションは、メモリマップトファイルの機能を使って、自分だけのためにすべてのデータがメモリ上にロードされているかのように、ディスク上のデータファイルにアクセスすることができます。本来、ファイルのデータにアクセスするには、あらかじめメモリを確保しておき、ファイルをオープンしてデータを読み込み、確保しておいたメモリ上に格納していく、という動作を行なうのです。しかし、メモリマップトファイルを使うと、メモリ確保やファイル読み込みを行なう必要がなくなります。(結局はメモリアドレスによる操作になるので、HSPには不向きですが。)

第3に、メモリマップトファイルを使うことによって、複数のプロセスの間でメモリ内のデータを共有することができるようになります。今回はこの目的のためにメモリマップトファイルの機能を使います。

ファイルマッピングオブジェクト

ところで、アプリケーションプログラム(プロセス)の側から見ると、このメモリマップトファイルの実体は「ファイル」としてではなく、『ファイルマッピングオブジェクト』というものとして認識されます。まずは、この説明をしておきましょう。

オブジェクトとは

まずは『オブジェクト』と呼ばれるものの話をしておきましょう。

オブジェクト(object)』とは本来『もの』という意味があるのですが、Windowsでは、『メモリ上に実体を持つもの』すべてがオブジェクトと呼ばれています。このような説明ではさっぱり分からないので、具体例を挙げると、プロセスやスレッドから、ウィンドウ、アイコン、カーソル、フォントに至るまでがオブジェクトとして管理されています。

Windowsのオブジェクトは大きく分けて次の3種類が存在します。

名前から分かるかもしれませんが、カーネルオブジェクト、GDIオブジェクト、ユーザーオブジェクトはそれぞれkernel32.dll、gdi32.dll、user32.dllによって提供されるオブジェクトです。

『オブジェクト』とは『メモリ上に存在する実体』のことです。そして、「プロセスがWindowsオブジェクト用のメモリを確保してそこにオブジェクトのデータや管理情報を格納する」ことを「オブジェクトを作成する」といいます。(ただし、このあたりの管理や処理の仕組みなどは、オブジェクトの種類によって異なってきます。)

すべてのWindowsオブジェクトは、『ハンドル』と呼ばれる値によって識別されます。このハンドルは、オブジェクトを作成したときに、オブジェクト作成用のAPI関数から戻り値として返されます。ハンドルはそれぞれのオブジェクトを指し示す値であり、アプリケーションからオブジェクトを操作するためにはこのハンドルを指定してAPI関数を呼び出さなければなりません。

ひとつ注意しておくべきことは、オブジェクトは通常プロセスごとに管理されるものであり、あるプロセスAで作成されたオブジェクトが別のプロセスBで有効ということはありません。プロセスAで作成されたオブジェクトのハンドルをプロセスBで指定したところで、何の意味もありません。プロセスBで同じオブジェクトを使いたければ、プロセスBでも同じようにオブジェクトを作成してハンドルを受け取る必要があります。

ファイルマッピングオブジェクト

ファイルマッピングオブジェクト』は、カーネルオブジェクトの1つです。役割としては、「ファイルのマッピングを管理するオブジェクト」といったところです。

ファイルマッピングオブジェクトに限らず、すべてのカーネルオブジェクトは、Windowsのカーネル(OSの一部・中核部分)によって管理されています。オブジェクト管理のためのデータ領域はカーネルによって確保されていて、プロセスからはそのデータブロックにアクセスすることはできないのです。プロセスは専用のAPI関数を呼び出すことで、オブジェクトの操作を行なえるようになっています。

オブジェクトをカーネルが管理しているという特性から、1つの同じオブジェクトに対して複数のプロセスからアクセスできることが分かるでしょうか。もしも、オブジェクトの情報が1つのプロセスからしかアクセスできないメモリ領域(すなわち、プロセスの仮想アドレス空間)に格納されていたとしたら、他のプロセスではその同じオブジェクトを使うことはできませんよね。OSが持つメモリ領域で管理されているからこそ、すべてのプロセスがAPI関数呼び出しによって同じオブジェクトを使うことができるのです。

ただし、オブジェクトは同一のものであるとしても、そのハンドルはプロセスごとに異なります。同じオブジェクトに対して、プロセスAが受け取るハンドルとプロセスBが受け取るハンドルは別のものになります。

ここで、オブジェクトハンドルがプロセスごとに同じにならない以上、他のプロセスから同じカーネルオブジェクトのハンドルを得るにはどうしたらいいかと思うことでしょう。実は、いくつかのカーネルオブジェクトは「名前つきのオブジェクト」というものを作成することができます。ファイルマッピングオブジェクトの場合も、名前つきのオブジェクトを作ることができます。すなわち、オブジェクトにアプリケーション固有のオブジェクト名を付けておけば、複数のプロセスから1つのオブジェクトを使うことができるのです。オブジェクトの名前には、アプリケーション名(またはそれの一部を変えたもの)など、他のアプリケーションとは重ならない名前を付ければよいでしょう。

実在のファイルを持たないファイルマッピングオブジェクト

これまでのメモリマップトファイルについての説明から考えると、メモリの共有を行なうには、マッピングを行なうためのファイルをいったん作成しなくてはならないように思えるでしょう。ところが実は、メモリ上のファイル(実際には存在しない仮想的なファイル)に対するマッピングオブジェクトを作成することができるのです。

実際には、本当にファイルが存在しないというわけではなく、ページングファイル上の領域が割り当てられて、仮想アドレス空間上にマッピングされます。『ページングファイル』というのは、Windowsが持つ仮想メモリ(ハードディスクをメモリのように見せかける機能)のデータが格納されているファイルのことです。

このような、実在のファイルが割り当てられないファイルマッピングオブジェクトは、複数のプロセス間でメモリを共有する目的で使用されます。したがって、名前付きのオブジェクトにしておく必要があります。

共有メモリの作成

さて、実際の手順を説明していきましょう。手順としては

  1. ファイルマッピングオブジェクトの作成
  2. 作成したオブジェクトを仮想アドレス空間にマッピング
  3. メモリに対して種々の操作
  4. アンマップ(マッピング解除)
  5. ファイルマッピングオブジェクトの削除

というようになります。

1.ファイルマッピングオブジェクト作成

まずはファイルマッピングオブジェクトを作成します。ファイルマッピングオブジェクトの作成にはCreateFileMapping関数を使います。

HANDLE CreateFileMappingA(
    HANDLE  hFile,            // ファイルハンドル
    PSECURITY_ATTRIBUTES psa, // セキュリティ指定
    DWORD   fdwProtect,       // ファイルデータ保護属性
    DWORD   dwMaxSizeHigh,    // サイズの上位32ビット
    DWORD   dwMaxSizeLow,     // サイズの下位32ビット
    PCTSTR  pszName           // オブジェクトの名前
);

hFileパラメータには本来CreateFile関数で取得されるファイルハンドルを指定するのですが、今回はメモリ共有のみが目的なので、0xFFFFFFFF(-1です)を指定しておきます。このようにすると、仮想ファイルに対してのファイルマッピングオブジェクトを作成できます。

psaパラメータには0 (NULL) を指定しておいて問題ありません。

fdwProtectパラメータは、ファイルマッピングオブジェクトからのデータ読み取りだけを行なうのか、それとも読み取りと書き込みの両方を行なうのかどうかを表す値を指定します。共有メモリでは当然読み取り・書き込み両方を行なうので4 (PAGE_READWRITE) を指定しましょう。このfdwProtectパラメータは、アクセス指定子と呼ばれるものです。これは、Windows 9x系では完全に無視されてしまいますが、Windows NT/2000/XPでは、正しい値を指定しておかなければなりません。

dwMaxSizeHighパラメータとdwMaxSizeLowパラメータには作成するマッピングオブジェクトのサイズ、すなわち共有するメモリのサイズを指定します。dwMaxSizeHighパラメータは4Gバイト以上のファイルマッピングオブジェクトを作成する場合に指定しますが、そんなに大きなサイズが必要になることなどまずないでしょう。(というか、ある理由によりWindows 9xでは作成できません。NT系でもハードディスクの空き容量が足りなければ無理ですので。) したがって、通常はこの引数は0になります。

pszNameパラメータには、作成するオブジェクトの名前を格納した文字列変数のアドレスを指定します。

この関数を呼び出すと、戻り値として作成されたファイルマッピングオブジェクトのハンドルが返されるので、これを変数に格納して保持しておきましょう。何らかの理由でオブジェクトを作成できなかった場合には、この値が0 (NULL) になっています。


ところで、すでに他のプロセスによってオブジェクトが作成されていたのか、それとも、自分のプロセスが初めてオブジェクトを作成したのかを知りたいときがありますよね。すでにオブジェクトが作成されていたかどうかを知りたい場合には、CreateFileMapping関数を呼び出した直後に、今度はGetLastError関数を呼び出します。

DWORD GetLastError(VOID);

この関数にはパラメータはありません。この関数は本来、API関数が失敗したときのエラーコードを返すためのものなのですが、CreateFileMapping関数の直後に呼び出すと、指定された名前のオブジェクトがすでに存在していたかどうかを判別できます。もしも指定された名前のオブジェクトがすでに作成されていた場合には、戻り値として183 (ERROR_ALREADY_EXISTS) が返されます。

2.仮想アドレス空間へのマッピング

次に、作成したオブジェクトを仮想アドレス空間へマッピングします。これは、作成したマッピングオブジェクトの一部もしくは全部をプロセスの仮想アドレス空間に割り当てて、プロセスからアクセスできるようにするものです。このようにしてマッピングされた個々の部分を『ビュー(view)』と呼びます。ファイルの一部だけをマッピングすることで、4Gバイトを超えるマッピングオブジェクトを仮想アドレス空間に割り当てることを可能にしています。(ただし、例によってWindows 9x系では無理。) ただし、このようにオブジェクトの一部のみをマップする場合、その先頭オフセットは割り当て単位の倍数でなければなりません。(割り当て単位は仮想アドレス空間の予約可能最小単位と呼ばれるもので、ほとんどの場合64Kバイトです。) 今回はオブジェクト全体を一度にマップするので、こういったことは考慮する必要はありません。

マッピングはMapViewOfFile関数を使います。

PVOID MapViewOfFile(
    HANDLE hMappingObject,     // マッピングオブジェクトのハンドル
    DWORD  dwDesiredAccess,    // データのアクセス方法
    DWORD  dwFileOffsetHigh,   // オフセットの上位32ビット
    DWORD  dwFileOffsetLow,    // オフセットの下位32ビット
    SIZE_T dwNumberOfByteToMap // マッピングをするバイト数
);

hMappingObjectパラメータには、ファイルマッピングオブジェクトのハンドルを指定します。これは、はじめにファイルマッピングオブジェクトを作成したときの戻り値になります。

dwDesiredAccessパラメータは、ビューからのデータ読み取りだけを行なうのか、それとも読み取りと書き込みの両方を行なうのかどうかを表す値を指定します。読み取りだけを行なう場合には4 (FILE_MAP_READ) を、読み取り・書き込み両方を行なうには2 (FILE_MAP_WRITE) を指定しましょう。

dwFileOffsetHighパラメータ、dwFileOffsetLowパラメータ、dwNumberOfByteToMapパラメータにはマッピングを行なう先頭オフセットやサイズを指定するのですが、今回のように、作成したファイルマッピングオブジェクトの全体をマッピングする場合には、これらのパラメータにすべて0を指定することができます。

MapViewOfFile関数は、戻り値として、マッピングされた領域の先頭アドレスを返します。

3.メモリへの読み書き

さて、MapViewOfFile関数の戻り値としてデータ領域の先頭のアドレスが返されるので、その領域に対してデータを書き込みましょう。データの書き込みには、ll_poke, ll_poke1, ll_poke2, ll_poke4の各命令を使うことができます。データを書きこむと、同じプロセスや他のプロセスから読み取ることができるようになります。読み取りには、ll_peek, ll_peek1, ll_peek2, ll_peek4を使うことができます。

データアクセスの際には、作成されたファイルマッピングオブジェクトよりも大きいサイズを書き込んだりしないように注意しておく必要があります。

4.マッピング解除

データを読み書きする必要がなくなったら、ビューをマッピング解除アンマップ)して、仮想アドレス空間から取り除きます。ビューをマッピング解除するにはUnmapViewOfFile関数を使います。

BOOL UnmapViewOfFile(
    LPCVOID pBaseAddress   // ビューのベースアドレス
);

pBaseAddressパラメータに指定するアドレスは、MapViewOfFile関数によって返されたビューの先頭アドレスでなければなりません。

この関数を呼び出してビューをマッピング解除しても、ファイルマッピングオブジェクト自体はまだ残っているので、再び読み書きする必要が生じたら再びMapViewOfFile関数を呼び出して先頭アドレスを取得し、アクセスすることができます。もちろん、アクセスするときだけマッピングするのではなく、常にマッピングされている状態にしてもかまいません。

5.オブジェクトのクローズ

ファイルマッピングオブジェクトが必要でなくなったら、オブジェクトのハンドルをクローズしてオブジェクトを削除します。オブジェクトをクローズするには、オブジェクトのハンドルを引数としてCloseHandle関数を呼び出します。

BOOL CloseHandle(
    HANDLE hObject    // オブジェクトのハンドル
);

ファイルマッピングオブジェクトをクローズしても、他のプロセスがそのオブジェクトをまだ使用していれば、そのオブジェクトは削除されずに残ります。そのオブジェクトを使用しているプロセスのすべてがオブジェクトをクローズすると、マッピングオブジェクトは完全に削除されます。

ここでひとつ注意しておくべきことは、もしまだマッピングされているビューがある場合には、必ずUnmapViewOfFile関数でマッピング解除を行なっておかなければなりません。マッピングされたままオブジェクトをクローズすると、メモリリークを起こす可能性が出てきます。


次回は具体的なサンプルスクリプトを作成して実行してみましょう。