非同期シグナルハンドラは Linux でどのように実行されますか?



Linux で非同期シグナル ハンドラーの実行がどのように機能するかを正確に知りたいです。まず、どれかはっきりしません スレッドはシグナルハンドラを実行します。次に、スレッドにシグナル ハンドラを実行させるための手順を知りたいです。


最初の問題について、一見相反するように見える 2 つの異なる説明を読みました:



  1. The Linux Kernel, by Andries Brouwer, §5.2「シグナルの受信」には次のように記載されています:



  2. StackOverflow の質問 "Dealing With Asynchronous Signals In Multi Threaded Program" から、Linux の動作は SCO Unix の動作に似ていると思います:



    また、Moshe Bar による「The Linux Signals Handling Model」には、「非同期シグナルは、シグナルをブロックしていないことが判明した最初のスレッドに配信されます」と記載されています。



どれが正しいですか?


2 つ目の問題として、選択したスレッドのスタックとレジスタの内容はどうなるでしょうか。 thread-to-run-the-signal-handler T を想定します。 do_stuff() を実行中です 関数。スレッドは T です のスタックは、シグナル ハンドラを実行するために直接使用されます (つまり、シグナル トランポリンのアドレスが T にプッシュされます)。 のスタックと制御フローはシグナル ハンドラに送られます)?または、別のスタックが使用されていますか?仕組みは?


答え:


ソース #1 (Andries Brouwer) はシングルスレッド プロセスに適しています。 Linux は sigwait(2) のスレッドを好まないため、ソース #2 (SCO Unix) は Linux では正しくありません。 Moshe Bar は、利用可能な最初のスレッドについて正しいです。


どのスレッドがシグナルを受信しますか? Linux のマニュアル ページが参考になります。プロセスは、CLONE_THREAD を指定した clone(2) を使用して、複数のスレッドを作成します。これらのスレッドは「スレッド グループ」に属し、単一のプロセス ID を共有します。 clone(2) のマニュアルには、



Linux は SCO Unix ではありません。Linux は、(sigwaitinfo、sigtimedwait、または sigwait を使用して) シグナルを待機しているスレッドと待機していないスレッドがある場合でも、任意のスレッドにシグナルを送信する可能性があるためです。 sigwaitinfo(2) のマニュアルでは、



シグナルのスレッドを選択するコードは linux/kernel/signal.c にあります (リンクは GitHub のミラーを指しています)。関数 wants_signal() および completes_signal() を参照してください。コードは、シグナルに使用できる最初のスレッドを選択します。使用可能なスレッドとは、シグナルをブロックせず、そのキューに他のシグナルがないスレッドです。コードはたまたま最初にメインスレッドをチェックし、次に他のスレッドを私にはわからない順序でチェックします。使用可能なスレッドがない場合、スレッドがシグナルのブロックを解除するか、そのキューを空にするまで、シグナルは停止します。


スレッドがシグナルを受け取るとどうなりますか? シグナル ハンドラがある場合、カーネルはスレッドにハンドラを呼び出させます。ほとんどのハンドラーは、スレッドのスタックで実行されます。プロセスが sigaltstack(2) を使用してスタックを提供し、SA_ONSTACK を指定した sigaction(2) を使用してハンドラーを設定する場合、ハンドラーは代替スタックで実行できます。カーネルは、選択されたスタックにいくつかのものをプッシュし、スレッドのレジスタのいくつかを設定します。


ハンドラーを実行するには、スレッドがユーザー空間で実行されている必要があります。スレッドがカーネルで実行されている場合 (システム コールやページ フォールトなど)、ユーザー空間に移動するまでハンドラーは実行されません。カーネルは一部のシステム コールに割り込むことができるため、スレッドはシステム コールが終了するのを待たずに、すぐにハンドラを実行します。


シグナル ハンドラは C 関数であるため、カーネルは C 関数を呼び出すためのアーキテクチャの規則に従います。 arm、i386、powerpc、sparc などの各アーキテクチャには、独自の規約があります。 powerpc の場合、handler(signum) を呼び出すために、カーネルはレジスタ r3 を signum に設定します。カーネルはまた、ハンドラーのリターン アドレスをシグナル トランポリンに設定します。戻りアドレスは、慣習によりスタックまたはレジスタに置かれます。


カーネルは、各プロセスに 1 つのシグナル トランポリンを配置します。このトランポリンは、スレッドを復元するために sigreturn(2) を呼び出します。カーネルでは、sigreturn(2) がスタックからいくつかの情報 (保存されたレジスターなど) を読み取ります。カーネルは、ハンドラーを呼び出す前に、この情報をスタックにプッシュしました。中断されたシステム コールがあった場合、カーネルは呼び出しを再開するか (ハン​​ドラーが SA_RESTART を使用した場合のみ)、EINTR で呼び出しを失敗させるか、短い読み取りまたは書き込みを返す可能性があります。