Super Technique 講座

シグナルとコールバック

この文書ではまず、関数ポインタとその型チェックについて述べた後で、UNIXのシグナル機能について解説する。そして、ウィンドウシステムのプログラミングで多用される「コールバック」について解説する。

ちなみにシグナルの機能は本質的には UNIX に固有である。他のOSにもないわけではないが、その異同については筆者は関知しない。また、UNIXでのシグナルの実装については Linux を基準に解説をしていく。UNIX シグナルは、実は具体的な実装において大変差がある機能なのだが、一応 POSIX で「こう実装しなさい」という風に決まってはおり、後発の Linux は比較的マジメにそれを実装しているので、まあ、Linux を基準にするのが無難というものであろう(ユーザ比の問題を別にしても)。勿論、伝統的に重要ないわゆる「SysVシグナル」「BSDシグナル」についてもしっかりと解説しているが、もはや「POSIXシグナル」に移行すべきであると、筆者は主張する。まあ、こういうノリの文書である。適度にユーモアも交えながら、解説しよう。(こんなに長くなるなんて思わなかったぜ!)


関数ポインタの型チェック

UNIX&Cの初心者が混乱する内容として、「関数ポインタ」がある。関数ポインタは単に関数(名)をそのまま「ポインタ(要するにその関数のエントリのアドレス)」として解釈するものであり、ホントは複雑なものではないのだが、一見複雑に見えるキャストが絡むために、敬遠される雰囲気がある。

C言語ではポインタの型チェックを行うが、これはC言語では数少ないアセンブリレベルの対応物を持たない「言語の機能」である。つまり、ポインタ代入に関して、厳格なチェックを行うべきである、という経験則がポインタの型チェックを言語機能に採り入れさせたのである。しかし、関数ポインタの場合には、これが一種の複合型として扱われる。つまり「戻り値」の型と「引数」の型の両方が完全に一致していないと、代入が不可となる。このための型宣言は次の通りである。

int sub( char *s ) {
     /* 処理内容 */
}

.................

    int ret;              /* int 型変数の宣言 */
    int (*func)(char *);  /* int 型を返し、char * を引数とする関数ポインタの宣言 */

    func = sub;           /* 関数ポインタへの代入 */
    ret = func( "test" ); /* 関数ポインタを介した関数呼び出し */
    /* ret = (*func)("test"); でも可。こっちの方が関数ポインタ経由であることを
       明示できるので、筆者は好き */

さて、これで見る限り、ちょっと記法がオシャレだが、別に難解なものではない。恐怖感が少しは薄らいだかな?


ハードウェア割り込みの処理

UNIXには「シグナル」という機能がある。このシグナル機能は大変便利なものであるだけではなく、一種のプログラミングモデルでもある。シグナル機能自体は一種のプロセス間通信の機能であり、主としてOSとアプリケーションのやりとりに使われるが、アプリケーション同士の通信にだって使えるので、大変有用な機能である。

しかし、この「シグナル」は一種の並行プログラミングの機能である。つまり、同時に並行して動作しているプロセスの間でやりとりが行われるのだから、タイミングが重要であることは言うまでもない。これが実はかなり根深い問題を含んでおり、この問題に対する対処から、実はUNIXでは3通りの対応(いわゆるSysVシグナル、BSDシグナル、POSIXシグナル)があり、問題を複雑化させている。これについては後でじっくりと触れる。

そもそも「シグナル」のアイデアは、ハードウェア・プログラミングから来ている。いわゆる「ハードウェア割り込み」の制御から着想された機能である。ハードウェア割り込みとはCPUの INTRピンに対する入力の変動があった時に、CPUが自動的に、今まで行っていた処理を放棄し、制御を特定アドレスに移す機能である。しかし、その特定アドレスの処理が終ったら、何事もなかったかのように、先程までの処理を継続するわけで、本来の処理の側では割り込みがあったことを「気づかない」。

もう少し、実例によって解説しよう。皆さんはキーボードを叩くと、コンソールに字が表示されるのを「アタリマエ」だと思っているに違いない。しかし、これはOSが「頑張って」実現している機能であり、これに「ハードウェア割り込み」が深く関与している。手順はこうである。

  1. CPUは何かの処理をしている。とりあえず、ユーザプログラムの処理としよう。
  2. ここでキーボードが押される。キーボードは PIC(Programable Interrupt Controller)と呼ばれるハードウェアに接続されているので、PICがキーボード入力を検知する。
  3. PICはそれがキーボードの番号であることを認知し、キーボードであることを知る。
  4. PICはCPUの INTR ピンに接続されており、この INTR ピンの状態を変更する(実は伝統的に INTR ピンは負論理のため、普通は ON 状態なのが OFF 状態になる)。
  5. そして、PICはデータバスにキーボードに対応する番号を流す。このキーボードに対応する番号というのが、いわゆる「IRQ(Interrupt ReQuest)」である。いわゆる「Plug&Play」が普及するまでは、この衝突に悩んだアレである。
  6. CPUは INTR ピンの状態変化を検知し、割り込み制御ロジックが働き出す。データバスの状態を読み込んで、それがキーボードの番号であることを知る。
  7. CPUは「物理メモリ上の特定のアドレス(とか特定のレジスタによってポイントされるアドレス)+キーボードの番号(IRQ)」で指定されるメモリ内容のアドレスを取得する。つまり、固定アドレスか特定のレジスタでポイントされるアドレスからは、IRQに応じたその処理プログラムのエントリポイントが、テーブルの形で入っているのである。これを「割り込みベクタ」と通称する。
  8. そして、ユーザプログラムの処理サイクルが問題ない時点で、システムスタックにユーザプログラムの次の行を積む(また実行状態が変わるのはまずいので、一般にフラグレジスタの値も積んでおく) 。
  9. そして制御をそのIRQに対応する割り込みベクタに移す。割り込みベクタのキーボード処理では、キーボードコントローラと通信して「どのキーか?」の情報を取得する。
  10. 一般に割り込みベクタの処理は短時間で済まないとマズいものである。だから、普通はこの「どのキーか?」の情報を単にFIFOバッファに保存するくらいの処理しかしない。
  11. どのプロセスに送られるべきか、というような処理は割り込みベクタの担当ではない。マルチタスクOSならば、多分このキー入力を適切なプロセスのキーボード入力バッファに分配するカーネルの処理のスケジュールを上げる位のことである。
  12. 割り込みベクタの処理は Intel CPUの場合「IRET」命令によって終了する。「IRET」命令は「RET(通常の関数から戻る)」と似ており、スタックから戻り番地を取得して制御を移す。ただし、フラグ状態(これは割り込みベクタの処理で変わっている可能性がある)も同時にスタックから回復する。まあ、一般にその他のレジスタの値を保存して回復するのは割り込みベクタの処理の責任になっていることが多い。
  13. そうすると、途中で放棄されたユーザプログラムの処理は何事もなかったかのように処理が継続される。そりゃすべてのレジスタはそのままであるから... ユーザプログラムのレベルでは割り込みがあったことさえ気づかない。
  14. 適当なスケジュールで、システムのキーボードFIFOバッファから、その入力を受け取ることが適切であるコンテキストの、プロセスのキーボードバッファにキーボード入力が転送される(まあ、ここらへんはOSにもよる)。
  15. 適当なユーザプログラムがキーボード入力を読み込むシステムコール(read)を発行する(ひょっとしたらユーザプログラムはXサーバかも知れない)。そうすると、read システムコールの結果としてキーボード入力内容が得られる。
  16. もしXサーバがキーボード入力を受け取ったのならば、Xサーバはキーボード入力をXのイベントのかたちに変えて、適切なプロセスにイベントとして送る。

けっこう大変なことをハードウェア&OSはしているわけだ。それもしっかりとした連係プレーである。この連係プレーの基礎となっているのが「ハードウェア割り込み」である。

ではもし「ハードウェア割り込み」がなかったとしたら、どうなるだろう? OSは適当な時間間隔で、キーボードコントローラに「キーボード入力があったの?」と尋ねて回らなくてはならない。これを「ポーリング」と呼ぶが、一般に不効率である。それよりも、「もしキーボード入力があったら教えてね!」とあらかじめ頼んでおいて、キーボード入力があったときに「ハードウェア割り込み」のかたちで通知を貰う方がずっと効率が良い。そのために、この「ハードウェア割り込み」は太古の昔からCPUの設計の中に組み込まれている(マイクロプロセッサだって Intel 8008 の頃からある!)。


UNIXシグナル

さて、「ハードウェア割り込み」が大変便利なものであることは御理解頂けたかと思う。これはあまりに便利なので、このやり方がOSの中で全面的に採用されたのである。なぜならば、すべての「出来事」はあらかじめ予定された通りに起きるものではなくて、思っても見なかったタイミングで起きるものである(ちょっと哲学)。だから、「ハードウェア割り込み」のように、「思ってもみないタイミングで起きること」をそれを処理するプログラムに処理させればイイな、と考えるのは自然である。つまり、「非同期なイベントを処理するイベントドリブン(イベント駆動型)プログラミング」のアイデアである。

たとえば、コマンドラインのOSの場合には、「Ctrl+C」のような有無を言わさずにプログラムの実行を停止する処理があった方が良いに決まっている。UNIXではそのプログラムの起動端末から「Ctrl+C」を押してやると(バックグラウンド実行でない限り)、そのプログラムが停止する。この「Ctrl+C」が押されるタイミングは誰も知らない。そのため、プログラムが行を実行するごとに「Ctrl+Cが押されたか?」をチェックする(ポーリング)ようではお話にならない。ここは非同期で「Ctrl+Cが押された!」ことを通知し、その通知に従ってプログラムを停止する仕組みを作らなくてはならないのだ。(実は MS-DOS は実に情けないことをしていた... MS-DOS では Ctrl+C が押されたかどうかをチェックするのは、MS-DOSシステムコールを呼び出した時だけだった。だからシステムコールを実行しないループに関しては、Ctrl+Cが効かなかった。ホントに情けない...)

UNIX ではこの問題に正面から取り組んだ。非同期で起きる可能性のあるイベントについては、「シグナル」という仕組みを作って、それを非同期にシグナルを受け取って処理できるようにしたのだ。この非同期で起きる可能性のあるイベントには次のようなものがある。

SIGINT
Ctrl+C が押された。デフォルトは終了。
SIGALRM
アラームタイマーの時間満了。デフォルトは無視。
SIGCHLD
そのプロセスが起動した子プロセスが終了した。デフォルトは無視。
SIGFPE
浮動小数点演算のエラー及び整数の0による割算の検出。デフォルトは終了。
SIGSEGV
メモリアクセスについて違反があった。いわゆる「セグメンテーションフォルト」が起きたのである。当然デフォルトは終了。
SIGTSTP
Ctrl+Z が押された。デフォルトはプロセスを一時停止する。いわゆる「バックグラウンドに回す」である。
SIGCONT
「バックグラウンドに回った」プロセスを再開する。シェルで「fg」と叩くとかね。

これらはユーザの気まぐれや、バグなどによって「いつ起きる」ということのまったく予測がつかない「イベント」である。だから、これらをいちいちチェックしていたのではまったく「身が持たない」。だから、あらかじめ、「このイベントが起きたらこうする!」ということを登録できておいたらいい。勿論デフォルト動作が適切にあらかじめ定められており、特にそれに文句がなければ何もしなくて良い、というのが上出来なアイデアである。だから、すべてに対してデフォルト動作があり、特にそれが気に入らない時に、別な処理を設定できるという風にシグナルは定められたのである。

このシグナルというアイデアは上出来である。だから、UNIXのOS開発者たちは、次のようなアイデアの拡張も行った。まず特に「プログラムの停止」に何段階かのレベルを与えた。

SIGINT
Ctrl+C による終了。これはプロセスで独自に再定義できるし、している場合が多い。
SIGTERM
プログラムは速やかに終了せよ!と勧告する。これもプロセスで独自に再定義できる。ただし、子プロセスがある場合には、そのプロセスが子プロセスも終了させることを想定している。
SIGQUIT
終了をデフォルト動作としている。いちおうプロセスで独自に再定義できるが、すべきではないだろう。
SIGKILL
これは最後の手段である。SIGQUIT はプロセスで再定義できないし、即刻のプロセス終了を引き起こす。もし、終了しなければ、OSのバグである!

このようなシグナルは、上に述べたものを含めて(SIGSEGV とかだって...)すべてシェルコマンドの kill によって生成できる。つまり、

%  ./sig &
[3] 15248
% kill 15248
[3]-  Terminated              ./sig

のように、この場合はデフォルトなので、SIGTERM がプロセス番号 15248 である ./sig に送られて、デフォルト動作として終了する。この kill コマンドにはいろいろなオプションもあるが、特に重要なのは、次のようにして任意のシグナルを送れることである。

% kill -KILL 15248

この -KILL は要するに「-」の後に「SIGKILL」の冗長な「SIG」を取った名前をつけると、SIGKILL がプロセス番号 15248 に送られるのである。この要領ですべてのシグナルを送ることができ、シグナルハンドラのデバッグもできる。また、signal.h(Linux だと実際には asm/signal.h)で定義されているシグナル名の番号を指定することもできるが、これで有名なのは「kill -9」(SIGKILL)だけである。(古手のUNIX管理者がやるんだな、これ。「9it」で語呂が合う...)

また、子プロセスを起動しているプログラムの場合には、親プログラムのプロセス番号を負にして kill を起動すると、それが「プロセスグループ」の番号である(プロセスグループはホントはセッションとの区別をするとなると難しいが、大まかに言えばパイプや fork(2) された親を共有するプロセスのグループのこと)と解釈されて、子プロセスに至るまですべてにそのシグナルが送られる。お茶目なことに、「-番号」はとりあえずシグナル番号として解釈されるので、常にシグナル名か番号を指定しなければならない。次の通り。

% kill -TERM -15248

さらにシグナルの便利さは、次のようなアプリケーション用のシグナルさえ ANSI標準に追加することになる。

SIGPROF
プロファイラ(関数の実行速度を計測するツール)用。
SIGTRAP
デバッガが、ブレークポイントに被デバッグプログラムが達したことを知るために使う。
SIGUSR1, SIGUSR2
用途を限定しない、アプリケーション用のシグナル。勝手に使って良い。しかし、デバッグが難しくなることを覚悟して! デフォルト動作は無視。

SysV シグナルシステムコール

さて、シグナルの実際のCプログラムからの操作に話は移る。まず最初はいわゆる「SysV シグナル」システムコールである。これはもはや古いモデルであるが、長らく使われて来ているために、一番馴染みも深い。こちらの方が単純であり、シグナルのさまざまな問題について理解するためには、手っ取り早いのでまずこれを解説する。

シグナルにはデフォルト動作と、ユーザプログラムで設定可能な動作(シグナルハンドラ)とが存在する。これらのシグナルの動作を指定するのが、signal(2) である。プロトタイプは次の通り。

#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);

おっと、ご心配召されるな。この複雑なプロトタイプは実は何のこともないのである。仮に signal(2) が void 型(何も返さない)と仮定して宣言してみよう。

void signal(  int signum, void (*handler)(int)  );

つまり、signal(2) は2つの引数を取る。第1引数は int signum であり、これは設定したいシグナル(の番号)を指定する。シグナル番号は signal.h(というか Linux では asm/signal.h)で定義されている文字定数を使うのが普通である。第2引数は設定すべきシグナルハンドラへの関数ポインタである。シグナルハンドラはこれで見る通り、void 型の戻り値と1つの int 型引数を持つ関数でなければならないことが判る。つまり、次のもので良い。

#include <signal.h>

void signal_handler( int signo )
{
     /* 処理 */
}

void main()
{
    signal( SIGINT, signal_handler );
    ................

これで Ctrl+C が押されたときには、自動的に signal_handler() 関数が呼び出されることになる。この時、signal_handler() 関数の引数 signo には、そのシグナルの番号が入る。複数のシグナルに対し同一のシグナルハンドラを設定した場合などに、区別するのに signo が使えるわけだ。

しかし、ユーザが用意したシグナルハンドラではなくて、「単に無視する」「デフォルト動作に戻す」という指定もできる。これもやはり signal.h(というか Linux では asm/signal.h)で定義されている。この定義は次の通り。

#define SIG_DFL	((__sighandler_t)0)	/* デフォルト */
#define SIG_IGN	((__sighandler_t)1)	/* 単に無視する */
#define SIG_ERR	((__sighandler_t)-1)	/* エラーを表す */

え、「__sighandler_t」って何? これは単なる typedef に過ぎない。(__ を含まない「sighandler_t」も定義されているぞ! 後で出る...)

typedef void (*__sighandler_t)(int);

「SIG_ERR」は何だろうか? これは signal(2) が戻り値として、それ以前に設定されていた古いシグナルハンドラを返す、というインターフェイス仕様なのである。だから、signal(2) のプロトタイプ宣言は次のようにも書ける。

__sighandler_t signal( int sig, __sighandler_t handler );

まあ、こっちのが判りやすいが、先程の複雑な宣言だって馴れればどってことはない。要するに Ctrl+C を無視するのならば、次のようなプログラムで構わない。

#include <signal.h>

void main( )
{
    __sighandler_t old;
    old = signal( SIGINT, SIG_IGN );
    if( old == SIG_ERR ) {
        printf( "何でシグナルハンドラが設定できないの?\n" );
    }
    ...................

という風にプログラムが書けるわけだ。

シグナルはOSやユーザが生成するだけではなくて、プログラムの中から自分自身(あまりないが...SIGTSTP や SIGCONT をトラップしたときにデフォルト動作を呼び直すのに使うことがある)や子プログラムや親プログラム(こっちがほとんど)に送ることができる。kill(1) のインターフェイスから想像が付くように、送る対象はプロセス番号で指定する。まあ、無関係なプロセスにシグナルを送るケースはあまりないだろう。何らかの親子関係のあるプロセスに送るわけだから、プロセス番号は ppid(2) で取得したり fork(2) の戻り値を保存しておいたりして、アクセスできるはずだ。(そりゃパイプ実行で ps を起動したり、/proc を解析して強引にプロセス番号を取得することだってできるけど...) kill(2) のインターフェイスは次の通り。

#include <signal.h>

int kill(pid_t pid, int sig);

第1引数が送る相手のプロセス番号で、第2引数が送るシグナル番号であることは言うまでもない。戻り値は 0 が正常で、-1 がエラーである。この kill(2) にはちょっとした応用としての使い方がある。重大な影響がない(無視されるとか...)シグナルを送ることで、あるプロセスが今生きているかどうか確認できるのだ。もし、そのプロセスが存在しなければ、kill(2) が成功するわけがなく、エラー値 -1 が返る。プロセスが存在しない(あるいは送るシグナル番号が定義されていない)以外に、ちょっとエラーする理由は考えられないので、これで特定のプロセス番号のプロセスが生きているかどうかのチェックとして使うことができるのである。

しかし、しかし、この kill の第1引数 pid にはちょっとしたバリエーションがある。実はこういう風になっている。

pid > 0
pid の番号を持つプロセスにシグナルが送られる。
pid < -1
-pid の番号を持つプロセスグループにシグナルが送られる。言い替えれば、あるプロセスが子プロセスを生成したりした時には、そのプロセスのプロセス番号が、プロセスグループの番号として登録される、ということになる。だから、そのプロセスの子孫全体に対して、このシグナルが送られることになる。
pid = 0
シグナルが自分のプロセスグループ全体に送られる。つまり、自分と自分の子プロセスたちに送られるわけだ。
pid = -1
この場合は特殊で、init プロセス以外のすべてのプロセスにシグナルが送られる。これは「shutdown」の処理の中で利用されるものである。だから、良い子は root でこれをしないように。実験のためこれをやったらXもデーモンも落ちまくったぜ!

シグナルの効果とリエントラントの問題

このようにシグナルは大変重要な役割を、一般的な「プロセス間通信」の道具として果たしている。OSとアプリケーションの間で、あるいはアプリケーション同士の間で少なくとも「通知」で済む相互作用ならばこれで充分果たすことが出来るのである。

しかし、シグナルの動作は本質的に「非同期」である。シグナルが届いた時に、そのプロセスがどんな状態であるかをあらかじめ知ることはできないし、シグナルハンドラのレベルで、どんな状態で呼び出されたか、どんな動作をすればすべてのプロセスの状態において適切なのかを判断することは、実は大変難しいことなのである。

たとえば、read(2) システムコールを発行して、入力を待っている(ブロックされている)状態だとしよう。この時、シグナルを受けたとしてどういう動作が適切なのだろう? もし、シグナルを受けてプロセスが終了すべきならば、システムコールは中断した方が良い。なぜならば、システムコールはUNIXではプロセスメモリ空間に属するものでも、プロセスの特権(ユーザ特権)によって動作するものではないからである。もし、プロセスが終了して後に read(2) が入力を受け付けるということにでもなれば、実に奇怪なことになる。しかし、シグナルによってプロセスが終了しない、単なる通知の場合にはシステムコールは継続すべきである。

また、シグナルハンドラがそれなりに時間のかかる処理をする場合、シグナルの処理中に(いいかえればシグナルハンドラを実行中に)、新しい同一シグナルが届く場合もありうる。この時にグローバルデータをシグナル処理が参照・変更しているとすれば、古い処理と新しい処理とが衝突しあうこともありうる。たとえば次のソースを見てみよう。

int num = 0;

void handler( int signo )
{
     char out[3];
     num++;
     /* 何か時間がかかる処理 */
     out[0] = num / 10 + '0';
     out[1] = num % 10 + '0';
     out[2] = '\n';
     write( 1, out, 3 );
}

この時、「/* 何か時間がかかる処理 */」の間に新しいシグナルが到達し、新規にシグナルハンドラが起動されたら、それは変数 num の値を更新し、その新しい num の値で出力をした後に、割り込まれた古いシグナルハンドラが処理を継続する。この時、num の値は2番目のシグナルハンドラが更新した値になってしまっている!

このことを「リエントラント(再入)問題」と一般に呼ぶ。このリエントラント問題は単にシグナルの問題ではなくて、スレッドなどの並行プログラミング一般の問題であり、どちらかと言えば「生成した!」以上の情報を持たないシグナルではやや一般のケースよりも問題が少ないともいえるのだが、それでもUNIXの開発者たちはこの「リエントラント問題」に頭を痛めることになる。ホントはハードウェア割り込みでもこの問題は大問題であり、割り込みベクタのプログラム(デバイスドライバがやっている)では、時間のかからない処理(たとえばポートを読んで値を保存する程度)と、時間がかかる処理(たとえば入力を変換する)に処理を分割し、時間のかからない処理(こっちを「トップハーフ」と呼ぶ)だけを割り込みベクタの処理として登録し、時間のかかる処理(こっちは「ボトムハーフ」)は適当なスケジュールで割り込みとは無関係に処理する、というような工夫がされるような場面でもある。また、スレッドプログラミングでは相互排他制御を実現するために、いわゆる「mutex」の処理を行って(Java で言えば synchronized 文や synchronized メソッド)再入を待たせることをするのである。

では、UNIXシグナルの開発者たちはどういう選択をしたのだろう? 最も古い解決である SysV シグナルではこのようになる。

システムコールの処理中にシグナルが到達したら
システムコールは問答無用で停止する。システムコールの関数は一般に -1 をエラーの戻り値として返すので、シグナル割り込みによる場合もエラー値 -1 を返す。特にエラー種別を取得する errno 変数(スレッドセーフ対応の結果、変数でない場合もあるが..)は EINTR の値がセットされて、それが割り込みによるシステムコールの失敗であることを示す。
シグナルハンドラの処理中に同一のシグナルが到達したら
この状況はまずいので起きないようにする。つまり、シグナルハンドラがそのシグナルに関して起動されるのは登録されたのち一回だけとする。逆に言えば、シグナルハンドラが起動されたら、それ以降に新しい同一シグナルが到達したとしても、デフォルト動作に戻ってしまう。
ただし、シグナルを無視する SIG_IGN の場合は
システムコールを中断しないし、一度起きた後にデフォルトに戻すこともしない。

実はこれはなかなか「意外な」解決法である。素直なプログラムである次のプログラムは実は重大な問題があることになるのである。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void trap( int signo )
{
    char *mes = "signal detect\n";
    write( 1, mes, strlen(mes) );
}

void main()
{
    char buff[256];

    signal( SIGINT, trap );
    while( 1 ) {
        int ret = read( 0, buff, sizeof(buff) );
        if( ret == -1 ) {
            fprintf( stderr, "read error!\n" );
            exit( 1 );
        }
   }
}

おそらくこのプログラムは、単に SIGINT 割り込みを実質上無視して標準入力を標準出力に吐き出すつもりで書かれているのだろう。しかし、signal(2) が SysV シグナルだった場合には、実に意外な動作をすることになる。つまり、最初の Ctrl+C 入力によって、read(2) の結果は -1 となり、エラー表示をして終了してしまうのだ。だから、この意図ならば、次のように書き換えなくてはならない。

#include <errno.h>
    ................
    while( 1 ) {
        int ret = read( 0, buff, sizeof(buff) );
        if( ret == -1 ) {
            if( errno != EINTR ) {
                fprintf( stderr, "read error!\n" );
                exit( 1 );
            }
        }
   }

しかし、これでも不十分である。なぜなら、シグナルハンドラは1回しか使えない。だから最初の Ctrl+C 入力は正しく動くが、2回目ではデフォルト動作に戻っているので、デフォルト動作であるプログラムの終了を引き起こしてしまう。これを回避するなら次のように直さなくてはならない。

void trap( int signo )
{
    signal( SIGINT, SIG_IGN );  /* とりあえず処理中は無視する */
    char *mes = "signal detect\n";
    write( 1, mes, strlen(mes) );
    signal( SIGINT, trap );     /* シグナルハンドラの再セット */
}

さすがにこのような結果をまずいと考えたOS開発者もいた。いわゆる BSD の開発者たちは別なアプローチを取ったのである。これを「BSD シグナル」と呼ぶ。違いは次の通り。

システムコールの処理中にシグナルが到達したら
処理のコンテキストは一時的にシグナルハンドラに移るが、シグナルハンドラの処理が終ればシステムコールの処理を継続する。つまり、エラー値で戻ることは原則的にしない。
シグナルハンドラの処理中に同一のシグナルが到達したら
新しいシグナルはOSによって「保留」され、現在のシグナルハンドラの処理の終了を待って、新たにシグナルハンドラを起動する。だから、シグナルハンドラは一度呼び出されたからと言って、デフォルト動作に戻ることはしない。

これならば、修正前のコードで充分動作するのである。

しかし、この BSDシグナルは、その幸せな実行結果ほどには、全体的な幸福を生んだわけではない。多くのUNIXに移植可能なプログラムを書く場合、同じ signal(2) であっても、それが SysV 風の動作をするのか、BSD 風の動作をするのかを判定した方が良いわけである。まあ、SysV 風に書いておけば、もし BSD シグナルであっても大概の場合には望むように動作する、というのはあるのだが.... だから、プログラム書方としてはBSD流の簡単な書き方はそれほど普及せずに、SysV風の書き方が一般的だったのである。

ちなみに Linux ではどうだろう? 実は古くは(libc-2 になる前)は、signal(2) は SysV 風に動作した。しかし、最近(libc-2 以降)では BSD 風に動作する。ちょっと「おいなあ」という感じである。

結論: signal(2) のセマンティクス(意味)は一意に定めることはできない。もはや signal(2) は使うべきではなく、新しいシステムコールである POSIX シグナルシステムコールを使うべきである。


POSIXシグナルシステムコール

このような混乱状況から脱出するために、UNIX の互換性を保証するための機関である POSIX では、新しいシグナルシステムコールを定めた。名前も変えてしまい、新しいシステムコールでは BSD の動作も SysV の動作も模倣できるようにしたのである。しかし、そのセマンティクスはまったく新しいものを採用している。だから、もはや signal(2) は時代遅れである。新規のプログラムでは POSIXシグナルだけを使いなさい。

POSIX シグナルは、BSD で採用されたシグナルの「保留」というアイデアを全面的に採用し、それを「シグナルマスク」という概念で一般化した。言い替えればあるシグナルに対応したシグナルハンドラを実行中に、そのシグナル以外のシグナルも「保留」できるようになっているのである。つまり、シグナルの「保留」を「シグナルマスク」の概念でハンドラの動作と独立させたわけである。

signal(2) の代わりに使うべきシステムコールは sigaction(2) である。

#include <signal.h>  /* ヘッダは変わらない */
int sigaction( int signum,  /* シグナル番号 */
               struct sigaction *act,    /* 変更する情報 */
               struct sigaction *oact ); /* 変更前の情報を取得 */

引数 act, oact は NULL でも良い。act == NULL ならば、現在の設定情報が oact に取得されるし、oact == NULL ならば、act の設定情報による変更がなされて、変更前の情報は捨てられるに過ぎない。

ということは、struct sigaction の構造が非常に重要である。これはこういう構造体である。

#include <signal.h>  /* 実際の定義は Linux では bits/sigaction.h */

struct sigaction {
    sighandler_t  sa_handler;  /* シグナルハンドラ。ただし、最新規格では3引数
                                  のシグナルハンドラに対応する仕様もある。
                                  当然 SIG_IGN, SIG_DFL も使える。*/
    sigset_t      sa_mask;     /* 「シグナルマスク」。この構造体は直接操作せずに、
                                   sigprocmask(2) を使って操作する。 */
    unsigned long sa_flags;    /* 動作の詳細を設定するフラグ。BSD や SysV の
                                  動作を模倣できる */
    void (*sa_restorer)(void); /* シグナルスタックを操作する。
                                  一般には気にしなくて良い。 */
};

このうち、シグナルマスクである sa_mask は、特に設定しなくても、当然シグナルハンドラを設定したシグナルに関しては、デフォルトでブロックする。だから、気にして設定するまでもないことが多い。

では POSIX シグナルを使って、BSD風動作のシグナルを実現してみよう。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void trap( int no )   /* BSD 風シグナルハンドラ */
{
     char *mes = "signal get\n";
     write( 1, mes, strlen(mes) );
}

void main( )
{
     char buff[256];
     int ret;
     struct sigaction sa;

     /* sa の内容をとりあえず 0 でクリアしておく */
     memset( &sa, 0, sizeof(struct sigaction) );  
     sa.sa_handler = trap;         /* シグナルハンドラの設定 */
     sa.sa_flags |= SA_RESTART;    /* システムコールが中止しない */ 

     /* ハンドラの設定。ただし古い情報は不要 */
     if( sigaction( SIGINT, &sa, NULL ) != 0 ) {
          fprintf( stderr, "sigaction(2) error!\n" );
          exit( 1 );
     } 

     while( 1 ) {
          printf( "read wait..\n" );
          ret = read( 0, buff, sizeof(buff) );
          write( 1, buff, ret );
     }
}

今度は SysV 風の動作の模倣である。trap() 内部で SIG_IGN を使って、多重に送られたシグナルを無視している。

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>

struct sigaction sa;     /* trap をシグナルハンドラとする */
struct sigaction ignore; /* SIG_IGN を実現する */

void trap( int no )  /* SysV 風シグナルハンドラ */
{
     char *mes = "signal get\n";

     sigaction( SIGINT, &ignore, NULL ); /* signal( SIGINT, SIG_IGN ); */
     write( 1, mes, strlen(mes) );
     sigaction( SIGINT, &sa, NULL );     /* signal( SIGINT, trap ); */
}

void main( )
{
     char buff[256];
     int ret;

     /* trap をシグナルハンドラとする設定 */
     memset( &sa, 0, sizeof(struct sigaction) );
     sa.sa_handler = trap;
     sa.sa_flags |= SA_NOMASK;   /* 二重に起動するシグナルをブロックしない */
     sa.sa_flags |= SA_ONESHOT;  /* 一度起動されたハンドラをリセットする */

    /* SIG_IGN 動作を実現する */     
     memset( &ignore, 0, sizeof(struct sigaction) );
     ignore.sa_handler = SIG_IGN; /* シグナルハンドラに SIG_IGN を設定 */
     ignore.sa_flags |= SA_NOMASK;
     ignore.sa_flags |= SA_ONESHOT;

     if( sigaction( SIGINT, &sa, NULL ) != 0 ) {
          fprintf( stderr, "sigaction(2) error!\n" );
          exit( 1 );
     } 

     while( 1 ) {
          printf( "read wait..\n" );
          ret = read( 0, buff, sizeof(buff) );
          if( ret <= 0 ) {
               if( errno != EINTR ) { /* システムコールが中断するので */
                    fprintf( stderr, "read error!\n" );
                    exit( 1 );
               }
          } else {
               write( 1, buff, ret );
          }
     }
}

ここで少し表のかたちで、POSIX シグナルのデフォルト動作を含めてまとめてみよう。今まで見たように、POSIX シグナルは、sa_flags メンバの設定によって BSD 風、SysV 風シグナルを模倣できることはいうまでもない。

 SysVBSDPOSIX
現在ハンドラが処理中のシグナルをブロックするかしないするする
ハンドラが一度実行されると、デフォルト動作に戻るか戻るそのままそのまま
システムコールがシグナルによってエラーするかするしないする

筆者は、何か POSIX シグナルのデフォルト動作って、両方の顔を立てているような気がする(気がするだけだが..)。

さらに、POSIXシグナルは「シグナルマスク」の概念によって、あるハンドラの実行中に別なシグナルによる割り込みを防ぐことも出来る。これは先程の struct sigaction の sa_mask メンバを設定することによってできるのだが、それだけではない。シグナルハンドラではないメイン処理の流れの中でも、指定のシグナルハンドラの起動を「保留」し、ある範囲ではそのシグナルによって割り込まれないことを保証できる。逆に言えば、その範囲でそのシグナルが発生したとしても、その範囲を抜けた後でしかそのシグナルのハンドラは起動されないようにできるのである。

このために、シグナルマスクの操作が必要である。しかし、シグナルマスクを表す構造体 sigset_t を直接そのメンバをいじるのではなくて、専用のライブラリ関数を使って行う。これは次の通り。

sigset_t  masks;       /* typedefされている */
sigemptyset( &masks ); /* sigset_t の内容を空にする */
sigaddset( &masks, SIGHUP ); /* SIGHUP をシグナルマスクに追加する */
sigdelset( &masks, SIGHUP ); /* SIGHUP をシグナルマスクから削除する */

このように作られたシグナルマスクは、struct sigaction に設定できる。たとえば、時間のかかるシグナルハンドラの処理の間、ほぼすべてのシグナルを保留して、その完了を保証するには次のようにする。

void trap( int no )
{
     char *mes1 = "signal get\n";
     char *mes2 = "handler end\n";
     write( 1, mes1, strlen(mes1) );
     sleep( 5 );  /* 時間のかかる処理の模倣 */
     write( 1, mes2, strlen(mes2) );
}

void main( )
{
     char buff[256];
     int ret;
     struct sigaction sa;
     sigset_t block;

     sigemptyset( &block );
     /* 以下3つは正しくブロックされる */
     sigaddset( &block, SIGHUP );
     sigaddset( &block, SIGQUIT );
     sigaddset( &block, SIGTERM );
     /* 次の2つはトラップ不能なシグナルであり、しかもブロックされることもない。
       つまり、即刻終了 or 停止する。だからシグナルマスクに設定する意味はない。 */
     sigaddset( &block, SIGKILL );
     sigaddset( &block, SIGSTOP );

     memset( &sa, 0, sizeof(struct sigaction) );  
     sa.sa_handler = trap;
     sa.sa_mask = block;  /* シグナルマスクの設定 */
     sa.sa_flags |= SA_RESTART;

     if( sigaction( SIGINT, &sa, NULL ) != 0 ) {
          fprintf( stderr, "sigaction(2) error!\n" );
          exit( 1 );
     } 
     ...............

さらに、メインルーチンの流れの中で、特定シグナルによる割り込みを保留するためには、sigprocmask(2) を使う。これはシグナルハンドラの処理によって邪魔されないことを保証するために使う。たとえば、シグナルハンドラによって追加されたデータの処理をする部分などでは、シグナルハンドラとメインコードが共通するグローバル変数を見ることが多い。だから、こういう場合にはメインコードの中でシグナルハンドラに邪魔されると大変まずい処理になる部分があることになるので、これを sigprocmask(2) で保護するわけである。

たとえば、次のコードを見てみよう。このプログラムでは Ctrl+C によってリングバッファにそれが押された時間を蓄え、メインルーチンの流れの中(具体的には display() の中)で、その保存された時間を表示している。(リングバッファが判らない人は「キュー(FIFO)」を見よ。)まず、割り込みからの保護を何も考えないコードを示す。

#include 
#include 
#include 
#include 

#define RINGSIZE  5
long Ring[RINGSIZE];  /* リングバッファ */
int Rp = 0;           /* 読み込みポインタ */
int Wp = 0;           /* 書き込みポインタ */
char *FullMessage = "Ring buffer is full!\n";

void trap( int no )
{
     long t;
     int next = (Wp + 1) % RINGSIZE;
     if( next == Rp ) {
          write( 1, FullMessage, strlen(FullMessage) );
          return;
     }

     Ring[Wp] = time( &t );
     Wp = next;
}

void display()
{
     if( Rp != Wp ) {
          /* わざとダサく書く.... */
          printf( "SIGINT %s\n", asctime( localtime( &Ring[Rp] ) ) );
          Rp = (Rp + 1) % RINGSIZE;
#if 0
          /* タイミングに神経質なのでホントはこう書く */
          long t = Ring[Rp];
          Rp = (Rp + 1) % RINGSIZE;
          printf( "SIGINT %s\n", asctime( localtime( &t ) ) );
#endif
     }
}

void main()
{
     struct sigaction sa;

     memset( &sa, 0, sizeof(struct sigaction) );  
     sa.sa_handler = trap;
     sa.sa_flags |= SA_RESTART;

     if( sigaction( SIGINT, &sa, NULL ) != 0 ) {
          fprintf( stderr, "sigaction(2) error!\n" );
          exit( 1 );
     } 

     while( 1 ) {
          getchar();
          display();
     }
}

display() と trap() は同一のデータを操作している。まあ、普通は問題がないのだが、通常のキーボードの入力と Ctrl+C の入力とのタイミングで、display() の実行中に trap() が実行される可能性を否定することはできない。この時に、Wp, Rp の更新は相互に依存しあっているから、ひょっとするとマズいタイミングで trap() が呼び出されて、Wp と Rp の関係が崩れる可能性がないわけでもない。display() の中でシグナルハンドラが起動しない保証があれば、こういう心配は不要になる。これはやはり sigprocmask(2) によって実現できる。

sigset_t block;  /* シグナルマスク */

void display()
{
     /* SIGINT 割り込みから保護 */
     sigprocmask( SIG_BLOCK, &block, NULL ); 
     if( Rp != Wp ) {
          printf( "SIGINT %s\n", asctime( localtime( &Ring[Rp] ) ) );
          Rp = (Rp + 1) % RINGSIZE;
     }
     sigprocmask( SIG_UNBLOCK, &block, NULL ); 
     /* 保護の終り */
}

void main()
{
     struct sigaction sa;

     /* シグナルマスクを作っておく */
     sigemptyset( &block );
     sigaddset( &block, SIGINT );

     memset( &sa, 0, sizeof(struct sigaction) );  

どうだい、なかなか POSIX シグナルって優秀だろ! これを使わない手はないぞ!


シグナルに関する雑多な話題

さて、シグナルの使い方に深まったところで、現実的なシグナルプログラミングで重要なポイントをいくつか解説しよう。筆者のUNIXプログラミングの経験値の限りを尽くして、ユーモアとともに御覧に入れよう!

まず、リエントラント問題である。シグナルハンドラ自体のリエントラント問題についてはこれを回避する方法を解説し、他のシグナルハンドラとの競合を排除するための sigprocmask(2) の使い方も解説した。しかし、一般論としてシグナルハンドラの処理はなるべく短時間で終るようにすべきである。あくまでもシグナルハンドラの処理は「例外的な」処理であり、メインルーチンの流れの処理に悪影響を及ぼさないように心がけるべきである。よくデーモンの場合には SIGHUP シグナルを受け付けると、設定ファイルを読み直して、再設定を有効にする。この処理をシグナルハンドラで行うことは大変愚かである(まあ、それまでの実行コンテキストが無茶苦茶になる可能性が高いし..)。これはやはりグローバル変数として「再読み込みフラグ」を作っておき、一連のリクエストの処理が完結した後で、「再読み込みフラグ」のチェックをしてもしそれが真なら設定ファイル再読み込み処理をすべきである。

このようにシグナルハンドラでしたい処理をうまくメインルーチンの流れの中に分配するやり方を「トップハーフ」「ボトムハーフ」と呼ぶことは既に述べた。また、このような処理の時には「再読み込みフラグ」を volatile で宣言しておくのが良い。volatile はそれほどには使われない宣言修飾子であるが、これは最適化を制御する修飾子であり、C言語の中で「これがなければ困る...」という重要な修飾子である。じゃあ、volatile 修飾って何だろう?

次のコードを見て欲しい。

while( 1 ) {
    if( RereadFlag == 1 ) {
         /* 特に関数を呼び出さない処理 */
         if( RereadFlag != 1 ) {
             do_proc();
         }
    }
}

何か矛盾してませんか? RereadFlag が 1 である、という場合分けの中で、更に RereadFlag が 1 ではないチェックをしているわけだ。特に関数を呼び出さないわけだから、RereadFlag が変更されるワケがない...だから、後の条件判定は常に成立しない。と考えたら大間違い。今の話のコンテキストでは、この RereadFlag がシグナルハンドラの中で変更される可能性の話なのである。しかし、コンパイラは「大間違い」の方を選ぶ可能性がある。コンパイラはプログラムを「最適化」した機械語コードを生成することに存在意義を持つ。遅いメモリアクセスよりも、速いレジスタによるアクセスを好むのである。だから、コンパイラは最適化の中で、最初のアクセス時の RereadFlag の内容をレジスタに保存し、それ以降の RereadFlag のアクセスにもレジスタアクセスとして使い回す可能性がある。この場合、通常の流れの外で RereadFlag が変更される可能性があるのだから、これは大幅にマズい。

ここで、コンパイラに対して RereadFlag が「通常の流れの外で変更される可能性があるよ!」ということを伝え、RereadFlag に対する参照をレジスタではなくて、実メモリに対する参照を強制する役割を持つのが、volatile 修飾子なのである。つまり、変数に対して部分的に最適化を抑制するのだ。

volatile int RereadFlag = 0;

というような宣言をすれば良いのである。先程のリングバッファの話でも、Wp, Rp はホントはこのように volatile 宣言すべきである。

volatile int Rp = 0;    /* 読み込みポインタ */
volatile int Wp = 0;    /* 書き込みポインタ */

さて、次の話題はライブラリ関数のリエントラント性である。元々このリエントラント性がうるさく言われるようになったのは、スレッドプログラミングがある程度盛んになってきたからである。C言語のライブラリは、遥か昔にインターフェイスが定められて、それで定着してしまっている。中には全然リエントラント性なんて考慮せずに定められたライブラリ関数だってあるのだ。特にヒドいインターフェイスとしては、引数で「どのオブジェクトを操作するか」を指定もしないのに、コンテキストを保存して何度も呼び出すタイプのもの(strtok(3)とかね)が標準ライブラリに入っていたりする。まあ、そうでなくても陰で何かの状態をグローバルデータとして保存しておいているライブラリ関数は実は多い。特に標準入出力ライブラリなんて、その最たるものだ。だから、一般にライブラリ関数はリエントラントではない(スレッド屋さんの言い方をすると「スレッドセーフ」でもない)。

だから、筆者は今までのサンプルプログラムでも、シグナルハンドラ内部で文字列を表示する関数として、一貫して「write(2)」を使っている。これは要するに、printf(3) がリエントラントではないからである(あと遅いし)。どのライブラリ関数がリエントラントか、ということについては、最近ではマルチスレッド対応で割と対応が進んでもいるので、具体的に述べることはしないが、一般にシステムコールはリエントラントである。これは同時に別なプロセスから同じシステムコールがされるんだから、当り前だな。まあ、移植性を考えてシグナルハンドラを書くのならば、ここは一つ純粋なライブラリ関数は使わない、くらいの覚悟をした方が良かろう。どうもX関連ライブラリが Linux ではまだちゃんとリエントラントになっていないようなのが、マルチスレッドで遊ぶためにはツライところであるが、シグナルハンドラでXを操作するなんてことは、まずありえないから良しとしよう。

(実は大昔 MS-DOS で TSR を書いていた時に、未熟者だったのでうっかり printf(3) を使ってしまい、ディスクシステムをブッこわしたことがある...)


さて、次の話題は SIGCHLD シグナルである。これは特に触れる必要があるシグナルである。このシグナルは、子プロセスが終了する時に親プロセスに送信される。親は子の生涯を見届けてから終了するものである(人間様とは逆だ...)わけで、よく子の終了を wait(2) などで待ち合わせをし、子の終了ステータスを取得する。しかし、このモデルが有効なのは、子が仕事をしている間は親は単に子の仕事が終るのを待つ、というモデルだけである。親と子が同時並行的に仕事をし、「子はちゃんと生きてさえいてくれればOK」というドライな親の場合には、wait(2) で待ち合わせをするのは難しい。

当然親が死ねば、子も死ぬ。しかし、子がバグなどで勝手に死んでしまうことも頻繁にある。もし、待ち合わせをしないのならば、この SIGCHLD シグナルをトラップして、そのシグナルハンドラで wait(2) 属の何かのシステムコールを呼んでその終了ステータスを取得すべきなのである。もし、これをしないと浮かばれない子は祟りをするかも知れない。つまり、「ゾンビ」になるのである。「ゾンビ」とはUNIXの技術用語であり、まだ親プロセスによって終了ステータスを取得されていない子プロセスのことであり、それがされるまでは「ps -x」オプションでの表示の中にシツコク生き残る(プロセステーブルを解放しない)。

ホントに薄情な親の場合は、この SIGCHLD シグナルを、SIG_IGN にして、SIGCHLD シグナルを受け取らないようにすることもできる。そうすると、子プロセスは薄情な親を怨むことなしに、「ゾンビ」とならずに成仏する。まあ、「ゾンビ」はうっとおしいだけのものなので、沢山の子プロセスを動かして、子と通信する時には kill(2) で存在を確認してからするのならば、SIGCHLD シグナルを SIG_IGN にするのも手である。


SIGUSR1 や SIGUSR2 のような、ユーザ定義シグナルを使って、IPC のようなプロセス間通信手段を併用する手もあったりする。つまり、データを IPC の共有メモリにあるプロセス(生産者)が置いた時に、それを使うプログラム(消費者)に、「共有メモリにデータがあるよ!」ということを伝えるために、シグナルを使うというのもアイデアの1つである。デバッグは難しいが、それでもなかなか面白いやり方ではある。


端末で変なことをしたくて、raw 端末に設定を変えてヘンテコなショートカットを実現している場合には、SIGTSTP、SIGCONT をちゃんとトラップして、端末設定をノーマルに戻すのををお忘れにならないように。Ctrl+Z を入力した時に、ユーザが混迷する可能性がある。

しかし、この2つのハンドラの場合には、デフォルトの動作もしなければ意味がない。だから、一旦シグナルハンドラの中で、自分のシグナルに対してデフォルト動作を再設定して、もう一度自分自身に対してそのシグナルを投げ返すことが必要になる。ちょっとここらへんが面倒ではある。

void TSTP_handler( int signo )
{
     end_raw_tty();   /* 端末設定を元に戻す */
     signal( SIGCONT, CONT_handler );  /* 再開時に正しく動くように準備 */
     if( OldTSTP == SIG_DFL ){
          signal( SIGTSTP, SIG_DFL );  /* デフォルト動作を設定して */
          kill( getpid(), SIGTSTP );   /* もう一度シグナルを送る */
     } else {
          signal( SIGTSTP, TSTP_handler );
     }
}

void CONT_handler( int signo )
{
     reset_raw_tty();  /* 端末設定を変える */
     signal( SIGTSTP, TSTP_handler );
     if( OldCONT == SIG_DFL ){
          signal( SIGCONT, SIG_DFL );
          kill( getpid(), SIGCONT );
     } else {
          signal( SIGCONT, CONT_handler );
     }
}

いわゆる「コールバック関数」

さて、いわゆる「コールバック関数」である。シグナル・モデルは大変便利なものなので、このアイデアを一般のプログラムの中でも使ってやろう、というのがコールバックである。形式的に見たときに、シグナルモデルは次のようなものである。

もし、○○が起きたときには、自動的にハンドラを起動するように、あらかじめ登録しておく。

だから、ハンドラは非同期に「何か」(イベント)が起きたときに起動される。これは視点を変えれば、いわゆる「イベント・ドリブンな」プログラミングになる。このイベント・ドリブンなプログラミングモデルが、GUI環境のツールキットで採用されたことから、このやり方が広まったのである。

しかし、これはそう驚くようなものでもない。Xでも Xlib のレベルでのプログラミングでは、従来型のプログラミングモデルに従っている。一般に「イベントループ」と呼ばれる、キーボード入力やマウスの移動・クリックなどの「イベント」がXサーバから送られてきたのを処理する部分では、通常次のように Xlib では書かれる。

Display *d;  /* サーバとの通信のためのオブジェクト */
XEvent e;    /* 送られたイベント */
Window w;    /* 作成するウィンドウ */

.............

/* このプロセスが受け取るイベントをサーバに通知 */
XSelectInput( d, w, ExposureMask );
XSelectInput( d, w, ButtonPressMask );
XSelectInput( d, w, KeyReleaseMask );
...............

/* イベントループ */
while( 1 ) {
    XNextEvent( d, &e );  /* イベントが届くまでブロック */
    switch( e.type ) {    /* 届いたイベントの種類を判定 */
    case Expose:       /* Expose イベント(ちょっと難しい) */
        expose_proc( e );
        break;
    case ButtonPress:  /* マウスボタンが押された */
        button_proc( e );
        break;
    case KeyRelease:   /* キーボードが押されて離された */
        key_proc( e );
        break;
    default:           /* 念のため */
        break;
    }
}

以上の例のように、届いたイベントに応じて、適切なそれを扱う関数が呼び出されている。別に不思議なことは何もない処理である。

しかし、イベントループ以下を1つのライブラリ関数にするとしたらどうだろう? これがツールキットの設計である。そうすると、今まではプログラムに埋め込まれていたイベント毎の処理関数をどうやって渡したら良いのだろう???

Xイントリンシクス(ツールキットを作るベースになるライブラリ)では XtMainLoop() 関数がこのイベントループを担当する。模式的にはこんな具合の関数である。(かなり説明をはしょっている。ホントはもっと複雑。)

#define LASTEvent  35   /* イベントの全種類数 */
/* 面倒なので typedef しておく */
typedef void (*XtEventHandler)( Widget, XtPointer, XEvent *, Boolean * );

/* イベントの全てに対するコールバックテーブル */
struct ActionData {
    XtEventHandler handler;  /* ハンドラ */
    Widget widget;           /* ウィジット(ちょっと今は説明をはしょっている)*/
    XtPointer  client_data;  /* データを与えておくことができる */
} Actions[LASTEvent];

void XtMainLoop( void )
{
   XEvent e;
   Widget w;
   XtPointer saved_data;
   while( 1 ) {
       XNextEvent( d, &e );
       if( Actions[e.type].handler == NULL ) {  /* 未登録なら何もしない */
           continue;
       }
       w = Window2Widget( e.window );   /* この処理はウソ! */
       saved_data = Actions[e.type].client_data;
       (*Actions[e.type].handler)( w, saved_data, &e, True );
   }
}

要するに関数ポインタを含む構造体配列というポインタの合わせ技から、関数ポインタを取得して呼び出しているに過ぎない。呼び出し側は今までの知識で何とかなるであろう。

問題はどうやって特定の関数を Actions[] 構造体配列にセットするかである。これをする関数は次のものである。

void XtAddEventHandler( Widget, /* 登録されるべきウィジット */
                     EventMask, /* イベント種別 */
                     XtBoolean, /* マスクできないイベント用のフラグ */
                XtEventHandler, /* ハンドラ */
                     XtPointer ); /* ハンドラが呼び出された時に渡すデータ */

これの実装はこんな具合。

void XtAddEventHandler( Widget w, EventMask type, XtBoolean flag, 
                        XtEventHandler hand, XtPointer data)
{
    Actions[type].handler = hand;
    Actions[type].client_data = data;
    Actions[type].widget = w;
    XSelectInput( d, w, type ); /* そのイベントが届くようにする */
}

そして、これを使ってイベントハンドラを登録するのは次のようにするだけである。

/* 設定されるハンドラの処理 */
void myHandler( Widget w, XtPointer data, XEvent *e, Boolean *flag )
{
     char *name;

     name = (char *)data;
     if( strcmp( name, "1st" ) == 0 ) {
     ..................
}

void main( )
{
     ................
     XtAddEventHandler( w, ButtonPressMask, False, myHandler, "1st" );

つまり、「コールバック」とは単にライブラリの使い方に過ぎないのである。myHandler のような関数ポインタを引数として渡す登録関数(この場合 XtAddEventHandler())によって、グローバルなデータにその関数ポインタが登録され、発生したイベントに応じてその保存された関数ポインタが呼び出される、という仕組みである。

C言語では「関数名=関数ポインタ」であるため、関数ポインタは独立したデータオブジェクトである。言語によっては関数ポインタが独立したデータオブジェクトではないケースもある。Java はその典型であり、Java の場合には特定のメソッドを備えたクラスをコールバックとして登録する。これはいわゆる「Observer」デザインパターンなのだが、これについては「Java 言語再入門」を参照されたい。また、言語によってはハッシュテーブルを使って文字列型の関数名によってコールバックを登録する、などというバリエーションもありうるだろう。

しかし、今までの説明では大幅に重要なことを落している。今の説明ではあるイベントに対してたった1つのハンドラしか登録できないのである。これは Actions が固定サイズの、配列添字をイベント種別に対応させた配列に過ぎないからである。このように、一つのイベントに対してたった1つのハンドラしか登録できないタイプのコールバックを「ユニキャスト・コールバック」と呼ぶ。それに対して、一つのイベントに対して複数のハンドラが登録できるものを「マルチキャスト・コールバック」と呼ぶ。この場合、ハンドラを削除できたり、ハンドラの呼ばれる順番を調整できたりすることもある。

実は今まではイベントがウィジットと結びついていることを無視して説明してきた。だから、Actions構造体配列を固定サイズではなく、realloc(3) を使った可変サイズの構造体配列として定義し直そう(ホントの実装はハッシュテーブルの方が良いだろう)。

/* イベントに対するコールバックテーブル */
struct ActionData {
    XEventMask event;        /* イベント種別 */
    XtEventHandler handler;  /* ハンドラ */
    Widget widget;           /* ハンドラが登録されるウィジット */
    XtPointer  client_data;  /* データを与えておくことができる */
};
static struct ActionData *Actions;

#define ADDSIZE 10
static int UsedSize = 0;
static int MaxSize = 0;

static int newHandler( void )
{
    if( UsedSize + 1 >= MaxSize ) {
         int newsize = sizeof(struct ActionData) * (MaxSize + ADDSIZE);
         void *tmp = realloc( Actions, newsize );
         if( tmp == NULL ) {
             fatal( "テーブルの拡大ができない!" );
             /* NOTREACHED */
         }
         Actions = tmp; MaxSize += ADDSIZE;
    }
    return UsedSize++;
}

void XtAddEventHandler( Widget w, EventMask type, XtBoolean flag, 
                        XtEventHandler hand, XtPointer data)
{
    int ind = newHandler();
    Actions[ind].event = type;
    Actions[ind].handler = hand;
    Actions[ind].client_data = data;
    Actions[ind].widget = w;
    XSelectInput( d, w, type ); /* そのイベントが届くようにする */
    /* すでに登録されているイベント&ウィジットのペアかチェックしてもいいな */
}

void XtMainLoop( void )
{
   XEvent e;
   Widget w;
   XtPointer saved_data;
   int i;

   while( 1 ) {
       XNextEvent( d, &e );
       w = Window2Widget( e.window );

       /* 今回は Actions 配列をすべてスキャンする */
       for( i = 0; i < UsedSize; i++ ) {
           /* イベント種別とウィジットの両方が一致した時にのみ */
           if( e.type == Actions[i].event 
               && w == Actions[i].widget ) {
               /* コールバックを実行する */
               saved_data = Actions[i].client_data;
               (*Actions[i].handler)( w, saved_data, &e, True );
           }
       }
   }
}

要するにマルチキャスト・コールバックはこういう風に実装されているのである。Java だと Vector クラスを使えば良いのでもっと簡単だが...

(はあ、こんなに膨大な解説になるとは思ってなかったぜ! けど大体完璧だろう。書くのに2日もかかったぞ。)



copyright by K.Sugiura, 1996-2006