メモリマップドファイルを使ったデータの共有


メモリマップドファイルとは,あたかもメモリの一部のように読み書きできるファイルの ことである. 実際のディスク上のファイルだけでなく,メモリ上の領域をメモリマップドファイルに割り当てることもできる. さらに,メモリマップドファイルを使えば,異なるプロセス間でデータを共有することができる. というか,Win32ではこれ以外にプロセス間にまたがったデータの共有方法が無い. DDE通信やOLEを使えばプロセス間でデータの交換はできる. ただし,自由度や処理速度の点では, メモリマップドファイルを使ったデータ共有の方が優れている.

今回,メモリマップドファイルを使って,Fortranで書かれた数値計算プログラムとDelphiで作ったプログラムとで データを共有する方法を検討したので,成果をまとめておく.


簡単な例

メモリマップドファイルを読み書きするためには,CreateMappingFileOpenMappingFileMapViewOfFileなどのAPIを用いる. まず,これらの使い方を練習するために,Delphiで以下のようなプログラムを作った.

host側プログラム
guest側プログラム

host側とguest側の両方のプログラムを起動しておく. host側でAとBの値を適当に設定し "copy to mapping file" ボタンを押すと メモリマッピングファイルにこれらの値がコピーされる. その後,guest側で "read from mapping file" ボタンを押すとメモリマッピングファイルから AとBの値を読み取り,その和を表示する.

両方のプログラムは完全に独立しているから,各々のアドレス空間は全く異なっている. したがって,host側からFindWindowを使ってguest側ウィンドウを検索し, host側のアドレス空間にあるデータへのポインタをSendMessageしたとしても, そのポインタはguest側のアドレス空間では全く意味を持たない. このように異なるアドレス空間を持つプロセス間でデータを共有するには, メモリマッピングファイルを使う方法が一番手っ取り早い.

host側のコードは以下のようになる.

var
  HFILE: THandle; // マッピングファイルオブジェクトのハンドル
  Arr: array[ 0..1 ] of Integer; // データを格納するための配列

procedure TForm1.FormCreate(Sender: TObject);
begin
   // マッピングファイルを作成する
   HFILE := CreateFileMapping( $FFFFFFFF, nil, PAGE_READWRITE,
                      0, SizeOf( Arr ), '_FileMappingTest' );
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
   // マッピングファイルを解放する
   CloseHandle( HFILE );
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  P: Pointer;
begin
   Arr[ 0 ] := SpinEdit1.Value;
   Arr[ 1 ] := SpinEdit2.Value;
   if HFILE <> 0 then
   begin
      // マッピングファイルをアドレス空間にマッピングする
      P := MapViewOfFile( HFILE, FILE_MAP_WRITE, 0, 0, 0 );
      if P <> nil then
      begin
         // 配列をコピーする
         CopyMemory( P, @Arr, SizeOf( Arr ) );
         // マッピングを解除する
         UnmapViewOfFile( P );
      end;
   end;
end;

Form1のOnCreateイベントの中でCreateFileMapping APIによって マッピングファイルを作成する. このAPIのプロトタイプは以下のようになっている.

function CreateFileMapping( hFile: THandle; lpFileMappingAttributes: PSecurityAttributes;
  flProtect, dwMaximumSizeHigh, dwMaximumSizeLow: DWORD; lpName: PChar ): THandle; stdcall;

hFileはファイルのハンドルであるが,メモリマッピングファイルを新たに作成するときは$FFFFFFFFを 指定する. lpFileMappingAttributesはセキュリティ属性であり,通常はnil(デフォルトのセキュリティ)でいいだろう. flProtectはマッピングファイルの保護に関するパラメータである. 今の場合は書き込みができなければならないのでPAGE_READWRITEを指定する. dwMaximumSizeHighおよびdwMaximumSizeLowはマッピングファイルのサイズであり, 前者は上位32ビットを,後者は下位32ビットを指定する(つまり64ビット=2^64バイトまでのサイズを指定できる). 最後のlpNameはマッピングファイルオブジェクトの名前となるヌル終端文字列へのポインタである. この例では "_FileMappingTest" という名前を付けた.

CreateFileMappingはマッピングファイルの作成に成功すると,そのハンドルを返す. このハンドルは後で必要になるので,グローバル変数のHFILEに格納しておく. マッピングファイルを解放するにはCloseHandle APIを用いる. 上の例では,OnCloseイベントの中でマッピングファイルの解放を行っている.

マッピングファイルは作成しただけでは使えない. 実際に値を読み書きする前に,MapViewOfFileによってアドレス空間にマッピングする必要がある. このAPIのプロトタイプは以下のようになっている.

function MapViewOfFile( hFileMappingObject: THandle; dwDesiredAccess: DWORD;
  dwFileOffsetHigh, dwFileOffsetLow, dwNumberOfBytesToMap: DWORD ): Pointer; stdcall;

hFileMappingObjectはマッピングファイルオブジェクトのハンドルであり, CreateFileMappingが返したハンドルを指定する. dwDesiredAccessはマッピングファイルへのアクセスを指定するパラメータであり, 今の場合は書き込みができなければならないのでFILE_MAP_WRITEを指定する. dwFileOffsetHighおよびdwFileOffsetLowはマッピングを開始する位置に関するパラメータであり, 先頭アドレスからのオフセットを指定する(前者に上位32ビット,後者に下位32ビットを指定する). 今の場合はファイルの先頭からマッピングするので,いずれも0を指定する. 最後のdwNumberOfBytesToMapはマッピングするバイト数であり, ファイルをすべてマッピングする場合は0を指定する.

MapViewOfFileはファイルのマッピングに成功すると,マッピングした領域の先頭アドレスを返す. 当然ながら,このアドレスは現在のアドレス空間に対してのみ有効である. 上の例ではCopyMemoryAPIを使ってメモリ内容をコピーしている. Pascalの標準手続きであるMoveはコピー元およびコピー先を変数パラメータで指定するので, 今の場合はアドレスを直接指定するCopyMemoryを使った方が簡単だろう.

マッピングファイルへのデータの読み書きが終了したらUnmapViewOfFileを呼び出して マッピングを解除する. このAPIにはマッピングした領域の先頭アドレス,すなわちMapViewOfFileが返したアドレスを 指定しなければならない. ポインタ演算などでMapViewOfFileの戻り値を変更しているときは要注意である.

次はguest側のコードである.

procedure TForm1.Button1Click(Sender: TObject);
var
  HFILE: THandle;
  P: Pointer;
  Arr: array[ 0..1 ] of Integer;
begin
   // 既存のマッピングファイルを開く
   HFILE := OpenFileMapping( FILE_MAP_READ, False, '_FileMappingTest' );
   if HFILE <> 0 then
   begin
      // マッピングファイルをアドレス空間にマッピングする
      P := MapViewOfFile( HFILE, FILE_MAP_READ, 0, 0, 0 );
      if P <> nil then
      begin
         // マッピングファイルからデータをコピーする
         CopyMemory( @Arr, P, SizeOf( Arr ) );
         Edit1.Text := IntToStr( Arr[ 0 ] + Arr[ 2 ] );
         // マッピングを解除する
         UnmapViewOfFile( P );
      end;
   end;
end;

既存のマッピングファイルを開くにはOpenFileMappingを用いる. このAPIのプロトタイプは以下のようになっている.

function OpenFileMapping( dwDesiredAccess: DWORD; bInheritHandle: BOOL;
                lpName: PChar ): THandle; stdcall;

dwDesiredAccessはマッピングファイルへのアクセスを指定するパラメータであり, 今の場合はデータを読み込むだけなのでFILE_MAP_READを指定する. bInheritHandleは新しいプロセスがハンドルを継承するか否かを指定するパラメータであるが, 今の場合は新しいプロセスを作成することはないのでFalseでいいだろう. lpNameはマッピングファイルの名前を指定するヌル終端文字列へのポインタである. ここでは,host側でCreateFileMappingを呼び出したときに指定した "_FileMappingTest" を指定する.

OpenFileMappingはマッピングファイルを開くのに成功するとそのハンドルを返す. 実際にマッピングファイル上の値を読むには,host側と同じようにMapViewOfFileを使って アドレス空間にマッピングしなければならない. guest側ではMapViewOfFileの2番目のパラメータにFILE_MAP_READを指定し, 3番目,4番目および5番目のパラメータにはすべて0を指定する.


Fortranコードとのデータ共有

ある講義の中で学部学生に二相流の数値解析を実演して見せることになった. Fortranで書かれたコンソールベースの数値解析のコードはすでに完成しており, このコードに適当なパラメータを設定して走らせ,ボイド率分布等の計算結果をグラフに表示させる というものである. 私たちが通常行っている作業は,コードへの入力をテキスト形式の入力ファイルで行い, テキスト形式で出力される計算結果をグラフソフト等を使って視覚化するというものである. しかしながら,ゲームやワープロ程度しかコンピュータを使ったことのない学生にこのような作業を行わせると, ソフトの使い方やグラフの見栄えだけに熱中してしまい, 肝心の数値解析への関心が薄れてしまう恐れがある.

そこで,パラメータをGUIで入力し,CreateProcessを使って数値解析コードの子プロセスをスタートさせ, 解析コードが終了すると同時に結果をグラフで表示するプログラムを作った. 結果のグラフを保存できるようにすれば,そのグラフを使ってレポートを作成できる.

ちなみに,子プロセスをスタートさせるコードは以下のように書けばよい.

procedure StartSimulation( Cmd: String );
var
   STARTUPINFO: TStartupInfo;
   PROCESSINFO :TProcessInformation;
begin
   with STARTUPINFO do
   begin
      cb := SizeOf( STARTUPINFO );
      lpReserved := nil;
      lpDesktop := nil;
      lpTitle := nil;
      dwFlags := STARTF_USESHOWWINDOW;
      wShowWindow := SW_SHOW;
      cbReserved2 := 0;
      lpReserved2 := nil;
      dwysize := 0;
   end;
   CreateProcess( nil, PChar( Cmd ), nil, nil,
            False,  CREATE_DEFAULT_ERROR_MODE, nil, nil,
            STARTUPINFO, PROCESSINFO );
   while WaitForSingleObject( PROCESSINFO.hProcess, 0 ) = WAIT_TIMEOUT do
    Application.ProcessMessages;
end;

このStartSimulation手続きは,Cmdパラメータで指定されたコマンドの子プロセスを生成し, WaitForSingleObjectで子プロセスを監視する. 子プロセスがシグナル状態になれば,すなわち子プロセスが終了すれば制御を戻す. 監視している間はApplicationオブジェクトのProcessMessagesメソッドを随時呼び出して ポストされたメッセージを処理するので,子プロセスが動いている間もウィンドウの再描画などが行える.

これでも十分かと思ったが,だんだん欲が出てきて, 時間ステップに従って変化するグリッド上の状態を刻々と表示できるようにしたいと思い立った.

この手の数値計算では,計算結果が安定するまで時間ステップを進めなければならない. どのくらい時間ステップを進めたらよいかというのは,実際に計算してみて試行錯誤的に決められるのが普通である. 時間ステップを経る毎に解析体系内の状態が漸近的に安定していく様子を学生に見せたいと考えたのである.

最初は,子プロセスの数値解析のループが一回終了する毎にグリッドの値をファイルに書き出し, 親プロセスではタイマ等で一定時間毎にそのファイルを読み出して値を表示する,という安易な方法を 試してみた. しかし,ループをある程度繰り返すと子プロセスが落ちてしまう. どうも,ファイルに読み書きするデータが大きく,子プロセス側ではファイルへの排他処理を行っていないため, ファイルへの出力に失敗するらしい.

いろいろ考えてみたが,メモリマップドファイルを使って子プロセスと親プロセスでの データの共有に挑戦してみることにした. 親プロセスはDelphiで作るので,上の練習のように,比較的簡単に使い方が理解できた.

問題は子プロセスである. マッピングファイルを開きデータを書き込むルーチンをFortranで書かなければならない. FortranコンパイラはCompaq Visual Fortan ver.5.0を 使うが,この開発環境ではWindows APIの呼び出しが完全にサポートされている. APIを使うためには,ブロックの最初に

      USE DFWIN
      USE DFWINTY

と書く. DFWINモジュールはAPIのプロトタイプが宣言されており, DFWINTYモジュールでは構造体や定数が宣言されている. 呼び出すAPIがどのように宣言されているか,逐一チェックしながらコーディングを行った. ちょっと見た感じでは,例えば汎用ポインタ(CでのLPVOID,DelphiではPointer)は, 単にINTEGER,つまり4バイト整数として宣言されているようである.

以下,元の数値計算コードに書き加えた部分を示す.

マッピングファイルの作成は親プロセスで行うので,子プロセスでは既存のマッピングファイルを開くことになる. したがって,メインルーチンの冒頭で

      COMMON /MAPPING/ HFILE
C
      HFILE = OpenFileMapping( FILE_MAP_WRITE, False,
     &                         '_VoidFractions' );

などとしてマッピングファイルのハンドルを取得しておく. ハンドルはCOMMONブロックのグローバル変数などに格納しておけばよいだろう.

実際にマッピングファイルに値を書き込むルーチンは以下のように書いた.

C********************************************************************
      SUBROUTINE OUTPUT_ALF_TO_MEM
C********************************************************************
      USE DFWIN
      USE DFWINTY
C
      IMPLICIT REAL*8(A-H,O-Z)
      COMMON /GEOD/ DX,DY,NX0,NYIN,NY0,NY1,NX,NY
      COMMON /UVGA/ UG(0:31,0:51),UL(0:31,0:51),VG(0:31,0:51),
     &          VL(0:31,0:51),U(0:31,0:51),V(0:31,0:51),
     &          VGM(0:31,0:51),UGM(0:31,0:51),
     &          P(0:31,0:51),ALF(0:31,0:51),ROM(0:31,0:51)
      COMMON /PRO/ ROL,ROG,AMUL,G,CGMA,OMG
      COMMON /INIT/ UIN,ALFIN,VGIN,UOUT,QIN
      COMMON /GEO/ DT,TIME,GAMG,NTIME,ISTEP,ITIME
C
      COMMON /MAPPING/ HFILE
C
      INTEGER PTR, PENTRY
      REAL*8 VALUE, THETIME
      LOGICAL STATUS
C
      PTR = MapViewOfFile( HFILE, FILE_MAP_WRITE, 0, 0, 0 )
      IF ( PTR.NE.0 ) THEN
        PENTRY = PTR
        THETIME = TIME
        CALL CopyMemory( PTR, LOC( THETIME ), SIZEOF( THETIME ) )
        PTR = PTR + SIZEOF( VALUE )
        DO I=1, NX
           DO J=1, NY
              VALUE = ALF( I, J )
              CALL CopyMemory( PTR, LOC( VALUE ), SIZEOF( VALUE ) )
              PTR = PTR + SIZEOF( VALUE )
           END DO
        END DO
        STATUS = UnmapViewOfFile( PENTRY )
      END IF
C
      RETURN
      END

上のサブルーチンにおいて,MAPPING以外のCOMMONブロックは数値計算上必要なデータが格納される. MapViewOfFileの戻り値は,DFWINの中で4バイト整数として宣言されていたので, INTEGERのPTRに格納する. PTRの値は同じくINTEGERのPENTRYにコピーしておく. 後でマッピングを解除する際にエントリポイントのアドレスが必要になるためにである.

MapViewOfFileが有意な値を返したら,まず現在の時間ステップをマッピングファイルにコピーし, PTRを倍精度実数のバイト数(8バイト)だけ進める. Fortranで変数やオブジェクトのアドレスを得るにはLOC関数を用い, サイズを得るにはSIZEOF関数を用いる. 次に,グリッド上のボイド率が格納されている配列ALFの要素を一つずつマッピングファイルにコピーする. この部分は配列を一度にコピーした方が処理が速いのだが,なぜかうまくいかなかった. コピーが終了したらUnmapViewOfFileを呼び出してマッピングを解除する.

次に親プロセスである. フォームのOnCreateイベントでマッピングオブジェクトを作成し, OnCloseイベントでマッピングオブジェクトを破棄する.

var
  VoidFractions: array[ 0..31, 0..51 ] of Double;
  HFILE: THandle;

procedure TVoidDlg.FormCreate(Sender: TObject);
begin
   HFILE := CreateFileMapping( $FFFFFFFF, nil, PAGE_READWRITE,
                               0, SizeOf( VoidFractions ) + SizeOf( Double ),
                               '_VoidFractions' );
end;

procedure TVoidDlg.FormClose(Sender: TObject; var Action: TCloseAction);
begin
   CloseHandle( HFILE );
end;

配列VoidFranctionはマッピングファイルから読み込んだボイド率を格納するための配列である. 実際にマッピングファイルから値を読み込むルーチンは以下のように書いた.

procedure TVoidDlg.UpdateDistribution;
type
  PDouble = ^Double;
var
  P, PEntry: PDouble;
  TheTime: Double;
  I, J: Integer;
begin
   if HFILE <> 0 then
   begin
      // メモリ空間にマッピング
      P := MapViewOfFile( HFILE, FILE_MAP_READ, 0, 0, 0 );
      if P <> nil then
      begin
         PEntry := P;
         TheTime := P^; // 時間を読み込む
         TimeLabel.Caption := 'Time = ' + FormatFloat( '#0.000', TheTime ) + ' sec';
         TimeLabel.Update;
         Inc( P );     // アドレスを進める
         // ボイド率を読み込む
         for I := 0 to MAX_I - 1 do
         begin
            for J := 0 to MAX_J - 1 do
            begin
               VoidFractions[ I, J ] := P^;
               Inc( P );
            end;
         end;
         DrawDistribution;  // ボイド率分布を表示
         VoidImage.Update;
         UnmapViewOfFile( PEntry ); // マッピングを解除
      end;
   end;
end;

このルーチンをタイマーを使って1秒毎に呼び出す. 以下の図は実行中の様子を示している.


お問い合わせはメールにて: akasaka@klc.ac.jp

戻る
SEO [PR] 爆速!無料ブログ 無料ホームページ開設 無料ライブ放送