今回,メモリマップドファイルを使って,Fortranで書かれた数値計算プログラムとDelphiで作ったプログラムとで データを共有する方法を検討したので,成果をまとめておく.
メモリマップドファイルを読み書きするためには,CreateMappingFile, OpenMappingFile,MapViewOfFileなどのAPIを用いる. まず,これらの使い方を練習するために,Delphiで以下のようなプログラムを作った.
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;
function CreateFileMapping( hFile: THandle; lpFileMappingAttributes: PSecurityAttributes;
flProtect, dwMaximumSizeHigh, dwMaximumSizeLow: DWORD; lpName: PChar ): THandle; stdcall;
CreateFileMappingはマッピングファイルの作成に成功すると,そのハンドルを返す. このハンドルは後で必要になるので,グローバル変数のHFILEに格納しておく. マッピングファイルを解放するにはCloseHandle APIを用いる. 上の例では,OnCloseイベントの中でマッピングファイルの解放を行っている.
マッピングファイルは作成しただけでは使えない.
実際に値を読み書きする前に,MapViewOfFileによってアドレス空間にマッピングする必要がある.
このAPIのプロトタイプは以下のようになっている.
function MapViewOfFile( hFileMappingObject: THandle; dwDesiredAccess: DWORD;
dwFileOffsetHigh, dwFileOffsetLow, dwNumberOfBytesToMap: DWORD ): Pointer; stdcall;
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;
function OpenFileMapping( dwDesiredAccess: DWORD; bInheritHandle: BOOL;
lpName: PChar ): THandle; stdcall;
OpenFileMappingはマッピングファイルを開くのに成功するとそのハンドルを返す. 実際にマッピングファイル上の値を読むには,host側と同じようにMapViewOfFileを使って アドレス空間にマッピングしなければならない. guest側ではMapViewOfFileの2番目のパラメータにFILE_MAP_READを指定し, 3番目,4番目および5番目のパラメータにはすべて0を指定する.
ある講義の中で学部学生に二相流の数値解析を実演して見せることになった. 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;
これでも十分かと思ったが,だんだん欲が出てきて, 時間ステップに従って変化するグリッド上の状態を刻々と表示できるようにしたいと思い立った.
この手の数値計算では,計算結果が安定するまで時間ステップを進めなければならない. どのくらい時間ステップを進めたらよいかというのは,実際に計算してみて試行錯誤的に決められるのが普通である. 時間ステップを経る毎に解析体系内の状態が漸近的に安定していく様子を学生に見せたいと考えたのである.
最初は,子プロセスの数値解析のループが一回終了する毎にグリッドの値をファイルに書き出し, 親プロセスではタイマ等で一定時間毎にそのファイルを読み出して値を表示する,という安易な方法を 試してみた. しかし,ループをある程度繰り返すと子プロセスが落ちてしまう. どうも,ファイルに読み書きするデータが大きく,子プロセス側ではファイルへの排他処理を行っていないため, ファイルへの出力に失敗するらしい.
いろいろ考えてみたが,メモリマップドファイルを使って子プロセスと親プロセスでの データの共有に挑戦してみることにした. 親プロセスはDelphiで作るので,上の練習のように,比較的簡単に使い方が理解できた.
問題は子プロセスである.
マッピングファイルを開きデータを書き込むルーチンをFortranで書かなければならない.
FortranコンパイラはCompaq Visual Fortan ver.5.0を
使うが,この開発環境ではWindows APIの呼び出しが完全にサポートされている.
APIを使うためには,ブロックの最初に
USE DFWIN
USE DFWINTY
以下,元の数値計算コードに書き加えた部分を示す.
マッピングファイルの作成は親プロセスで行うので,子プロセスでは既存のマッピングファイルを開くことになる.
したがって,メインルーチンの冒頭で
COMMON /MAPPING/ HFILE
C
HFILE = OpenFileMapping( FILE_MAP_WRITE, False,
& '_VoidFractions' );
実際にマッピングファイルに値を書き込むルーチンは以下のように書いた.
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
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;
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;
SEO | [PR] 爆速!無料ブログ 無料ホームページ開設 無料ライブ放送 | ||