(第3回)システムコールの追加(メッセージ通信とか)

2007/10/20

あなたは 人目のお客様です.

前回まででKOZOSのスレッドディスパッチ機能がだいたい動いたのだけど, システムコールでスレッドの起動と終了,スリープとwakeupしかないので さすがに寂しすぎる.ていうかこれではぜんぜん使いものにならん.

スレッドどうしが同期して動作できるように, とりあえずスレッド間通信の機能をもたせたい. というわけで今回は,スレッド間通信としてメッセージ通信を実装してみよう. ついでにスレッドIDの取得と優先度変更も実装してみる.

というわけで今回実装するシステムコールは以下の4つ.

まずKOZOSでのシステムコール追加なのだけど,システムコールを追加するには 以下の手順が必要になる. で今回の4つのシステムコールなのだけど, kz_getid()とkz_chpri()は単体で動作するシステムコールなので, 実はそれほど難しくはない. 問題は,kz_send() と kz_recv() だ. というように,送受信の相手の状態に応じて処理内容が変わってくる.

実装したソースは以下のようになる.

(2009/04/10 ライセンスに関する文書として,KL-01とLICENSEを追加. 詳しくは第43回を参照)

ちなみに以下は前回からの修正の差分. 上で書いた,システムコールの新規追加時の必要作業と見比べてほしい.

では,システムコールの追加について, 上記の差分に対して修正内容を説明していこう.

まず,kozos.h に外部公開用のサービス関数を追加している.

diff -ruN -U 10 kozos01/kozos.h kozos03/kozos.h
--- kozos01/kozos.h	Sun Oct 21 20:07:33 2007
+++ kozos03/kozos.h	Sun Oct 21 20:07:33 2007
@@ -4,15 +4,19 @@
 #include "configure.h"
 
 typedef int (*kz_func)(int argc, char *argv[]);
 
 /* syscall */
 int kz_run(kz_func func, char *name, int pri, int argc, char *argv[]);
 void kz_exit();
 int kz_wait();
 int kz_sleep();
 int kz_wakeup(int id);
+int kz_getid();
+int kz_chpri(int pri);
+int kz_send(int id, int size, char *p);
+int kz_recv(int *idp, char **pp);
 
 /* library */
 void kz_start(kz_func func, char *name, int pri, int argc, char *argv[]);
 
 #endif

kz_getid() は引数無しで自分自身のスレッドIDが返る. kz_chpri() は優先度を引数に取り,スレッドの優先度を切替える.

kz_send()は指定したスレッドIDのスレッドに,サイズとして数値をひとつ, データとしてポインタをひとつ送信する. kz_recv() では戻り値としてサイズ,さらに送信してきたスレッドのIDと, データを指すポインタが返ってくる. 送受信の方法をもうちょっと具体的に説明すると, 送信側では

int size;
char *data;
...
kz_send(size, data);
のようにすることで,サイズとデータ(へのポインタ)を送信する. 受信側では
int size, id;
char *data;
size = kz_recv(&id, &data);
のようにすることで,戻り値としてサイズが返され, 第1引数のポインタが指す先に送信元スレッドID, 第2引数のポインタが指す先にデータへのポインタが格納されて返ってくる. 第1引数,第2引数にはNULLを指定することでとくに値を取得しないことも可能だ. ただしデータの内容はコピーされるわけではなく, ポインタ値がそのまま渡されるだけだ.

システムコールの呼び出しにはそれなりの手順が必要(パラメータを用意して 自分自身に対して SIGSYS を送信する)だが,システムコール呼び出しのたびに こういったことをいちいち行うのは面倒なので,syscall.c でサービス関数が 用意されている. たとえばUNIXではシステムコールはたいてい引数を用意してからシステムコール用の ソフトウエア割り込みを行うことになるが,これらはアセンブラで書く必要があり, 定型の操作でもあるため,関数化されていて,ユーザは通常はその関数を呼び出せば いいことになる(アセンブラを知る必要は無い).これと同じようにシステムコールを 関数化している.

で,kozos.h ではこれらの関数を外部に対して公開している. つまり KOZOSを使う場合には,ユーザは kozos.h のみをインクルードすれば いいことになる.

サービス関数は syscall.c に書いてある.これが以下の部分だ.

diff -ruN -U 10 kozos01/syscall.c kozos03/syscall.c
--- kozos01/syscall.c	Sun Oct 21 20:07:33 2007
+++ kozos03/syscall.c	Sun Oct 21 20:07:33 2007
@@ -47,10 +47,44 @@
   return param.un.sleep.ret;
 }
 
 int kz_wakeup(int id)
 {
   kz_syscall_param_t param;
   param.un.wakeup.id = id;
   kz_syscall(KZ_SYSCALL_TYPE_WAKEUP, ¶m);
   return param.un.wakeup.ret;
 }
+
+int kz_getid()
+{
+  kz_syscall_param_t param;
+  kz_syscall(KZ_SYSCALL_TYPE_GETID, ¶m);
+  return param.un.getid.ret;
+}
+
+int kz_chpri(int pri)
+{
+  kz_syscall_param_t param;
+  param.un.chpri.pri = pri;
+  kz_syscall(KZ_SYSCALL_TYPE_CHPRI, ¶m);
+  return param.un.chpri.ret;
+}
+
+int kz_send(int id, int size, char *p)
+{
+  kz_syscall_param_t param;
+  param.un.send.id = id;
+  param.un.send.size = size;
+  param.un.send.p = p;
+  kz_syscall(KZ_SYSCALL_TYPE_SEND, ¶m);
+  return param.un.send.ret;
+}
+
+int kz_recv(int *idp, char **pp)
+{
+  kz_syscall_param_t param;
+  param.un.recv.idp = idp;
+  param.un.recv.pp = pp;
+  kz_syscall(KZ_SYSCALL_TYPE_RECV, ¶m);
+  return param.un.recv.ret;
+}
syscall.c では自動変数としてパラメータを用意して kz_syscall() を呼び出す. kz_syscall() の内部では kill() によって SIGSYS を自分自身に発行することで, ソフトウエア割り込みもどきの処理を行っている.

syscall.h には,kz_syscall_type_t に新システムコールを追加し,さらに kz_syscall_param_t の定義に新システムコール用のパラメータを追加する必要がある.

diff -ruN -U 10 kozos01/syscall.h kozos03/syscall.h
--- kozos01/syscall.h	Sun Oct 21 20:07:33 2007
+++ kozos03/syscall.h	Sun Oct 21 20:07:33 2007
@@ -2,20 +2,24 @@
 #define _KOZOS_SYSCALL_H_INCLUDED_
 
 #include "kozos.h"
 
 typedef enum {
   KZ_SYSCALL_TYPE_RUN,
   KZ_SYSCALL_TYPE_EXIT,
   KZ_SYSCALL_TYPE_WAIT,
   KZ_SYSCALL_TYPE_SLEEP,
   KZ_SYSCALL_TYPE_WAKEUP,
+  KZ_SYSCALL_TYPE_GETID,
+  KZ_SYSCALL_TYPE_CHPRI,
+  KZ_SYSCALL_TYPE_SEND,
+  KZ_SYSCALL_TYPE_RECV,
 } kz_syscall_type_t;
 
 typedef struct {
   union {
     struct {
       kz_func func;
       char *name;
       int pri;
       int argc;
       char **argv;
@@ -27,16 +31,34 @@
     struct {
       int ret;
     } wait;
     struct {
       int ret;
     } sleep;
     struct {
       int id;
       int ret;
     } wakeup;
+    struct {
+      int ret;
+    } getid;
+    struct {
+      int pri;
+      int ret;
+    } chpri;
+    struct {
+      int id;
+      int size;
+      char *p;
+      int ret;
+    } send;
+    struct {
+      int *idp;
+      char **pp;
+      int ret;
+    } recv;
   } un;
 } kz_syscall_param_t;
 
 void kz_syscall(kz_syscall_type_t type, kz_syscall_param_t *param);
 
 #endif
kz_syscall_type_t の定義はシステムコール発行時のシステムコール番号であり, 実際にはシステムコールの呼び出しには kz_getid() などのサービス関数を 用いるので,ユーザが知る必要は,まあ,無い.ただ追加すればいいだけのものだ.

kz_syscall_param_t の定義では,新システムコールで必要になるパラメータを 追加する.パラメータの個数や種類はシステムコールごとに異なるので, 共用体を使ってポインタによってシステムコール処理側に渡すような 構造になっている.

ここで,システムコールのパラメータ中に,戻り値の返却用として ret という メンバがあることに注目してほしい. システムコールの結果というか戻り値は,このメンバを用いて呼び出し側に通知する. パラメータはシステムコールの呼び出し時に,呼び出し用のサービス関数によって (自動変数として)スタック上に確保されるので,戻り値も同じ場所に確保されることに なる.

システムコールの呼び出しには kill() によるソフトウエア割り込み(のようなもの) を利用しているので,通常の関数コールのように戻り値を返すことができない. この対策として実は初期の KOZOS では,システムコールの戻り値は外部変数によって 返していた.しかしこれで,実は以下の問題が出たのだ.

ということで,戻り値はパラメータ領域の中に埋め込んで, スレッド単位で管理できるようになっている.

次に,順番はちょっと逆になるが,thread.h の修正について説明する. システムコールの追加の際,thread.h も修正を行う必要は必ずしもないのだが, 今回はメッセージ通信のためにメッセージキューと, kz_recv() による受信待ちの際の引数の保存領域が追加されている.

diff -ruN -U 10 kozos01/thread.h kozos03/thread.h
--- kozos01/thread.h	Sun Oct 21 21:12:59 2007
+++ kozos03/thread.h	Sun Oct 21 21:12:59 2007
@@ -5,31 +5,40 @@
 #include 
 #include 
 
 #include "kozos.h"
 #include "syscall.h"
 
 #define THREAD_NUM 16
 #define PRI_NUM 32
 #define THREAD_NAME_SIZE 16
 
+typedef struct _kz_membuf {
+  struct _kz_membuf *next;
+  int id;
+  int size;
+  char *p;
+} kz_membuf;
+
 typedef struct _kz_thread {
   struct _kz_thread *next;
   char name[THREAD_NAME_SIZE + 1];
   struct _kz_thread *id;
   kz_func func;
   int pri;
   char *stack;
 
   struct {
     kz_syscall_type_t type;
     kz_syscall_param_t *param;
   } syscall;
+
+  kz_membuf *messages;
 
   struct {
     jmp_buf env;
   } context;
 } kz_thread;
 
 extern kz_thread *current;
 
 #endif
メッセージは構造体 kz_membuf によって管理され, 構造体 kz_thread の messages メンバにリンクリストとして連結される.

最後に,thread.c へのシステムコールの処理の追加だ. ここはひとつひとつ見ていこう.

static int thread_getid()
{
  putcurrent();
  return (int)current->id;
}
まず kz_getid() の処理は簡単だ.自分自身のIDを返すだけだ. さらに前回に説明したとおり,putcurrent()によってスレッドを レディーキューに繋げている.これをやらないと kz_getid() の呼び出し後に スレッドがスリープしてしまうことになる.

次に優先度の変更だ.

static int thread_chpri(int pri)
{
  int old = current->pri;
  if (pri >= 0)
    current->pri = pri;
  putcurrent();
  return old;
}
引数として渡された値に優先度を変更するだけなのだが, 以前の優先度を戻り値として返し, さらに優先度として負の値が渡された場合には,優先度の変更は行わないように なっている.つまり自身の優先度が知りたいだけならば,
pri = kz_chpri(-1)
を実行すればいいようになっている.

次に kz_send(),kz_recv() によるメッセージの送受信だ.

static void recvmsg()
{
  kz_membuf *mp;

  mp = current->messages;
  current->messages = mp->next;
  mp->next = NULL;

  current->syscall.param->un.recv.ret = mp->size;
  if (current->syscall.param->un.recv.idp)
    *(current->syscall.param->un.recv.idp) = mp->id;
  if (current->syscall.param->un.recv.pp)
    *(current->syscall.param->un.recv.pp)  = mp->p;
  free(mp);
}

static void sendmsg(kz_thread *thp, int id, int size, char *p)
{
  kz_membuf *mp;
  kz_membuf **mpp;

  current = thp;

  mp = (kz_membuf *)malloc(sizeof(*mp));
  if (mp == NULL) {
    fprintf(stderr, "cannot allocate memory.\n");
    exit(1);
  }
  mp->next = NULL;
  mp->size = size;
  mp->id = id;
  mp->p = p;
  for (mpp = ¤t->messages; *mpp; mpp = &((*mpp)->next))
    ;
  *mpp = mp;

  if (putcurrent() == 0) {
    /* 受信する側がブロック中の場合には受信処理を行う */
    recvmsg();
  }
}

static int thread_send(int id, int size, char *p)
{
  putcurrent();
  sendmsg((kz_thread *)id, (int)current, size, p);
  return size;
}

static int thread_recv(int *idp, char **pp)
{
  if (current->messages == NULL) {
    /* メッセージが無いのでブロックする */
    return -1;
  }

  recvmsg();
  putcurrent();
  return current->syscall.param->un.recv.ret;
}
kz_send() 実行時には thread_send() が呼ばれる. thread_send() からは sendmsg() が呼び出されている (これを関数化しているのは,今後タイマやシグナル受信時の通知処理を実装する際に, メッセージ送信を行う必要があるので,メッセージの送信部分だけ関数化して おきたかったため.これらはまた今後説明する). sendmsg()では,以下の処理を行っている. kz_recv() 実行時には thread_recv() が呼ばれる. ここでは以下の処理が行われている.

kz_recv()で受信待ちには入った際には,他スレッドがメッセージを送信してくるまで, 受信処理を行うことはできない.で,メッセージが送信されると,受信処理として 送信元スレッドIDの格納と,データへのポインタの格納が行われる.

ところで,kz_send()の実行時には, 引数としてサイズとデータへのポインタを渡していた. kz_recv()では,それらを戻り値として返しているだけである. つまり kz_send() によりデータ(へのポインタ)を渡したとしても, kz_recv() する側にそのデータがコピーされて渡されるわけではない. たとえば kz_recv() 側でデータを書き換えたりすると,kz_send() した側では データが突然書き変わって見えるようになる.

このため malloc() によって獲得したデータを渡すような場合には注意が必要だ. 基本的には kz_send() 側で malloc() を行い,kz_recv() 側で free() を行う べきだろう.また kz_send() 側では,kz_send() の後にはそのデータを参照しては いけないことになる.kz_recv() 側でいつ free() されるかわからないからだ. (それらの注意点をどうしても破る必要があるならば,スレッドの優先度を工夫して, データ参照される前にfree()されることは絶対に無いような優先度設計にするなどの 考慮が必要)

またサイズは kz_send() の引数として渡された値を kz_recv() の戻り値として 返すだけなので,数値としての意味はとくに無い. たぶんデータのサイズを渡したいことが多いだろうから size というパラメータ名に しているだけなので,任意の数値を渡したり,不要ならばゼロとか負の値に してしまうことも可能だ.

さて,システムコールの処理用関数の呼び出しは以下のようになる.

 static void syscall_proc()
 {
   /* システムコールの実行中にcurrentが書き換わるのでポインタを保存しておく */
   kz_syscall_param_t *p = current->syscall.param;
 
   getcurrent();
 
   switch (current->syscall.type) {
   case KZ_SYSCALL_TYPE_RUN:
     p->un.run.ret = thread_run(p->un.run.func, p->un.run.name, p->un.run.pri,
@@ -153,20 +229,32 @@
     thread_exit();
     break;
   case KZ_SYSCALL_TYPE_WAIT:
     p->un.wait.ret = thread_wait();
     break;
   case KZ_SYSCALL_TYPE_SLEEP:
     p->un.sleep.ret = thread_sleep();
     break;
   case KZ_SYSCALL_TYPE_WAKEUP:
     p->un.wakeup.ret = thread_wakeup(p->un.wakeup.id);
+    break;
+  case KZ_SYSCALL_TYPE_GETID:
+    p->un.getid.ret = thread_getid();
+    break;
+  case KZ_SYSCALL_TYPE_CHPRI:
+    p->un.chpri.ret = thread_chpri(p->un.chpri.pri);
+    break;
+  case KZ_SYSCALL_TYPE_SEND:
+    p->un.send.ret = thread_send(p->un.send.id, p->un.send.size, p->un.send.p);
+    break;
+  case KZ_SYSCALL_TYPE_RECV:
+    p->un.recv.ret = thread_recv(p->un.recv.idp, p->un.recv.pp);
     break;
   default:
     break;
   }
   return;
 }
これは syscall_proc() に単に追加しているだけだ.

ここまでが今回の機能追加のための修正内容なのだけど, 次にサンプルプログラムによって動作確認してみよう. 今回用意したサンプルプログラムは以下.

前回同様,コンパイルして実行してみよう. まずはコンパイル.上記の main.c を KOZOS のディレクトリに置いて make を行う.
% make 
cc -c thread.c  -g -Wall -static
cc -c syscall.c  -g -Wall -static
cc -c main.c  -g -Wall -static
cc thread.o syscall.o main.o -o koz  -g -Wall -static
% 
で,これが実行結果.
% ./koz 
main start (08062da0)
main start2 pri(1)
func1 start
main start3 pri(3)
message sending
func1 recv 18 "message sample 1."
func1 send
func2 send
func2 start
func2 recv 18 "message sample 2."
func1 recv 0 "message sample 3."
% 
まず main.c を簡単に説明するが,kz_run() により "func1", "func2" という2つのスレッドを起動する. これらのスレッドは,kz_recv() によりメッセージ受信待ちに入り, メッセージを受信したらその内容を表示する,という動作を行う. "func2" はメッセージ内容の表示後,さらに "func1" に対してメッセージを送信する.

実行結果を見てみよう. 最初に "main start" というメッセージが出力され,スレッドIDとして 0x08062da0 という値が表示されている.スレッドIDは kz_getid() により取得した値だ. さらに kz_run() により "func1","func2" というスレッドを起動し, 自身の優先度を kz_chpri() により1→3に変更している.

ここで最初に起動した "main" スレッドの優先度は1, "func1"の優先度は2,"func2"の優先度は4である. このため kz_run() によるスレッド生成を行っても,優先度が1である "main" スレッドがそのまま走行する.しかし kz_chpri() により優先度が3になると, "main" よりも "func1" のほうが優先順位が高くなるので,その直後から動作は "func1" に切り替わる.このため "func1 start" というメッセージが直後に 出力されている("main start3 pri(3)"のメッセージよりも先に表示されていることに 注意."func1"のほうが優先順位が高いので,"main" によるメッセージ出力よりも先に "func1 start" が表示されている).

"func1" は動作を開始すると,kz_recv() による受信待ちに入る.このためスリープ するので,動作は再び "main" に戻る."main" は kz_send() により "func1"と"func2"にメッセージ送信を行う.

"func1","func2"は"main"から送られてきたメッセージを受信し内容を表示 するのだが,ここでも各スレッドの優先度が効いている. まずは "main" は "func1" に対して kz_send() によりメッセージを送信して いるのだが,"func1" のほうが優先順位が高いので,kz_send()の実行直後に 動作は "func1" に切り替わり,"func1" が受信を行って,メッセージの内容を 出力している."main" が "func1 send" というメッセージを出力するのはその後 ということになる.

さらに "main" は "func2" に対してメッセージを送信し, "func2 send" というメッセージを出力している. "func2" はここでようやくスレッドの動作が開始し, "func2 start" という文字列を表示した後に "main" からのメッセージを受信して 出力している.これらの送信側,受信側の動作の順番は "func1" のときとは逆に なっているが,これらは優先度関係の違いによるものだ. さらに "func2" は "func1" にメッセージを投げ,"func1" 側で再びメッセージが 表示されている.

"func1" 側の受信は,"func1" が kz_recv() により受信待ち状態に入ってから メッセージが送信されてくるが, "func2" 側の受信は,まず "main" の kz_send() によりメッセージが既に送信されて いる状態で,"func2" が kz_recv() による受信を行うことになる. 前者は kz_recv() によるスリープとその後の wakeup 処理, 後者はメッセージを受信されるまで保持しておくためのキューイング処理が 必須になってくる.どちらも,正常に動作しているようだ.

ちなみにメッセージとして投げる文字列は,

  char p[] = "message sample 1."; /* スタック上の文字列 */
  ...
  kz_send(id1, 18, p);
のようにしてスタック上に確保したものと,
  kz_send(id2, 18, "message sample 2.");
のようにして,文字列リテラルとして静的に確保したものの両方を使っているが, どちらも問題なく表示できている.

あと "func2" が最後に "func1" にメッセージを投げるときは実は size をゼロとして 投げているのだが,"func1" ではメッセージを正常に受信して, kz_recv() の戻り値はゼロとして表示している. kz_send() の size メンバにはとくに意味は無く,渡された数値を そのまま受信側に渡すだけだからだ.


メールは kozos(アットマーク)kozos.jp まで