プロセス間共有メモリ

今回の目的は「プロセス間のメモリの共有」です。これを実現させるには、Windowsが持つメモリマップトファイルの機能を使います。

メモリの共有

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

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

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

メモリマップトファイル

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

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

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

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

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

以前の仮想アドレス空間の説明で、仮想アドレス空間上のメモリにアクセスするためには、仮想アドレス空間に物理ストレージを割り当てる(マッピングする)必要があることを述べました。今回のメモリマップトファイルでは、その物理ストレージとして物理RAMではなくディスク上のファイルが割り当てられるということです。→Windowsの仮想アドレス空間


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

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

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

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

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

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

オブジェクトとは

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

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

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

名前から分かるかもしれませんが、カーネルオブジェクト、GDIオブジェクト、ユーザーオブジェクトはそれぞれkernel32.dllgdi32.dlluser32.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関数で取得されるファイルハンドルを指定するのですが、今回はメモリ共有のみが目的なので、-1 (INVALID_HANDLE_VALUE) を指定しておきます。このようにすると、仮想ファイルに対してのファイルマッピングオブジェクトを作成できます。

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) を指定しましょう。FILE_MAP_READを指定したビューに対して書き込みを行うとアクセス違反が発生し、HSP3の場合にはシステムエラーが報告されます。

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

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

3.メモリへの読み書き

さて、MapViewOfFile関数の戻り値としてデータ領域の先頭のアドレスが返されるので、その領域に対してデータの読み書きを行いましょう。これには、dupptr命令を使用することができます。この命令は、任意のアドレスが指し示す任意のサイズのメモリ領域を、HSPの変数領域として割り当てる機能があります。(→HSPでポインタ操作をするには)

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

4.マッピング解除

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

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

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

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

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

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

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

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

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

サンプルスクリプト

今回作成するサンプルプログラムでは、テキストボックスに表示されるテキストを共有メモリでやり取りします。同時に複数のプロセスを起動させて、それぞれのプロセスで書き込みや読み取りを行ってみると、うまくデータの共有がされていることが分かると思います。

各処理の部分に分けて見ていくことにします。また、今回はHSP標準のWindows API関数一括定義ファイルを使用することにします。一括定義ファイルではすべての関数が#func命令で定義されているため、Windows API関数を関数形式ではなく命令形式で実行し、戻り値をシステム変数statから取得しなければならないことに注意してください。

マッピングオブジェクト作成部分

#include "kernel32.as"

#define FILE_MAP_WRITE        2
#define FILE_MAP_READ         4
#define PAGE_READWRITE        4
#define ERROR_ALREADY_EXISTS  183

screen 0, 300, 224
textsize = 1024
sdim textbuf, textsize
mesbox textbuf, 300, 200
objsize 150, 24
pos   0, 200 : button gosub "書き込み", *WritedSharedMem
pos 150, 200 : button gosub "読み取り", *ReadSharedMem
onexit *OnAppExit      ; 終了時にジャンプ

; ファイルマッピングオブジェクトの作成
name = "HSP_mem_Test"  ; ファイルマッピングオブジェクトの名前
CreateFileMapping -1, 0, PAGE_READWRITE, 0, textsize, varptr(name)
hmapobj = stat
if hmapobj == 0 {
    dialog "共有メモリを作成できませんでした",1,"エラー"
    end
}

; オブジェクトが作成されていたかどうかの判別
GetLastError           ; GetLastError関数によるエラーコード取得
if (stat == ERROR_ALREADY_EXISTS) {
    ; すでに同じ名前のオブジェクトが存在する場合.
    ; 現在の共有メモリの内容を読み込んでおく.
    gosub *ReadSharedMem
}
stop

まずはマッピングオブジェクト作成部分です。

今回はメモリ上にマッピングオブジェクトを作成するためにCreateFileMapping関数の第1引数に-1 (INVALID_HANDLE_VALUE) を指定しています。ここでは共有メモリのサイズを1024バイトにしてあります。

マッピングオブジェクトを識別できるように、固有の名前を作成するオブジェクトにつけます。すでに同じ名前のマッピングオブジェクトがあるかどうかは、CreateFileMapping関数呼び出し直後にGetLastError関数を呼び出して、その戻り値を調べることでわかります。戻り値が183であれば、すでに同じ名前のマッピングオブジェクトが存在していることになります。

共有メモリへの書き込み

*WritedSharedMem
; ==== サブルーチン:共有メモリに書き込み ====
; ビューのマッピング
MapViewOfFile hmapobj, FILE_MAP_WRITE, 0, 0, 0
lpdata = stat           ; 先頭アドレス
; 共有メモリ領域を変数に割り当てる(文字列型)
dupptr sharedbuf, lpdata, textsize, 2
; 共有メモリ領域に文字列をコピー
sharedbuf = textbuf
; ビューのマッピング解除
; (この後で変数 sharedbuf にアクセスするとエラーになります)
UnmapViewOfFile lpdata
; (上記エラーの回避のため変数 sharedbuf の再確保)
dim sharedbuf, 1
return

共有メモリへの書き込み処理を行うサブルーチンです。テキストボックスに文字列を入力して「書き込み」ボタンを押すと、このサブルーチンにジャンプしてきて共有メモリに内容が書き込まれるようにしてあります。

MapViewOfFile関数を実行すると、データ領域の先頭アドレスが返されるので、dupptr命令を用いてそのアドレスの領域を変数に割り当てています。

ここではマップしてからアンマップするまでの処理を一度に行っていますが、アンマップのし忘れを防ぐためにも、このようにしたほうがいいでしょう。

共有メモリへからの読み取り

*ReadSharedMem
; ==== サブルーチン:共有メモリから読み取り ====
; ビューのマッピング
MapViewOfFile hmapobj, FILE_MAP_READ, 0, 0, 0
lpdata = stat           ; 先頭アドレス
; 共有メモリ領域を変数に割り当てる(文字列型)
dupptr sharedbuf, lpdata, textsize, 2
; 共有メモリ領域に文字列をコピー
textbuf = sharedbuf
; ビューのマッピング解除
; (この後で変数 sharedbuf にアクセスするとエラーになります)
UnmapViewOfFile lpdata
; (上記エラーの回避のため変数 sharedbuf の再確保)
dim sharedbuf, 1
; テキストボックスに反映させる
objprm 0, textbuf
return

こちらは、共有メモリの内容を読み込んで、テキストボックスに表示させるサブルーチンです。「読み取り」ボタンを押すことにより実行されます。また、2回目以降の起動時にも実行されるようになっています。

今回は読み出すことしかしないので、MapViewOfFile関数の第2引数には4 (FILE_MAP_READ) を指定しています。このようにしたときに変数sharedbufに対して書き込みを行うと、システムエラーで停止してしまうので注意が必要です。書き込みも行いたい場合には2 (FILE_MAP_WRITE) を指定しなければいけません。もちろん、読み取りしか行わない場合にFILE_MAP_WRITEを指定したとしても問題ありませんが。

ここでは再びdupptr命令を使って共有メモリの領域を変数に割り当て、その領域の文字列を読み取っています。その後、その内容をテキストボックスに表示しています。

終了時に実行される処理

*OnAppExit
; 終了処理(マッピングオブジェクトのハンドルのクローズ)
if hmapobj {
    CloseHandle hmapobj
}
end

終了時に実行される処理です。CloseHandle関数を呼び出してマッピングオブジェクトのハンドルをクローズし、オブジェクトの破棄を行います。ここで明示的にクローズを行わなかったとしても、プロセス終了時に自動的にクローズされるようになっているため、あえてこの部分を記述する必要はありませんが。

実際には、このマッピングオブジェクトを使用しているすべてのプロセスがオブジェクトハンドルをクローズしない限り、マッピングオブジェクトは破棄されません。共有メモリを使用しているすべてのプログラムでハンドルをクローズ(またはプログラム終了)して初めて、マッピングオブジェクトが削除されます。