信号と非同期信号安全でない機能について学んでいます。特に、これはprintf
非同期信号に対して安全ではなく、基本プログラムスレッドおよび信号ハンドラから呼び出されるとデッドロックを引き起こす可能性があることがわかりました。これを確認するために、次のプログラムを作成しました(少し見苦しい)。
/*
* sig-deadlock.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void
sigint_handler(int signum)
{
int i;
printf("Signal");
for (i = 0; i < 30; i++) {
printf("%d\n", i);
}
}
int
main(void)
{
int i;
printf("PID: %d\n", getpid());
signal(SIGINT, sigint_handler);
for (i = 0; i < 1e9; i++) {
printf("a");
sleep(1);
}
return EXIT_SUCCESS;
}
私はプログラムが内部ループを実行し(したがって呼び出すprintf
)、SIGINTを送信するたびに(使用してkill -INT $PID
)30個のシグナルハンドラを実行したいと思います。
しかし、実行中にシグナルハンドラが一度実行され、次のシグナルがプロセスを終了することが観察されました。この動作の原因と解決策は何ですか?
オペレーティングシステム:Linux 4.9.0。
答え1
signal(2)
これは以前のシグナルAPIの一部であり、常に使用するのに便利ではありません。主に次の理由による。
signal()の唯一のポータブル使用は、信号の処理をまたは
SIG_DFL
に設定することですSIG_IGN
。シグナルハンドラを設定するときに使用される意味はsignal()
システムによって異なります(そしてPOSIX.1はこれらの変形を明示的に許可します)。この目的で使用しないでください。もともとUNIXシステムでは、
signal()
シグナリングを介してハンドラが呼び出されたとき、信号処理が次にリセットされる。SIG_DFL
、システムは追加のシグナルインスタンスをブロックしません。システムVはまだsignal()
。BSDでは、シグナルハンドラが呼び出されるとシグナル処理はリセットされず、ハンドラの実行中にシグナルの他のインスタンスが転送されないようにブロックされます。
Linuxの状況は次のとおりです。
- カーネルの
signal()
システムコールはSystem Vセマンティクスを提供します。- デフォルトでは、glibc 2以降では、
signal()
ラッパー関数[...]がsigaction(2)
BSDセマンティクスを提供するフラグとともに呼び出されます。- glibc 2以降では、
_BSD_SOURCE
機能テストマクロが定義されていない場合、signal()
System Vセマンティクスが提供されます。 (標準モードのいずれかで呼び出されると、デフォルトの暗黙的な_BSD_SOURCE
定義は提供されません。または...参照gcc(1)
-std=xxx
-ansi
signal(2)
。
つまり、多くの場合、設定されたハンドラは複数回使用されません。シグナルの後に定義した関数は削除され、プロセスが終了すると、signal
ハンドラはデフォルト値にリセットされます。SIGINT
引用したばかりのマニュアルページを読むことをお勧めしますが、この状況を処理する最善の方法は使用を中止してsignal
に切り替えることsigaction
です。マニュアルページから必要なすべての詳細を取得できますが、以下は簡単な例です。
void sighandler(int signum);
int main(void) {
struct sigaction sa;
sa.sa_handler = sighandler;
sigemptyset(&(sa.sa_mask));
sigaddset(&(sa.sa_mask), SIGINT);
sigaction(SIGINT, &sa, NULL);
int i;
for(i = 0; i < 5; i++) {
printf("Sleeping...\n");
sleep(5);
printf("Woke up!\n");
}
}
void sighandler(int signum) {
printf("Signal caught!\n");
}
ただし、重要な注意事項があります。すでにわかっているように(したがって関数の「無限」ループ)、シグナルはプログラムがシグナルを処理するかどうかにかかわらず、シグナルが受信されるとシステムコールmain
(コールなど)を中断します。sleep(3)
上記の例では、プログラムは〜する印刷起こった!これを行うたびに、kill
睡眠コールは早期に終了します。マニュアルページを見ると、通話が中断したときに残りの省電力時間(秒単位)を返す必要があることがわかります。
答え2
ここでは強調されていない別の前提条件があります。
一貫した結果を保証するには、sigaction構造でsa_flagsを設定する必要があります。
sa.sa_flags = 0 ; // this works for me
同じ方法で信号を登録する2つの異なるアプリケーションがあり、そのうちの1つだけがこの問題があったので、数分間パニックになりました(信号を一度だけ捕捉しました)。
しばらくの間、私のコードを見て、マニュアルページを再読んだ後、両方のアプリケーションの関数スタック副作用の異なるニュアンスによって異なる動作が発生することに気づきました。
メモリを割り当てたり、入れ子になった関数を入力する前に、 main() の非常に初期に sigaction() を使用して信号ハンドラを登録するプログラム (これは不規則な sigaction 動作を持つプログラムです)。
メインの上部にメモリなどを占める他の変数を宣言した後、sigaction()という別のプログラムがありますが、sa.sa_flagsのメモリを0(または0に初期化できる数値)で一貫した動作を提供する偶然があるように見えました。 SA_RESETHANDビットはセットされていません)。
一貫性のないアプリケーションが時々登録された信号を初期化し、アクションを一度キャプチャし、時にはすべてのアクションをキャプチャするという事実は、ゴミなどによって何らかのランダム性が発生する可能性があることに気づきました。
Johnの答えの単純なサンプルフラグメントルールはまだ不完全であり、sa_flags設定が省略され、一貫性のない結果を提供する可能性があります。