Manjusaka

Manjusaka

プロセス内でのシグナル処理について簡単に話しましょう。

最近、ある技術グループで 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);
  }
}

実際、このコードは典型的なシグナル処理のコードです。次の内容を紹介するために、いくつかの重要なシステムコールを復習してみましょう。

  1. signal1: シグナルハンドラを指定するための関数です。ユーザーはこの関数を使用して、特定のシグナルに対してハンドラを指定することができます。シグナルが発生すると、システムは対応するハンドラを呼び出して対応するロジックを実行します。
  2. sigfillset2: signal sets(シグナルセット)を操作するための関数の一つで、ここではシステムでサポートされているすべてのシグナルをシグナルセットに追加することを意味します。
  3. fork3: 皆さんには馴染みのある API です。新しいプロセスを作成し、pidを返します。親プロセスの場合、返されるpidは対応する子プロセスのpidです。子プロセスの場合、pidは 0 です。
  4. execve4: 特定の実行可能ファイルを実行します。
  5. sigprocmask5:プロセスのシグナルマスクを設定します。最初の引数がSIG_BLOCKの場合、関数は現在のプロセスのシグナルマスクを第三引数で指定されたシグナルセット変数に保存し、現在のプロセスのシグナルマスクを第二引数で指定されたシグナルマスクに設定します。最初の引数がSIG_SETMASKの場合、関数は現在のプロセスのシグナルマスクを第二引数で指定された値に設定します。
  6. wait_pid6: 精確ではない概要ですが、終了した子プロセスのリソースを回収して解放します。

OK、これらの重要なsyscallについて理解した後、このコードはほぼ理解できるはずです。ただし、このコードを完全に理解するには、Linux または POSIX のいくつかのメカニズムを復習する必要があります。

  1. forkによって作成された子プロセスは、親プロセスの多くの要素を継承します。この記事で説明するシグナルの一部に関しては、子プロセスは親プロセスのシグナルマスクとシグナルハンドラの関連設定を継承します。
  2. execveを実行すると、現在のプロセスのプログラムセグメントとスタックが再設定されます。したがって、上記のコードで/bin/dateを実行した後、子プロセスは再設定されます。シグナルハンドラなどの設定も再設定されます。
  3. 各プロセスにはシグナルマスクがあり、シグナルマスクに含まれるシグナルがトリガーされると、一時的にシグナル処理が実行されず、シグナルはペンディング状態になります。対応するシグナルのブロックとアンブロック後に、プロセスのシグナル処理が再びトリガーされます。プロセスがシグナルを明示的に無視するように宣言した場合、シグナル処理はトリガーされません。(Tips: 信号キューに関しては、これは POSIX 1 の規定です。POSIX では、このメカニズムを信頼性のあるシグナルと呼び、ブロックされている間に複数のシグナルが発生した場合、信号が確実に配信される信頼性のあるキューに入ります。Linux は信頼性のあるシグナルをサポートしていますが、他の Unix/Unix ライクなシステムではサポートされない場合があります)
  4. 子プロセスが終了すると、所属する親プロセスにSIGCHLD1シグナルが送信されます。親プロセスはこのシグナルを受け取った後、子プロセスを処理するためにwait_pid6関数を呼び出す必要があります。そうしないと、回収されていない子プロセスはゾンビプロセスになります。

OK、ここまでくれば、これらの内容を理解した上で、上記のコードについて完全に理解できるはずです。ただし、おそらくまだ疑問が残っているかもしれません。なぜこのコードでsigprocmask5を呼び出してプロセスのシグナルマスクをブロックする必要があるのでしょうか?これは別の問題に関連しています。

前述のように、シグナルがトリガーされると、プロセスは対応するシグナルハンドラに "ジャンプ" して処理を行います。ただし、シグナルハンドラの処理が完了した後の動作はどうなるでしょうか?Linux の設計に従うと、次の 2 つの状況が発生する可能性があります。

  1. 再入可能な関数の場合、シグナルハンドラが返された後、処理が継続されます。
  2. 再入不可能な関数の場合、EINTR1が返されます。

OK、ここまでくれば、なぜこの場所でsigprocmask5を使用する必要があるかについて具体的な理解ができるはずです。実際、これはいくつかの関数が正常に完了することを保証し、シグナル処理によって中断されることがないようにするためです。ただし、この処理はシグナルが非常に頻繁にトリガーされる場合には追加のコストをもたらす可能性があるため、異なるシナリオに応じてトレードオフを行う必要があります。

さて、もう十分です。長い間記事を書いていると疲れてしまいますね、💊。次の記事では、最近行ったカーネルプロトコルスタックのモニタリングに関するいくつかの記録を共有する予定です(フラグ ++(逃。

参考文献#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。