Solarisのdoorついて
このページでは、doorによる通信について解説を行う。
なお、「主筆」のクライアント・エディタ本体とサーバ間の通信には、Solarisのdoorを用いている。何の関係もないことだが。
概要
doorはプロセス間通信(InterProcess Communication : IPC)を実現する機能の一つである。
doorを使うことで、あるプロセスから別のプロセス内の手続きを呼び出すことが可能となる。呼び出される手続きを公開するプロセスのことをdoorサーバと呼び、呼び出す側をdoorクライアントという。
絵で描くとこうなる。

まず、サーバはdoor_create関数でdoorを作成する。ここではfooという関数をdoorとして公開している。
そしてクライアントはdoor_call関数でdoorの関数を呼び出している。
doorの作成
まずはサーバ側でdoorを作成する手続きについて解説する。
doorを作成するには、door_create関数を用いる。
dd = door_create( foo, (void*)"COOKIE", 0 );
|
この関数を呼び出すことにより「foo」という関数をdoorとして公開することを宣言する。第二引数の「(void*)"COOKIE"」と、第三引数の「0」は、manページを参照して欲しい。
戻り値はこのdoorを使うためのディスクリプタである。クライアントはこのディスクリプタを使ってdoorを呼び出す。
だがしかし、ここで返されたディスクリプタは、サーバでのローカルなものであって、他のプロセスで使うことはできない。
他のプロセスからdoorを呼び出すためには、名前付きパイプの要領でファイルシステム上のファイルにアタッチして、他のプロセスから使えるようにしてやる必要がある。
newfd = creat( "door", 0644 );
fdetach( "door" );
fattach( dd, "door" );
|
こうすることで、カレントディレクトリに"door"という名前の、doorにアタッチされたファイルが生成される。
 |
サーバ側でdoorを構築し、返されたディスクリプタをファイルにアタッチする。 |
でもって、doorを呼び出したいプロセスは、この"door"というファイルをopenしてディスクリプタを取得してやればいい。
 |
doorにアタッチされたファイルを開き、ディスクリプタを取得する。その後、取得したディスクリプタでdoorを呼び出す。 |
なお、サーバプロセスは、doorを作るだけ作ってmain関数を抜けてはならない。当然だがmain関数を抜けたらプロセスが終了してしまい、後に残るのは使えないDOOR_DEVICEのファイルだけとなってしまう。
だから、サーバプロセスのmain関数では、終了するべき時が来るまで待ち合わせを行うようにしなければならない。
なお、doorの手続きはサーバプロセスのmain関数を実行しているスレッドとは異なるスレッドで実行されるため、mainのスレッドを単純に止めてしまっても問題ない。
doorの手続き
次に、doorの手続きの作成方法について解説する。
doorで公開される関数のプロトタイプは下記のようになっている。
void foo(
void *cookie,
char *argp,
size_t arg_size,
door_desc_t *dp,
uint_t desc
);
|
関数名は任意だが、引数や戻り値の型は一致させる必要がある。当然だが。また、各引数の詳細な意味はmanページを参照して欲しい。
doorで公開される関数はマルチスレッドで処理される。doorの手続きだからといって、特にこれといった制約があるわけではないのだが、常にマルチスレッドで処理される可能性があるということにだけは気を付ける必要がある。
doorの手続きの最後には、必ずdoor_return関数を呼び出さなければならない。door_return関数を呼び出すことで、doorの手続きが終了される。
doorの破棄
サーバプロセスを終了するときや、doorが不要になったときには、doorを破棄しなければならない。
doorを破棄するには、door_revoke関数を呼び出す。この関数を呼び出すことにより、door手続きを無効化することができる。
if ( door_revoke( dd ) ) {
……
}
|
door_revoke関数を呼び出した瞬間にdoorの手続きが実行中だった場合はどうなるのか。詳細は解らないが、manページには「その処理は正常に終了することができる」と記載されている。おそらくdoorを安全に破棄することができるという意味だと思われる。
doorの呼び出し
次はクライアント側の処理について解説する。
doorを呼び出すためには、まずdoorのディスプリプタを取得しなければならない。その為、サーバ側で作成された"doorにアタッチされたファイル"をopenする必要がある。
dd = open( "door", …… ); /* doorのディスクリプタを取得 */
|
そして、取得したディスクリプタを用い、door_call関数によって、サーバの手続きを呼び出す。
DoorArg.data_ptr = …… /* 引数となる構造体に値を設定する */
……
DoorArg.rsize = 256;
door_call( dd, &DoorArg ) /* doorを呼び出す */
|
door_call関数はサーバのdoor関数が終了するまで(正確にはdoor_return関数が呼ばれるまで)制御を返さない。だからクライアント側では特に同期制御を行う必要はない。
door_call関数の第二引数に指定するdoor_arg_t構造体は、doorの手続きに渡す引数を指定するのと同時に、戻り値を受け取るためにも使用される。
まず、door関数のargpに渡すデータをdata_ptrに指定する。それと、data_ptrに指定したバッファのバイト長をdata_sizeに指定する。
 |
data_ptrとdata_sizeに指定したバッファが、doorの手続きの引数に渡される。 |
次に、クライアントからサーバに引き渡したいディスクリプタ(現在はdoorディスクリプタだけだそうだ)をdesc_ptrに、ディスクリプタの数をdesc_numに指定する。これは面倒だから説明は省略する。
最後に、door関数の戻り値を受け取る為のバッファをrbufに、rbufのバッファのバイト長をrsizeに指定する。
 |
rbufとrsizeに指定したバッファに、戻り値が書き込まれる。 |
以上の値を指定することで、door_call関数を呼び出すことが可能となる。そして、door_call関数を呼び出すことにより、引数に渡したdoor_arg_t構造体の値が書き換えられ、戻り値が与えられる。
まず、rbufに指定した戻り値を受け取るためのバッファの中身が書き換えられる。
だがその際、バッファが不足した場合、システムが勝手にメモリ領域を割り当てて、そのアドレスをrbufに書き込んでよこす。
 |
戻り値用のバッファとして256バイトのバッファを指定した。しかし、door_returnには1024バイトのバッファが指定された。
このままでは、戻り値を受け取ることができない。 |
 |
システムは、新たに1024バイトのバッファを確保して、アドレスをrbufに設定する。 |
だから、クライアント側では必要に応じて確保されたバッファをmunmap関数で解放してやる必要がある。
具体的には、呼び出し前にrbufに設定したアドレス値と、返ってきた後のrbufのアドレス値が異なっていたらmunmapしてやることになる。
/* 戻り値用のバッファを確保 */
void *pBuffer = malloc( 256 );
/* door_arg_t構造体のメンバに、*/
/* 戻り値用のバッファのアドレスを設定する */
DoorArg.rbuf = pBuffer;
DoorArg.rsize = 256;
/* doorの手続きを呼び出す */
door_call( dd, &DoorArg );
/* システムによってメモリ割り当てが行われたら、*/
/* その領域を開放する */
if ( DoorArg.rbuf != pBuffer ) {
munmap( DoorArg.rbuf, DoorArg.rsize );
}
/* 戻り値用のバッファを解放する */
free( pBuffer );
|
またそれ以外に、data_ptrとdesc_ptrの値も変更される。
この二つの値には、rbuf中の"文字列"の戻り値(door_return関数の第一引数)と、"ディスクリプタ"の戻り値(door_return関数の第三引数)のアドレスがセットされる。
 |
rbufはバッファの先頭アドレス、data_ptrにはバイト配列の先頭アドレス、desc_ptrにはディスクリプタの配列の先頭配列が設定される。 |
プログラム例
以上のことを踏まえてプログラムを作ってみる。
まずはサーバ側のプログラム。
#include <stdio.h>
#include <door.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
// doorとして公開される手続き
void foo(
void *cookie, // door_create関数の第二引数の値
char *argp, // door_call関数のDoorArg構造体のdata_ptrの値
size_t arg_size,// argpのバッファ長(バイト数)
door_desc_t *dp,// door_call関数のDoorArg構造体のdesc_ptrの値
uint_t desc // dpにあるディスクリプタの個数
)
{
// 別スレッドで実行される
printf( "cookie = %s\n", cookie );
strncpy( argp, "MODIFIED CLIENT DATA", arg_size );
// doorサーバの手続きの最後に呼び出さなければならない。
// この関数を呼び出すと、クライアントに戻り値を返して、
// 次のdoor呼び出しがあるまでブロックする。
// 第一引数には"文字列"の戻り値を指定する。
// 第二引数には第一引数び指定したバッファのバイト長を指定する。
// 第三引数には"ディスクリプタ"の戻り値を指定する。
// 第四引数には"ディスクリプタ"の戻り値の個数を指定する。
door_return( (void*)"DOOR RETURN DATA", 17, dp, desc );
}
int main()
{
int dd;
struct stat buf;
int w;
printf( "Server start\n" );
// doorを生成する
// 第一引数には、doorサーバとなる関数を指定する。
// 第二引数には、doorサーバ関数のcookieに渡される値を指定する。
// 第三引数には、生成するdoorの属性を指定する。(詳細は略)
dd = door_create( foo, (void*)"COOKIE", 0 );
if ( dd < 0 ) {
// 失敗した
printf( "ERROR : create_door\n" );
switch ( errno ) {
case EINVAL:
// 属性の値が不正
break;
case EMFILE:
// ファイル開きすぎ。もう開けない。
break;
}
return 0;
}
// カレントディレクトリに
// doorという名前のファイルを用意する
if ( stat( "door", &buf ) < 0 ) {
int newfd;
newfd = creat( "door", 0644 );
if ( newfd < 0 ) {
printf( "ERROR : creat\n" );
return 0;
}
close( newfd );
}
fdetach( "door" );
// 構築したdoorのファイル記述子をアタッチする
if ( fattach( dd, "door" ) < 0 ) {
printf( "ERROR : fattach\n" );
return 0;
}
// 終了を待ち合わせる
scanf( "%d", &w );
// doorを無効化する
// この呼び出し以降の要求は全て拒否される。
// なお、この関数を呼び出して時点で実行しているdoor呼び出しは、
// 正常終了することができる。
if ( door_revoke( dd ) ) {
// 失敗
printf( "ERROR : door_revoke\n" );
switch ( errno ) {
case EBADF:
// 不正なdoorディスクリプタが指定された。
break;
case EPERM:
// 指定されたdoorディスクリプタは、
// このプロセスでdoor_createによって生成された物ではない。
// (他のプロセスで作られたdoorを無効化することはできない。)
break;
}
}
// アタッチを解除する
fdetach( "door" );
return 0;
}
|
次はクライアント側のプログラム。
#include <stdio.h>
#include <stdlib.h>
#include <door.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <memory.h>
#include <errno.h>
int main( int argc, char* argv[] )
{
int dd;
door_arg_t DoorArg;
char ArgBuf[256]; // サーバに渡す"文字列"の引数
char RetBut[256]; // 戻り値を受け取るためのバッファ
// 引数が不足していれば終了
if ( argc < 2 ) return 0;
// doorにアタッチされているファイルを開く
dd = open( argv[1], O_RDWR );
// doorの呼び出しの、引数と戻り値用のバッファを初期化
strncpy( ArgBuf, "CLIENT DATA", 256 );
strncpy( RetBut, "", 256 );
// DoorArg構造体に値を設定する。
DoorArg.data_ptr = ArgBuf; // "文字列"の引数
DoorArg.data_size = 256; // data_ptrのバッファのバイト長
DoorArg.desc_ptr = NULL; // サーバに渡すディスクリプタ
DoorArg.desc_num = 0; // desc_ptrのディスクリプタの個数
DoorArg.rbuf = RetBut; // 戻り値用のバッファ
DoorArg.rsize = 256; // rbufのバッファのバイト長
// (実を言うと、data_ptrとrbufには
// 同じバッファを使用することができるらしい。)
printf( "Client start\n" );
// doorの呼び出し
if ( door_call( dd, &DoorArg ) ) {
// 失敗
printf( "ERROR : door_call\n" );
switch ( errno ) {
case E2BIG:
// 引数が大きすぎる。サーバスレッドのスタックが足りない。
break;
case EAGAIN:
// サーバのリソースが使用可能な状態ではない。
break;
case EBADF:
// 不正なdoorディスクリプタが指定された。
break;
case EFAULT:
// 引数のポインタがアロケートされたメモリ領域の
// 範囲外をポイントしている。
break;
case EINTR:
// クライアントでシグナルが発生した、
// クライアントでforkが呼び出された、
// サーバが処理を終了している、のどれか。
break;
case EINVAL:
// 不正な引数が渡された。
break;
case EMFILE:
// クライアントもしくはサーバで、
// ディスクリプタを開きすぎている
break;
case ENOTSUP:
// doorにDOOR_REFUSE_DESCフラグが設定されており、
// desc_numの引数が0でない。
break;
case EOVERFLOW:
// 呼び出し側で、戻り値のオーバーフロー領域の確保に失敗した。
break;
}
}
// doorの実行結果を表示
// rbufには"文字列"とディスクリプタの両方の戻り値が書き込まれる。
// そして、data_ptrには"文字列"の戻り値の開始アドレスが設定され、
// desc_ptrにはディスクリプタの戻り値の開始アドレスが設定される。
// つまり、door_call関数から返ってきた時には、
// data_ptrとdesc_ptrは、rbufのバッファ上のアドレスを
// ポイントしていることになる。
printf( "ArgBuf = 0x%08X : %s\n", ArgBuf, ArgBuf );
printf( "RetBut = 0x%08X : %s\n", RetBut, RetBut );
printf( "DoorArg.data_ptr = 0x%08X : %s\n",
DoorArg.data_ptr, DoorArg.data_ptr
);
printf( "DoorArg.rbuf = 0x%08X : %s\n",
DoorArg.rbuf, DoorArg.rbuf
);
// 万一戻り値がDoorArg.rsizeに設定した値よりも大きかった場合には、
// システムは自動的にメモリ領域を割り当てて、
// DoorArg.rbufにそのアドレスを設定する。
// だから、door_call関数を呼び出す前に設定したアドレスと、
// 関数から返ってきた時のアドレスが異なっていたら、
// munmapでメモリ領域を開放してやる。
if ( DoorArg.rbuf != RetBut ) {
// システム側でメモリをアロケートしていたら、それを解放する
munmap( DoorArg.rbuf, DoorArg.rsize );
}
return 0;
}
|
一見すると長いような気がするが、よく見るとコメントとエラー処理が多いだけである。
上記二つのプログラムをコンパイルするには、doorのライブラリが必要となる。その為、コンパイル時に"-ldoor"を指定する必要がある。
実行すると下記のようになる。
 |
左の図は、サーバを起動したときの状態である。 右上の端末エミュレータでサーバプロセスを起動しており、右下の端末エミュレータではクライアントを起動する。
また、左側のファイルマネージャには、生成したdoorにアタッチされたファイル"door"が表示されている。ついでに言うと、この状態で"door"というファイルを削除しようとしても失敗する。
|
 |
クライアントを実行すると左図のようになる。
doorの呼び出しによって、サーバ側では"coolie = COOKIE"という文字列が表示され、クライアント側にはサーバ側から与えられた文字列が返されているのが確認できる。 |
補足1
manページの和訳
あっているかどうかは知らない。
補足2
manページの原文
補足3
サーバ用プロセスを途中で殺すと、DORR_DEVICEのファイルが残る事がある。このファイルは単純に削除しようとしても削除できない。
削除するためには一旦デタッチする必要がある。
デタッチを行うには"fdetach"システムコールを呼び出すか、あるいは同名のコマンドを用いる必要がある。
補足4
door_info関数を用いることにより、公開しているdoorに関する様々な情報を取得することができる。そのため、door_info関数で取得可能な範囲にはセキュアな情報を格納してはいけない。
特にCOOKIEには注意する必要がある。
|