はじめに
多くの開発者が日常的に Ctrl + C を利用していることでしょう。
例えば、npm run dev
コマンドで起動した開発サーバーや、ターミナル上で実行中の各種プログラムは、この入力でスパッと終了します。日常的なキー操作と言って差し支えないと思います。
それではなぜ Ctrl + C を押すだけでプログラムが終了するのでしょうか?
シグナル
結論から言えば、プログラムが Ctrl + C で終了するのは、そのプログラムに「シグナル」が送信されたからです。
◆ シグナルとは?
シグナルは、いわば「ソフトウェア割り込み」のようなものです。
OSやユーザーなどがプロセスに対して、何らかの「イベント」が起きたことを非同期に知らせるための通知手段です。
◆ 身近なシグナルの活用例
どのようにシグナルが用いられているのか、少し具体的な例を見てみましょう。
- Ctrl + C: ターミナルがこれを検知し、実行中のプログラムに「SIGINT」というシグナルを送信します。みなさんが利用する多くのプログラムでは、このシグナルの送信で終了の処理が行われていると思います。
- segmentation fault(セグフォ): 通常、プログラムがアクセスしてはいけないメモリ領域にアクセスしようとすると、OSが「SIGSEGV」を送信します。一般的なWeb開発者の場合、あまり出会うことはないかもしれません。
- Dockerのコンテナ停止: docker stop を実行すると、まずコンテナ内のプロセスに SIGTERM が送られ、一定時間後に SIGKILL が送られます。後ろでこのような仕組みがあるからこそ、私たちは特にシグナルについて考えることなくコンテナを扱うことができるんですね。
◆ シグナルの種類
シグナルには用途に応じていくつか種類があります。
代表的なものをいくつか紹介します。
番号 | シグナル名 | 意味 |
---|---|---|
2 | SIGINT | 割り込み |
9 | SIGKILL | 強制終了 |
15 | SIGTERM | 終了要求 |
11 | SIGSEGV | メモリ違反 |
19 | SIGSTOP | プロセス一時停止 |
18 | SIGCONT | 停止したプロセスの再開 |
10 / 12 | SIGUSR1/SIGUSR2 | ユーザー定義 |
ちなみに他にもたくさんあります。興味があれば調べてみてください。
◆ シグナルの非同期性
シグナルの特徴として「非同期性」があります。したがって、シグナルはいつ、どんなタイミングで飛んでくるのか予測ができないということです。
プログラムがどんな処理をしていても、OSが必要と判断すればシグナルを送信できます。プログラムは、その処理を中断して、シグナルに対する処理(シグナルハンドラ)を実行する必要があります。この「割り込み」の性質が、シグナルを扱う上で覚えておくべき重要な点となります。
Ctrl + C と SIGINT
それではタイトルの事例について考えてみましょう。
Ctrl + C を押すと、実行されているプロセスに対して SIGINT シグナルが送信されます。
この SIGINT のデフォルトの挙動は「プロセスを終了させる」ことです。そのため、特別な設定をしていない多くのプログラムは、Ctrl + Cを押すとすぐに終了するわけですね。
◆ 通常の取り扱い
極めて単純なプログラムを例に挙げます。
#include <stdio.h>
#include <unistd.h>
int main(void) {
while (1)
{
printf("Running\n");
sleep(1);
}
return (0);
}
無限ループを行う単純なプログラムです。
実行中に、Ctrl + Cを入力するとSIGINTが送信され、プログラムが停止すると思います。
シグナルを「ハンドル」してみる
先ほどの例で、プログラムがCtrl + Cで終了することが確認できました。受け取ったSIGINTに対して、プログラムがデフォルトの挙動(終了)をとったためです。
それでは、受け取ったシグナルに対して固有の処理を追加してみましょう。
◆ signal()
を使った例
シグナルハンドラを追加するためにsignal()
を使ってみましょう。実際に使うことはあまり推奨されませんが、サンプルとしては十分です。
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void handler(int sig)
{
printf("Caught SIGINT!!!\n");
}
int main(void)
{
signal(SIGINT, handler);
while (1){
printf("Running\n");
sleep(1);
}
return (0);
}
SIGINTに対してhandlerを登録しています。
先ほどとは異なり、Ctrl + Cを入力してもプログラムは終了せずに設定したメッセージが出力されると思います。
シグナルハンドラを追加したことで、固有の処理が登録されたわけです。
ちなみに Ctrl + \ で終了します。
◆ sigaction()
を使う
シグナルについてさっくりと知りたい場合は読み飛ばしていただいて問題ありません。
プログラム内で実際にシグナルハンドラを使用する場合はこちらを利用するべきです。
sigaction()
はより詳細に、かつ堅牢にシグナルを制御できます。
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void handler(int sig) {
printf("Caught SIGINT!!!\n");
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
while (1)
pause();
return (0);
}
あまり深掘りはしませんが、signal()
より詳細に制御できることは一目瞭然ですね。
移植性が高いので、実際に利用するならこちらをお勧めします。
5. 【発展】非同期シグナル安全とは?
シグナルハンドラを書く上で非常に重要な概念が「非同期シグナル安全(Async-Signal-Safe)」です。
◆ シグナルの非同期性
シグナルハンドラはプログラムの任意の時点で実行される可能性があります。
ですので、簡単に言えばシグナルハンドラの中で、動作が安全でない関数を使ってはいけませんということです。
多くある関数群の中で、シグナルセーフな関数が決まっていますので、シグナルハンドラではそれらを使いましょう。
先ほどのサンプルコードではprintf()
を利用していますが、本来これも安全ではありません。
◆ POSIXが定める「安全な関数」
POSIX標準によって、シグナルハンドラ内で安全に呼び出せる関数が明確に定められています。
例えば、以下の関数はシグナルセーフです。
_exit()
write()
kill()
getpid()
signal()
sigaction()
これらは、シグナルハンドラから呼び出されても問題なく動作することが保証されています。
◆ 安全なハンドラ
write()
関数はシグナルセーフなシステムコールなので、シグナルハンドラで安全にメッセージを出力するために使えます。
#include <signal.h>
#include <unistd.h>
void safe_handler(int sig) {
const char msg[] = "Caught signal\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}
int main() {
signal(SIGINT, safe_handler);
while (1) {
printf("Running\n");
sleep(1);
}
return 0;
}
したがってこれはシグナルセーフなハンドラといえます。
6. おわりに
普段何気なく使っている Ctrl + C の裏側には、OSがプログラムと連携するための「シグナル」という仕組みが存在します。シグナルは非同期にプロセスへイベントを通知し、プログラムはそれをハンドルすることで、独自の振る舞いを定義できるのです。
誤った記述がありましたらご指摘いただけますと幸いです。