シグナルの誕生

今日はシグナルの歴史を調べてみたいと思う。シグナルというのはUNIXにおけるプロセス間通信の一手段であるが、CPUにおける例外のように、プロセスにとって非同期に発生するものなので、その実装はいろいろ面倒くさい。したがっていろいろ問題もあり、長年の改良の歴史を経て、今のシグナルの仕様に落ち着いた*1BSDSVR、そしてPOSIX標準になるまでのシグナルの拡張については文献が多いが、V6/V7以前は知られていないのでは。ということで、私の出番w まぁ、わかったところで喜ぶのは相当な好事家だろうが*2

いきなりV7以前の話を始めるのも何なので、前提知識として、FreeBSD版悪魔本「4.7.1 シグナルの歴史」からちょっと長いけど引用する。

シグナルは、ユーザが暴走したプログラムを強制終了する場合など、例外的なイベントをモデルとして当初設計された。それは、汎用のプロセス間通信として使われることを意図していなかったので、信頼性は考慮されなかった。初期のシステムにおいては、一度シグナルが捕捉されるとアクションはデフォルト状態に戻された。ジョブ制御が導入されシグナルの利用頻度が増したことで問題が顕在化し、プロセッサの処理速度の向上がそれに拍車をかけた。2つのシグナルが短い間隔で到着すると、シグナルハンドラは1つ目のシグナルを捕捉することができるが、2番目のシグナルによってプロセスが終了させられてしまったのである。そのため、信頼性がより求められるようになり、古い機能をサブセットとして持ちつつ新しい機能を実現する新たな枠組みが開発された。

最初から汎用的なプロセス間通信を考えていたら、シグナルを投げるシステムコールの名前をkillにはしなかったよね。おそらく大抵のことはパイプで実現できるので、汎用的なプロセス間通信の必要性はわかっていても、実装しなかったのだろう。System Vになるとメッセージキュー、共有メモリ、セマフォが導入される。余談だが、PDP-7 UNIX(V1以前)にはsmes()、rmes()というメッセージング機能があったが、wait(2)の実装にしか使われなかったので、消えたそうだ(参照:「The Evolution of the Unix Time-sharing System」)。

また、Bach本によると、dmrは次のようなことを語っている。

According to Ritchie (private communication), signals were designed as events that are fatal or ignored, not necessarily handled, and hence the race condition was not fixed in early releases.

"race condition"とは、上記の「2つのシグナルが短い間隔で到着」した場合のことだ。シグナルを受信してもハンドラをクリアしないという実装もありだが、シグナルがネストしてユーザスタックがどんどん延びていく可能性もある。当時としてはこれが現実的な実装だったんだろう。

では、当初考えられていたシグナルとはどんなものだったのだろうか。TUHSのUNIXアーカイブを眺めたところ、sig(現signal)とkillシステムコールが追加されたのはV4と考えられる。それ以前はどうであったかだが、V1とV2の間ぐらい(unix-jun72)にシグナルの原形が見て取れる。

V1のシステムコール数は20個だったが、V2までに10個強増えている。その中にquit、intr、emit、ilginsといった見慣れないシステムコールがある。どうもこれは今で言うところのSIGQUIT、SIGINT、SIGEMT、SIGFPEに相当するハンドラを設定するためのシステムコールらしいのだ(quitはemitにはハンドラを設定することはできず、無視するかどうかを設定可能)。例えば端末からDELETEを押したとき(今で言うCtrl-c相当)のハンドラをintrを使って指定できるとか。

このようなad-hocなシステムコール追加を整理するために導入されたのがシグナルと考えられる。V4のman sig(2)によると、サポートしているシグナル番号は次の通りで、驚くことに今と同じである*3。SIGIOTに関してはSIGABRTが一般的だが、今でもSIGIOTと書くことはできるはずだ。また、これらのシグナルはPDP-11のトラップ(例外)にほぼ対応している*4。シグナルとは、OSによるハードウェアのトラップ(例外)の仮想化と考えることもできるが、UNIXPDP-11に強い影響を受けていることの一端がここからも伺える。

1 hangup
2 interrupt
3* quit
4* illegal instruction
5* trace trap
6* IOT instruction
7* EMT instruction
8* floating point exception
9 kill (cannot be caught or ignored)
10* bus error
11* segmentation violation
12* bad argument to sys call

せっかくなので、シグナルの実装についても軽く書いておこう。シグナルが発生すると受信プロセスのproc構造体のp_sigフィールドにシグナルの種類を記録する(V6の時点ではビットマップではなく、シグナル番号だったので、複数種類のシグナルを一度に受信できなかった)。そして、カーネルからユーザモードへ戻るときや、sleepでプロセスが切り替わる前後にp_sigを検査して、ハンドラが指定されていればユーザ空間のシグナルハンドラに飛ぶ。シグナルハンドラからreturnするときには、シグナル発生時のユーザ空間アドレス、つまりシステムコールや割込みが起こったアドレスに戻って、元のコード実行を継続する。なお、システムコール実行中にシグナルが発生した場合は、エラーコードとしてEINTRを返すので、システムコールを再実行するコードを書く必要がある。

(追記)シグナルについて調べてたらg新部さんのcodeblogにヒットした(「シグナルについて」、「signalに残る地層」、「いや、これが、深かった」)。なかなか面白い。4.3BSD以降では、シグナルハンドラの後始末をして元の状態に復帰するために、シグナルハンドラ実行後にsigreturn(2)を呼んで、カーネルにいったん戻る処理が必要となる。そのためにトランポリンコードをスタックに積んでおくというのが昔からのやり方だったけど、最近はスタックは実行禁止だし、sa_restorer使っているよと言う話が書かれている。しかし、Linuxのman sigaction(2)には、「The sa_restorer element is obsolete and should not be used.」とある。今の実装はどうなっているんだろう? 結局、この手の処理は、なかなかきれいに実装するのは難しいという結論なのかな。ちなみに、V6の場合は、ユーザスタックの底にカーネル遷移時のアドレスを仕込んでおき、シグナルハンドラからリターンするときにそのアドレスに飛ぶという単純な仕掛けで実装されている。

BSDカーネルの設計と実装―FreeBSD詳解

BSDカーネルの設計と実装―FreeBSD詳解

  • 作者: マーシャル・カークマキュージック,ジョージ・V.ネヴィル‐ニール,砂原秀樹,Marshall Kirk McKusick,George V. Neville‐Neil,歌代和正
  • 出版社/メーカー: アスキー
  • 発売日: 2005/10/18
  • メディア: 単行本
  • クリック: 122回
  • この商品を含むブログ (58件) を見る
Design of the UNIX Operating System: International Edition

Design of the UNIX Operating System: International Edition

*1:ちなみにPlan 9ではシグナルではなくノート(note)である。

*2:こんなしょうもないことを調べるのは、考古学というより「好古学」だよなぁ、とふと思った次第である。

*3:もちろん数はどんどん増えている。SVR2の時点で19個、POSIXで32個弱。

*4:SVR2にはSIGPWRという電源異常時のシグナルがあるけど、これは消えたなぁ。PDP-11には電源異常時のトラップがあった。