(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

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

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

というわけで今回実装するシステムコールは以下の4つ.
  • kz_getid() ... スレッドIDの取得
  • kz_chpri() ... スレッド優先度の変更
  • kz_send() ... 別スレッドにメッセージを送信する
  • kz_recv() ... 別スレッドからのメッセージを受信する
まずKOZOSでのシステムコール追加なのだけど,システムコールを追加するには以下の手順が必要になる.
  • 外部公開用のサービス関数を kozos.h に追加
  • syscall.c にサービス関数の本体を追加
  • syscall.h の kz_syscall_type_t の定義に新システムコールを追加
  • syscall.h の kz_syscall_param_t の定義に新システムコール用のパラメータを追加
  • thread.c にシステムコールの処理を追加.
  • thread.c の syscall_proc() にシステムコール処理の呼び出しを追加.
で今回の4つのシステムコールなのだけど,kz_getid()とkz_chpri()は単体で動作するシステムコールなので,実はそれほど難しくはない.問題は,kz_send() と kz_recv() だ.
  • kz_send()
    • 送信先のスレッドが kz_recv() で受信待ち状態ならば,受信処理を行って wakeup する必要がある.
    • そうでないなら送信先スレッドのメッセージキューにメッセージを繋げる.
  • kz_recv()
    • メッセージキューにメッセージがすでに存在するならば,受信処理を行う.
    • そうでないならスリープしてメッセージ受信待ちに入る.
というように,送受信の相手の状態に応じて処理内容が変わってくる.

実装したソースは以下のようになる.ちなみに以下は前回からの修正の差分.上で書いた,システムコールの新規追加時の必要作業と見比べてほしい.では,システムコールの追加について,上記の差分に対して修正内容を説明していこう.

まず,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, &param);
return param.un.wakeup.ret;
}
+
+int kz_getid()
+{
+ kz_syscall_param_t param;
+ kz_syscall(KZ_SYSCALL_TYPE_GETID, &param);
+ 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, &param);
+ 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, &param);
+ 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, &param);
+ 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 では,システムコールの戻り値は外部変数によって返していた.しかしこれで,実は以下の問題が出たのだ.
  • 外部変数を経由して戻り値を返すので,戻り値はスレッド単位でなくKOZOS に1個,という管理になる.
  • メッセージ通信の実装では,kz_recv() をすると実際にメッセージが送信されてくるまで受信待ちでスリープする.
  • 別のスレッドが kz_send() によりメッセージを送信すると,kz_recv() しているスレッドには wakeup がかかり,動作再開する.
  • この際 kz_send() した側のスレッドは,kz_send() の戻り値として,戻り値格納用の外部変数を参照することになる.
  • しかし kz_recv() した側のスレッドも,kz_recv() の戻り値として戻り値格納用の外部変数を参照することになる.しかしここには kz_send() の戻り値が格納されている.結果として kz_recv() したスレッドは,戻り値として正しい値を受け取ることができない.
ということで,戻り値はパラメータ領域の中に埋め込んで,スレッド単位で管理できるようになっている.

次に,順番はちょっと逆になるが,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 = &current->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_membuf をひとつ,malloc()によって確保する.さらにパラメータを保存する.
  • kz_membuf を送信先のスレッドのメッセージキューのお尻に接続する.
  • 送信先のスレッドをレディーキューに繋ぐ(putcurrent()).putcurrent()の戻り値がゼロの場合には,受信側は kz_recv() による受信待ちだったということなので,recvmsg()を呼び出してさらに受信処理を行う.
kz_recv() 実行時には thread_recv() が呼ばれる.ここでは以下の処理が行われている.
  • メッセージキューに何もない場合には受信待ちを行う必要があるので,putcurrent() を行わずにそのまま返ることでスリープ状態になる.
  • メッセージキューにメッセージがあった場合には,recvmsg()により受信処理を行い,putcurrent() でスレッドをレディーキューに繋ぐ.
  • recvmsg()ではメッセージキューの先頭からメッセージを取り出し,kz_recv() の戻り値としてデータのサイズを格納し,さらに受信するスレッドが kz_recv() に渡した引数(送信元IDやデータを指すポインタの格納先ポインタ)の指す先に,戻り値としてそれらの情報を格納する.


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 メンバにはとくに意味は無く,渡された数値をそのまま受信側に渡すだけだからだ.