Linux シグナルの基礎
TLPI (The Linux Programming Interface) 再々。
TLPI の輪読の際に @matsumotory よりシグナルセットあたりをまとめるようにと指令が出たので、拙遅な感じでまとめました。
目次
シグナルとは
プロセス間通信の一種。「プロセスにシグナルを送信すると、そのプロセスの正常処理に割り込んで、シグナル固有の処理(シグナルハンドラ) が実行される」プロセス側では、シグナルを受信した際の動作(シグナルハンドラ) を設定することや、シグナルをブロックすることも可能。
コンソールで、プロセスを終了させるために kill -9 <PID> とか Ctrl+C とかした際にも、対象プロセスにシグナルが送信されている。
ちなみに、PID「1」の init や systemd に kill -9 1 しても何も起らない。(そういえば昔、oom-killer に init を殺された覚えがあるな。勘違いだったか…)
シグナルの主な特徴
- シグナルの状態
- シグナルを送ることを「生成」といい、そのシグナルが処理されることを「配送」という。また、生成から配送まで間の状態を「保留」という
- シグナルの生成
- シグナルは、別プロセスや自身からも送信可能だが主にカーネルから送られることが多い
- シグナルは、プロセスだけでなく特定スレッドに対しても送信可能
- シグナルの配送
- シグナルの種類によって、受信した際の標準の動作が決められている(SIGINTはプロセス終了、など)
- シグナルを受信するプロセスは、シグナル受信時の動作を変更できる(シグナルハンドラの登録)
- シグナルの保留
- シグナルを受信するプロセスは、特定のシグナルをブロック・アンブロックすることができる(ブロック中にきたシグナルはアンブロック時に配送される)
シグナルの種類
シグナルには、古くからある「標準シグナル」と機能が追加された「リアルタイムシグナル」の 2種類がある。 シグナルには一意の整数(シグナル番号) が割り当てられていて、標準シグナルの場合は 1 から 32(NSIG) となっている。
|
|
標準シグナルによってプロセスに伝えれらる情報は「シグナル番号」のみ。いつ、どこから何回受信したかと言った情報は伝えられない。 リアルタイムシグナルでは、こういった情報を伝えることができるようになっている。なお、リアルタイムシグナルには、標準シグナルのようなシグナル番号に対応すに名前(SIGHUP など)は定義されていない。
シグナルの生成
プロセスからシグルナルを送信する場合は、以下のようなシステムコールや glibc の関数を用いる。
| システムコール・関数 | 用途 |
|---|---|
| kill() | プロセスにシグナルを送信 |
| pthread_kill() | スレッドにシグナルを送信 |
| raise() | 自身にグナルを送信 |
| killpg() | プロセスグループにシグナルを送信 |
また、ハードウェアや端末契機でもシグナルは送られてくる。
- ハードウェア例外
SIGFPE: 0 除算SIGSEGV: メモリアクセス違反
- 端末へキー入力
SIGINT: Ctrl+C が入力されたSIGQUIt: Ctrl+\ が入力された
- ソフトウェアイベント
SIGCHLD: 子プロセスが終了したSIGXCPU: CPU の利用上限に達した (ハードリミットではSIGKILL)SIGIO: fd からデータが読み取れる状態になった
シグナルの配送
シグナルが配送されると、プロセスの通常処理に割り込んで、シグナルハンドラが実行される。
シグナル種類ごとにデフォルトの動作が決められており、個別にシグナルハンドラを登録していなければ、そのデフォルトの動作がカーネルにより実行される。デフォルトの動作は以下の5つ。
- プロセスを終了する
- コアを出力し、プロセスを終了する
- シグナルを無視する
- 処理を一時停止する
- 処理を再開する
シグナルハンドラは、以下どちらかの関数を使用してシグナル番号ごとに登録できる。signal より sigaction のほうが、機能や移植性から見ても有用なので、利用を推奨されているらしい。
signal()
1void ( *signal(int sig, void (*handler)(int)) ) (int);sigaction()
1int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);引数 act の型である sigaction 構造体は、以下のようになっている。
123456struct sigaction {void (*sa_handler)(int);sigset_t sa_mask;int sa_flags;void (*sa_restorer)(void);};1 つ目のメンバである
sa_handlerは signal() の引数handlerと同様にシグナルハンドラを表す。
2 つ目のメンバであるsa_maskはハンドラ実行中にブロックしたいグナルを指定する。ブロックしたいシグナルが複数ある場合もあるので、複数のシグナルを表すことができるシグナルセット(sigset_t) が利用されている。
シグナルセット
シグナルセットは、複数のシグナルをまとめて表現するもの。どのシグナルが選択されているかを表すだけなので、Linux では以下のような整数型で実装されており、各ビットの位置がシグナル番号に対応する。
|
|
init(PID:1) を例にすると、
|
|
プロセスがどのシグナルをブロックしているかは /proc/PID/status を見るとわかる(上記は 64bit 環境)。SigIgn の値がブロックしているシグナルを表していて、これは16進表記になっているので、2進表記にすると以下のようになる。
|
|
つまり、13bit 目にビットが立っているので、上に記載した「シグナルの種類」を見ると、シグナル番号 13 つまり、SIGPIPE のみブロックしていることがわかる。
ただ、Linux の実装がビットマスク使用しているだけなので、シグナルマスクを操作する場合は、必ず以下の関数を用いる必要がある。
| 関数 | 用途 |
|---|---|
| sigemptyset | すべてシグナルが選択されていない状態に初期化する |
| sigemptyset | すべてシグナルが選択されている状態に初期化する |
| sigaddset | シグナルセットに一つのシグナルを追加する |
| sigdelset | シグナルセットから一つのシグナルを削除する |
| sigismember | シグナルセットに特定のシグナルが含まれているか調べる |
| sigandset | シグナルセットに指定した sigset_t とAND した結果を設定する |
| sigorset | シグナルセットに指定した sigset_t とOR した結果を設定する |
| sigisemptyset | シグナルセットが空かどうかしらべる |
シグナルの保留
シグナルが「生成」され、該当のプロセスが次に実行されたタイミングで、シグナルは「配送」される。この生成から配送までの間の状態を「保留」という。
保留の状態は、スケジュール待ちの場合だけでなく、プロセス自身でシグナルをブロックすることでも発生する。
さきほど説明した sigaction() では、シグナルセットを使用してシグナルのブロックを指示することができた。ただ、これは、シグナルハンドラ実行中のみブロックされており、ハンドラが終了すると自動的に解除(アンブロック)される。
そのため、 明示的にブロック・アンブロックしたい場合は、sigprocmask(), pthread_sigmask() を使用する。
シグナルマスク
ブロックしているシグナルは「シグナルマスク」というプロセス(正しくはスレッド)の属性で管理されており、以下のようなタイミングで操作される。
- シグナルが配送された際、下記シグナルをシグナルマスクに追加し、シグナルハンドラが完了するとシグナルマスクから自動的に削除する
- 受診した該当のシグナル(sigaction で変更可能)
- sigaction で指定したブロックしたいグナルセットのシグナル
- sigprocmask(), pthread_sigmask() が実行されたタイミング
- 引数に
SIG_BLOCKが指定されると追加、SIG_UNBLOCKが指定されると削除される
- 引数に
ブロック中のシグナル
ブロック中のシグナルが生成されると、カーネルはシグナルを保留シグナルに追加し、該当のシグナルがアンブロックされるまで配送しない。また、ブロック中に同じシグナル番号のシグナルが複数回生成されても、アンブロック時には一度しか配送されない。
保留シグナルは、ブロックシグナル(SigBlk)と同様に /proc/PID/status で確認できる。
sleep コマンドを STOP し、SIGUSR1 を送っている。STOP しているのでシグナル(SIGUSR1)が配送されず、保留状態となっている。
なお、SIGKILL、SIGSTOP をブロックしようとしても無視され、エラーにもならない。
シグナル処理時のカーネルの動作
(本当は、ここの内容をメインにしたかったが、概要を書いただけで力尽きたので、次回書く。)