C MAGAZINE Linux programming Tips
第5回 プロセス間通信(System V IPC)プログラミング登竜門
■はじめに
今回は、Linux上で2つ以上のプロセスがデータを受け渡すためのに必要なプログラミングについて紹介します。 UNIXのプロセス通信機構としては、パイプ (pipe)、ソッケト(socket)、メッセージキュー(message queue)、 共有メモリ (shared memory)、セマフォ(semaphore)などがあります。 後者3つは、 SystemV IPC(Inter Process Communication)と呼ばれるもので、 その中のメッセー ジキュー、共有メモリーのプログラミングについて説明します。
System V IPCは、歴史的には、名前の通りSystem V Unixで実装されたもので、 現在のほとんどのUnix互換OSで利用可能になっております。 もちろんLinuxカー ネルも例外でなくSystem V IPCが利用可能です。 カーネルのコンパイルオプションで、無効にもできますが、 ほとんど世の中に配布されているカーネルのプリコ ンパイルバイナリーは有効になっていることでしょう。 System V IPCを無効にし た場合、いくつかのアプリケーションが動作しなくなります。
■メッセージキュー、共有メモリ、セマフォとは?
共有メモリ(shared memory)
共有メモリは、複数のプロセスで、一つのメモリゾーンを共有する機構です。 通 常、あるプロセスが(mallocなどで)確保したメモリは、他のプロセスからはアクセスできません。 共有メモリ機構は、複数のプロセスが読み込み、書き込み可能 なメモリゾーンを作成します。

メッセージキュー(message queue)
メッセージキューは、よくレターボックスに例えられますが、ピンとこないと思います(少なくとも自分は)。
下図を参考にしてください。サーバは、複数のデータをある長さで分割して、順番を保存しながらバッファーに登録します。 このバッファがメッセージキューが 呼ばれるものです。この機構では、クライアントは、ある特定のデータをメッセージキューから取り出すことができます。
セマフォ(semaphore)
今回は、紙面の都合上省かして頂きましたが、セマフォは、カーネルモードプロセスの同期などに使われている非常に重要な機構です。
この機構は、マルチタスクOSの上で、同一データを複数のプロセスがアクセスできるようにします。 セマフォは、共有フラグの一種とも言えます。同期が必要な プロセス同士がこのフラグに注目し、 フラグ変化に応じて処理を行なうようにす ることで、同期を実現します。
ちなみにこのセマフォ(semaphore)というちょっと変わった名前は、ギリシャ語のサイン(sema)と担い手(phoreus)が語源のようです。
■System V IPCプログラミング
System V IPCは、他の通信機構とは異なりファイルシステムを使用しません。 そのためメッセージキュー、セマフォ、共有メモリのプログラミングを行なう場合は、open,read,writeなどは使用できません。 もちろんファイルディスクリプター もないので、変わりに特別な識別番号(IPCキー)を生成し、他のプロセスと通信することになります。
IPCキーを生成するには、関数 ftok() を使用します。関数 ftok は、IPC キー を生成するために、アクセス可能なファイルのパス名とプロジェクト識別名を求めます。
以下にIPCキー生成の例を示します。パス名とプロジェクト識別名は、データを 共有するプログラム間で同じでないと行けないので、共通のヘッダーファイルにいれておくのが便利でしょう。
#define KEYFILE_PATH "hogehoge"
#define PROJ_CHAR (char)'z'
int generate_key(void)
{
return ftok(KEYFILE_PATH,PROJ_CHAR);
}
■共有メモリ(shared memory)のプログラミング
共有メモリは、複数のプロセス間で、共有メモリゾーンへの読み書きを行なえるのですが排他処理等が必要になるので、今回は簡略化して、一つのプロセスが共有メモリへの書き込み、 もう一方のプロセスが共有メモリから読みだしを行なう単純な実装を紹介します。
プログラムリスト 1-1, 1-2, 1-3が共有メモリの実装例になります。この実装例では、書き込み側のプログラムがsinテーブルを共有メモリに書き込み、 読み込み側のプログラムが共有メモリからsinテーブルを読み込み、標準出力に出力します。
プログラムリスト 1-1, 1-2の書き込み側、読みだし側の流れは以下のようになります。
書き込み側 - プログラムリスト 1-2
0. IPCキーの作成
1. 共有メモリの識別子の作成
2. 共有メモリゾーン(セグメント)の割り当て
3. 共有メモリへの書き込み
4. 共有メモリゾーン(セグメント)の破壊
- 読みだし側 - プログラムリスト 1-3
0. IPCキーの作成
1. 共有メモリの識別子の作成
2. 共有メモリゾーン(セグメント)の割り当て
3. 共有メモリからの読みだし
共有メモリの識別子の作成
識別子の作成は、関数 shmget()で行ないます。
int shmget(ket_t key, int size, int option)
keyは、関数 ftok()で生成されたもので、sizeには、メモリゾーンの大きさを指定します。 実際に作成されるメモリゾーンの大きさは、sizeを PAGE_SIZEの倍数へ引き上げた値になります。
optionには、動作フラグとモードをORしたものを指定します。例えば、書き込み側は、IPC_CREAT|IPC_EXCL|0600のように指定します。
fork() した場合、その子プロセスは付加された共有メモリゾーンを継承します。一方、exec() した後、全ての付加された共有メモリゾーンは分離されます。
共有メモリゾーン(セグメント)の割り当て
メモリゾーンの割り当ては、関数 shmat() で行ないます。
void *shmat( int shmid, const void *shmaddr, int shmflg )
shmidには、関数shmget()で生成した共有メモリの識別子を指定します。 shmaddrは、今回の例では、NULLを指定しておきます。shmflgは、動作フラグで、読みだし側はSHM_RDONLYを指定しています。 この関数の戻り値が共有メモリへのポインターになりその領域への読み込み、書き込みは通常のメモリ領域同様に 行なえます。
共有メモリゾーン(セグメント)の破壊
共有メモリの使用を終える場合は、メモリゾーンの破壊をする必要があります。 関数 shmctl()で行ないます。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmidに、関数shmget()で生成した共有メモリの識別子を指定し、cmdに、 IPC_RMIDを指定します。
コンパイル方法 & 実行方法
以下のように実行すると、ファイル datにサインテーブルが書き込まれます。
$ cc -Wall -o shm-write shm-write.c -lm
$ cc -Wall -o shm-read shm-read.c -lm
$ (./shm-write&) ; ./shm-read > dat
■メッセージキュー(message queue)のプログラミング
プログラムリスト 2-1, 2-2, 2-3がメッセージキューの実装例になります。 この実装例では、サーバが2つのsinテーブル(一つは位相が90度なのでcosですが)をメッセージキューに送り、 クライアントがメッセージキューからその内一つを読みだし標準出力に出力します。 両方のデータに対して、クライアントから終了の合図をメッセージキューから受け取るとサーバは終了します。
プログラムリスト 2-1, 2-2のサーバ、クライアントプログラムのおおまかな流 れは以下のようになります。
- サーバ - プログラムリスト 2-2
0. IPCキーの作成
1. メッセージキューの識別子の作成
2. メッセージキューへデータを送る
3. メッセージキューから終了の合図を待つ
4. メッセージキューの破壊
- クライアント - プログラムリスト 2-3
0. IPCキーの作成
1. メッセージキューの識別子の作成
2. メッセージキューからデータを取得
3. メッセージキューへ終了の合図を送る
メッセージキューの識別子の作成
識別子の作成は、関数 msgget()で行ないます。
int msgget ( key_t key, int msgflg )
keyは、関数 ftok()で生成されたもので、メッセージキューの作成は、以下のように行ないます。 この場合、パーミッション 666(全ユーザ read,write可能)で メッセージキューを作成します。
msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666 ) ;
クライアント側は、以下のようになります。戻り値がメッセージキューの識別子 になります。
msgid = msgget(key, IPC_EXCL);
メッセージキューの書き込み、読み込み
メッセージキューへの書き込み、書き囲みは、それぞれ関数 msgsnd(), msgrcv()で行ないます。 引数等は、マンページで確認してください。
ヘッダーファイルで定義されている構造体は以下のようなものですが、これでは 1バイトづつしか送れないように見えます。
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
一回のメッセージ送受信を1バイト以上で行ないたい場合は、上記の構造体を変更して、関数 msgsnd(),msgrcv()に渡すことができます。
今回の実装では、以下のような構造体の宣言をしています。 ここで注意したいのは、必ず先頭に long mtype が必要なことです。 これはメッセージのタイプで、今回の実装例では、サインテーブルが1、コサインテーブルが2に割り当てられています。
typedef struct {
long mtype; /* type of message */
size_t len; /* 0 <= length of buf < BUFLEN
: -1 means no more data */
char buf[BUFLEN];
} MsgBuf;
メッセージキューの破壊
メッセージキューの使用を終える場合は、関数 msgctl()で、メッセージキュー を破壊する必要があります。サーバ側のみ必要な処理です。
int msgctl ( int msqid, int cmd, struct msqid_ds *buf )
msgidに、関数msgget()で生成したのメッセージキューの識別子を指定し、cmdに、 IPC_RMIDを指定します。
コンパイル方法 & 実行方法
以下のように実行すると、ファイル dat1,dat2にサイン(コサイン)テーブルが書き込まれます。
$ cc -g -Wall -o mq-server mq-server.c -lm
$ cc -g -Wall -o mq-client mq-client.c -lm
$ (./mq-server &) ; ./mq-client 1 > dat1 ; ./mq-client 2 > dat2
waiting for client terminated
client terminated : type 1
client terminated : type 2
|
■Linuxでのメッセージキュー、セマフォ、共有メモリの扱い
ファイルディスクリプターの数に最大値があるように、メッセージキュー、セマフォ、共有メモリにもリソースの最大値があります。 Linuxの場合、コマンド ipcs でそれらの値を取得することができます。またいくつかの最大値は、procファイルシステム経由で動的に変更可能です。 それ以外のものは、動的に変更はできませんが、カーネルのソースコードにハードコートしてある値を変更し、 コンパイルすれば変更可能です。 動的に変更したい場合は、やはりprocファイル システムへのインターフェイスを新たに実装する方法がよいでしょう。
参考までに、TurboLinuxが配布している kernel-2.2.16-5 カーネルでの System V IPCのリソースの初期値を以下のテーブルに載せておきます。
シェアードメモリの各リソースの上限値及び変更方法
| 名称ディフォルト値 |
変更方法 |
| セグメント数の最大値128 |
なし |
| セグメントサイズの最大32768(単位:KByte) |
/proc/sys/kernel/shmmax
(単位:Byte) |
| 合計シェアードメモリの最大16777216 (単位:KByte) |
/proc/sys/kernel/shmall
(単位:KByte/4) |
| セグメントサイズの最小1 (単位:KB) |
なし |
メッセージキューの各リソースの上限値及び変更方法
| 名称ディフォルト値 |
変更方法 |
| システム全体での最大キュー数128 |
なし |
| 最大メッセージサイズ4056 (単位:Byte) |
なし |
| キューサイズ最大値の初期値16384 (単位:Byte) |
なし |
セマフォの各リソースの上限値及び変更方法
| 名称ディフォルト値 |
変更方法 |
| 最大配列数128 |
なし |
| 配列毎の最大セマフォ数250 |
なし |
| <システム全体の最大セマフォ数32000 |
なし |
| semop 呼び出し毎の最大命令数32 |
/proc/sys/kernel/semopm |
| セマフォ最大値32767 |
なし |
上記で、変更方法がある場合、設定変更は、echo コマンドなどで以下のように 行ないます。
# cat /proc/sys/kernel/semopm
32
# echo 16 > /proc/sys/kernel/semopm
# cat /proc/sys/kernel/semopm
16
■おまけ
IPCリソースの削除
使っているリソースを削除したい場合があります。その場合は、コマンド ipcrm を使います(なんとなく lprmに似ている気もするけど )。
以下に、メッセージキューの削除例を示します。
$ ipcs -q
------ メッセージキュー --------
キー msqid 所有者 権限 使用バイト数メッセージ
0x6e03b930 0 hoge 666 0 0
0x7a03b92f 385 hoge 666 1152 48
$ ipcrm msg 385
リソースを削除しました
|
カーネルのヘルプメッセージ
冒頭で、カーネルのコンパイルオプションで、System V IPCを無効にするといくつかのアプリケーションは動作しなくなると言いましたが、 カーネルコンフィグ 時のIPCのヘルプメッセージには、"dosemuを使う場合は必要です"のように書かれています。なんとなく例が古さを感じますね......
データの表示方法
プログラムリストをコンパイル、実行したときにできたファイルを見るには、コマンド gnuplotを使うのが便利でしょう。Xが動作 している環境で、以下のように行ないます。
$ gnuplot
....
.... gnuplotのメッセージ
....
gnuplot> plot 'dat' w l
System V IPC関連のマンページ
一応、System V IPC関連の関数、コマンドのマンページの情報を載せておきます。 man ipcは、以外はセクションを付けなくても知りたい内容がでてきます。 ちなみに、
man 5 ipc
でなく、
man ipc
とすると、"標準ライブラリの実装者やカーネ ル・ハッカーのみがipc について知る必要がある。"などと書かれているのでちょっと引いてしまう人もいるでしょう。
マンページ情報
| 内容セクション |
名前 |
| System V プロセス間通信機構5 |
ipc |
| ftok(): IPC キーの生成3 |
ftok |
| 共有メモリ関連2 |
shmget,shmctl,shmat,shmdt |
| メッセージキュー関連2 |
msgget,msgctl,msgsnd,msgrcv |
| セマフォ関連2 |
semget,semctl,semop |
| IPC関連コマンド8 |
ipcs,ipcrm |
[プログラムリスト 1-1 : shm-common.h : ヘッダーファイル]
#ifndef __SHM_COMMON_H__
#define __SHM_COMMON_H__
#define KEYFILE_PATH "shm-write"
#define PROJ_CHAR (char)'x'
#define SHMSIZE (360)
#endif
|
[プログラムリスト 1-2: shm-write.c : 共有メモリ 書き込み側]
/* ------------------------------------------------------------
This program is one of the implementations for shared memory ;
writing side to shared memory
Written by Kazutomo Yoshii
------------------------------------------------------------ */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <math.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "shm-common.h"
static char char_sin( int d, int p )
{
return (char)( sin(((double)d/180.0 * M_PI) +
((double)p/180.0 * M_PI) ) * 127 ) ;
}
int main(int argc, char *argv[])
{
key_t key;
int shmid;
char *shmaddr;
int i;
/* generation of key */
key = ftok(KEYFILE_PATH,PROJ_CHAR);
if( key == -1 ) {
perror("ftok()");
return 1;
}
shmid = shmget(key, SHMSIZE, IPC_CREAT| IPC_EXCL| 0600 );
if( shmid == -1 ) {
perror("shmget()");
return 1;
}
shmaddr = shmat(shmid, NULL, 0);
if( shmaddr == (char*)-1 ) {
perror("shmat()");
return 1;
}
for( i=0; i<360; i++ ) {
shmaddr[i] = char_sin( i, 0 );
}
sleep(2);
if( shmctl( shmid, IPC_RMID, NULL) == - 1 ) {
perror("shmctl()");
return 1;
}
return 0;
}
|
[プログラムリスト 1-3: shm-read.c : 共有メモリ 読みだし側]
/* ------------------------------------------------------------
This program is one of the implementations for shared memory ;
reading side to shared memory
Written by Kazutomo Yoshii
------------------------------------------------------------ */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <math.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "shm-common.h"
int main(int argc, char *argv[])
{
key_t key;
int shmid;
char *shmaddr;
int i;
/* generation of key */
key = ftok(KEYFILE_PATH,PROJ_CHAR);
if( key == -1 ) {
perror("ftok()");
return 1;
}
shmid = shmget(key, SHMSIZE, 0 );
if( shmid == -1 ) {
perror("shmget()");
return 1;
}
shmaddr = shmat(shmid, NULL, SHM_RDONLY);
if( shmaddr == (char*)-1 ) {
perror("shmat()");
return 1;
}
for( i=0; i<360; i++ ) {
printf("%d\n", (int)(shmaddr[i]) );
}
return 0;
}
|
[プログラムリスト 2-1: mq-common.h : ヘッダーファイル]
[プログラムリスト 2-1: mq-common.h : ヘッダーファイル]
#ifndef __MQ_COMMON_H__
#define __MQ_COMMON_H__
#define KEYFILE_PATH "mq-server"
#define PROJ_CHAR (char)'z'
#define BUFLEN (128)
typedef struct {
long type; /* type of message */
size_t len; /* 0 <= length of buf < BUFLEN
: -1 means no more data */
char buf[BUFLEN];
} MsgBuf;
#endif
|
[プログラムリスト 2-2: mq-server.c : メッセージキューサーバー]
/* ------------------------------------------------------------
This program is one of the server implementations for message
queues.
Written by Kazutomo Yoshii
------------------------------------------------------------ */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <math.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include "mq-common.h"
static char char_sin( int d, int p )
{
return (char)( sin(((double)d/180.0 * M_PI) +
((double)p/180.0 * M_PI) ) * 127 ) ;
}
static int send_messages_wait(int msgid)
{
MsgBuf data[2];
int pos = 0;
int t;
data[0].len = data[1].len = -1;
data[0].type = 1;
data[1].type = 2;
t = 0;
pos=0;
while( t<360 ) {
data[0].buf[pos] = char_sin( t, 0 );
data[1].buf[pos] = char_sin( t, 90 );
pos ++;
if( pos == BUFLEN ) {
data[0].len = data[1].len = pos;
if( msgsnd(msgid,(struct msgbuf*)&(data[0]),sizeof(MsgBuf),0)==-1 ) {
perror("msgsnd()");
return 1;
}
if( msgsnd(msgid,(struct msgbuf*)&(data[1]),sizeof(MsgBuf),0)==-1 ) {
perror("msgsnd()");
return 1;
}
pos = 0;
}
t++;
}
if( pos > 0 ) {
data[0].len = data[1].len = pos;
if( msgsnd(msgid,(struct msgbuf*)&(data[0]),sizeof(MsgBuf),0)==-1 ) {
perror("msgsnd()");
return 1;
}
if( msgsnd(msgid,(struct msgbuf*)&(data[1]),sizeof(MsgBuf),0)==-1 ) {
perror("msgsnd()");
return 1;
}
}
/* to inform 'no more data' to client */
data[0].len = data[1].len = -1;
if( msgsnd(msgid,(struct msgbuf*)&(data[0]),sizeof(MsgBuf),0)==-1 ) {
perror("msgsnd()");
return 1;
}
if( msgsnd(msgid,(struct msgbuf*)&(data[1]),sizeof(MsgBuf),0)==-1 ) {
perror("msgsnd()");
return 1;
}
/* wait for client */
printf("waiting for client terminated\n");
data[0].len = data[1].len = -1;
for(;;) {
struct msgbuf rcvbuf;
if( msgrcv(msgid, &rcvbuf, sizeof(struct msgbuf),
1, IPC_NOWAIT) != -1 ) {
printf("client terminated : type %d\n",1);
data[0].len = 0;
}
if( msgrcv(msgid, &rcvbuf, sizeof(struct msgbuf),
2, IPC_NOWAIT) != -1 ) {
printf("client terminated : type %d\n",2);
data[1].len = 0;
}
if( data[0].len==0 && data[1].len==0 ) break;
}
return 0;
}
int main(int argc, char *argv[])
{
key_t key;
int msgid;
int rc;
/* generation of key */
key = ftok(KEYFILE_PATH,PROJ_CHAR);
if( key == -1 ) {
perror("ftok()");
return 1;
}
msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666 ) ;
if( msgid == -1 ) {
msgid = msgget(key, IPC_EXCL | 0666 ) ;
if( msgid == -1 ) {
perror("msgget()");
return 1;
}
}
rc = send_messages_wait(msgid);
if( rc ) return 1;
/* close */
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
|
[プログラムリスト 2-3: mq-server.c : メッセージキュークライアント]
/* ------------------------------------------------------------
This program is one of the client implementations for message
queues.
Written by Kazutomo Yoshii
------------------------------------------------------------ */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include "mq-common.h"
static MsgBuf data;
static int rcv_messages_print(int msgid, int type)
{
int i;
int rc;
struct msgbuf sndbuf;
for(;;) {
if( (rc=msgrcv(msgid,(struct msgbuf *)&data,
sizeof(MsgBuf),type,0) ) == -1 ) {
perror("msgrcv()");
return 1;
}
if(data.len == -1 ) break;
for( i=0; i< data.len; i++ ) {
printf("%d\n", (int) (data.buf[i]));
}
}
/* to inform disconnection to server */
sndbuf.mtype = type;
rc = msgsnd(msgid, &sndbuf ,sizeof(struct msgbuf), 0 );
return 0;
}
int main(int argc, char *argv[])
{
key_t key;
int msgid;
int rc;
int type;
if( argc<2 ) {
printf("Usage : %s message_type\n", argv[0]);
return 1;
}
type = atoi( argv[1] );
/* generation of key */
key = ftok(KEYFILE_PATH,PROJ_CHAR);
if( key == -1 ) {
perror("ftok()");
return 1;
}
/* connect to the message queu */
msgid = msgget(key, IPC_EXCL);
if( msgid == -1 ) {
perror("msgget()");
return 1;
}
rc = rcv_messages_print(msgid,type);
if( rc ) return 1;
return 0;
}
|