最近、ある技術グループで Linux プログラミングにおけるシグナル処理のコードを分析するのを手伝いました。このコードは非常に良い例だと思ったので、このコードを使用して Linux のシグナル処理について話をしようと思います。
本文#
まず、このコードを見てみましょう。
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
void deletejob(pid_t pid) { printf("delete task %d\n", pid); }
void addjob(pid_t pid) { printf("add task %d\n", pid); }
void handler(int sig) {
int olderrno = errno;
sigset_t mask_all, prev_all;
pid_t pid;
sigfillset(&mask_all);
while ((pid = waitpid(-1, NULL, 0)) > 0) {
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
deletejob(pid);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
if (errno != ECHILD) {
printf("waitpid error");
}
errno = olderrno;
}
int main(int argc, char **argv) {
int pid;
sigset_t mask_all, prev_all;
sigfillset(&mask_all);
signal(SIGCHLD, handler);
while (1) {
if ((pid = fork()) == 0) {
execve("/bin/date", argv, NULL);
}
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
addjob(pid);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
}
実際、このコードは典型的なシグナル処理のコードです。次の内容を紹介するために、このコードでいくつかの重要なシステムコールを復習してみましょう。
- signal1: シグナルハンドラを設定するための関数です。この関数を使用して、特定のシグナルに対してハンドラを指定できます。シグナルが発生すると、システムは対応するハンドラを呼び出して対応する処理を行います。
- sigfillset2: シグナルセット(シグナルの集合)を操作するための関数の 1 つで、ここではシステムでサポートされているすべてのシグナルをシグナルセットに追加することを意味します。
- fork3: 皆さんにはよく知られている API で、新しいプロセスを作成し、pidを返します。親プロセス内では、返されるpidは対応する子プロセスのpidです。子プロセス内では、pidは 0 です。
- execve4: 特定の実行可能ファイルを実行します。
- sigprocmask5:プロセスのシグナルマスクを設定します。最初の引数がSIG_BLOCKの場合、関数は現在のプロセスのシグナルマスクを第三引数に渡されたシグナルセット変数に保存し、現在のプロセスのシグナルマスクを第二引数に渡されたシグナルマスクに設定します。最初の引数がSIG_SETMASKの場合、関数は現在のプロセスのシグナルマスクを第二引数に設定された値に設定します。
- wait_pid6: 精確ではない概要ですが、終了した子プロセスのリソースを回収して解放します。
OK、これらの重要なシステムコールについて理解した後、このコードはほぼ理解できるはずです。しかし、このコードを完全に理解するには、Linux または POSIX のいくつかのメカニズムを復習する必要があります。
fork
で作成された子プロセスは、親プロセスの多くの要素を継承します。この記事で話すシグナルの一部に関しては、子プロセスは親プロセスのシグナルマスクとシグナルハンドラの関連設定を継承します。execve
を実行すると、現在のプロセスのプログラムセグメントとスタックが再設定されます。したがって、上記のコードで/bin/date
を実行すると、子プロセスは再設定されます。シグナルハンドラなどの設定も再設定されます。- 各プロセスにはシグナルマスクがあり、シグナルマスクに含まれるシグナルがトリガーされると、一時的にシグナル処理がブロックされ、シグナルはペンディング状態になります。対応するシグナルのブロックとアンブロック後、プロセスのシグナル処理が再びトリガーされます。プロセスがシグナルを明示的に無視するように宣言した場合、シグナルの処理はトリガーされません。(Tips: シグナルキューに関しては、これは POSIX 1 の規定です。POSIX では、このメカニズムを信頼性のあるシグナルと呼び、ブロック中に複数のシグナルが発生した場合、信頼性のあるキューに入ることでシグナルが確実に配信されることを保証します。Linux は信頼性のあるシグナルをサポートしており、他の Unix/Unix 系は必ずしもサポートしていません)
- 子プロセスが終了すると、所属する親プロセスにSIGCHLD1シグナルが送信されます。親プロセスはこのシグナルを受け取った後、子プロセスを処理するためにwait_pid6関数を呼び出す必要があります。そうしないと、回収されていない子プロセスはゾンビプロセスとなります。
OK、ここまでくれば、皆さんはこれらの要素を理解した上で、上記のコードについて完全に理解できるはずです。ただし、もう 1 つ疑問があるかもしれません。なぜこのコードでsigprocmask5を呼び出してプロセスのシグナルマスクをブロックする必要があるのでしょうか?これは別の問題に関連しています。
前述のように、シグナルがトリガーされると、プロセスは対応するシグナルハンドラに "ジャンプ" して処理を行います。しかし、シグナルハンドラの処理が完了した後、どのような動作が行われるのでしょうか?Linux の設計に従うと、次の 2 つの状況が発生する可能性があります。
- 再入可能な関数の場合、シグナルハンドラが返された後、処理が継続されます。
- 再入不可能な関数の場合、EINTR1が返されます。
OK、ここでsigprocmask5をここで使用する理由について具体的に理解しているはずです。実際には、いくつかの関数がシグナルハンドラによって割り込まれることなく正常に実行されることを保証するためです。もちろん、この処理はシグナルが非常に頻繁にトリガーされる場合には追加のコストをもたらす可能性があります。したがって、さまざまなシナリオでトレードオフを考慮する必要があります。
さて、これでおしまいです。長い間記事を書いていて疲れました💊。次の記事では、最近行ったカーネルプロトコルスタックのモニタリングについてのいくつかのメモを共有する予定です(フラグ ++(逃。
参考文献#
- [3]. Linux man page: fork