主な問題は、信号が malloc()
を中断した場合 または同様の関数の場合、空きリストと使用済みリストの間でメモリ ブロックを移動している間、または他の同様の操作中に、内部状態が一時的に矛盾している可能性があります。シグナル ハンドラのコードが関数を呼び出し、その関数が malloc()
を呼び出す場合 、これはメモリ管理を完全に破壊する可能性があります.
C 標準では、シグナル ハンドラでできることについて非常に保守的な見方をしています。
<ブロック引用>ISO/IEC 9899:2011 §7.14.1.1 signal
関数
¶5 abort
の呼び出し以外でシグナルが発生した場合 または raise
volatile sig_atomic_t
として宣言されたオブジェクトに値を代入する以外の方法で、ロックフリーのアトミック オブジェクトではない静的またはスレッド ストレージ期間を持つオブジェクトをシグナル ハンドラーが参照する場合、動作は未定義です。 、またはシグナルハンドラが abort
以外の標準ライブラリの関数を呼び出します 関数、_Exit
関数、quick_exit
関数、または signal
ハンドラの呼び出しの原因となったシグナルに対応するシグナル番号に等しい最初の引数を持つ関数。さらに、そのような呼び出しが signal
関数の結果は SIG_ERR
になります return、errno
の値 不確定です。
非同期シグナル ハンドラーによってシグナルが生成された場合、動作は未定義です。
POSIX は、シグナル ハンドラーでできることについて、より寛大です。
POSIX 2008 版の Signal Concepts には次のように書かれています:
<ブロック引用>プロセスがマルチスレッドの場合、またはプロセスがシングルスレッドで、次の結果以外でシグナル ハンドラーが実行される場合:
-
abort()
を呼び出すプロセス 、raise()
、kill()
、pthread_kill()
、またはsigqueue()
ブロックされていないシグナルを生成する -
ブロックが解除され、ブロックを解除した呼び出しが戻る前に配信される保留中のシグナル
シグナルハンドラが errno
以外のオブジェクトを参照する場合、動作は未定義です volatile sig_atomic_t
として宣言されたオブジェクトに値を割り当てる以外の静的ストレージ期間 、またはシグナル ハンドラーが、次の表にリストされている関数の 1 つ以外の、この標準で定義されている関数を呼び出す場合。
次の表は、async-signal-safe である関数のセットを定義しています。したがって、アプリケーションはシグナルキャッチ関数から制限なくそれらを呼び出すことができます:
_Exit() fexecve() posix_trace_event() sigprocmask()
_exit() fork() pselect() sigqueue()
…
fcntl() pipe() sigpause() write()
fdatasync() poll() sigpending()
上記の表にないすべての関数は、シグナルに関して安全でないと見なされます。シグナルが存在する場合、POSIX.1-2008 のこのボリュームで定義されているすべての関数は、シグナルをキャッチする関数から呼び出されたとき、またはシグナルをキャッチする関数によって中断されたときに、定義どおりに動作する必要があります。キャッチ関数が安全でない関数を呼び出す場合、動作は未定義です。
errno
の値を取得する操作 errno
に値を代入する操作 async-signal-safe である必要があります。
シグナルがスレッドに配信されるとき、そのシグナルのアクションが終了、停止、または継続を指定している場合、プロセス全体がそれぞれ終了、停止、または継続されます。
ただし、printf()
関数のファミリは、そのリストに特に含まれておらず、シグナル ハンドラーから安全に呼び出されない可能性があります。
POSIX 2016 update は、安全な関数のリストを拡張して、特に <string.h>
からの多数の関数を含めます。 、これは特に価値のある追加です (または特にイライラする見落としでした)。リストは次のとおりです:
_Exit() getppid() sendmsg() tcgetpgrp()
_exit() getsockname() sendto() tcsendbreak()
abort() getsockopt() setgid() tcsetattr()
accept() getuid() setpgid() tcsetpgrp()
access() htonl() setsid() time()
aio_error() htons() setsockopt() timer_getoverrun()
aio_return() kill() setuid() timer_gettime()
aio_suspend() link() shutdown() timer_settime()
alarm() linkat() sigaction() times()
bind() listen() sigaddset() umask()
cfgetispeed() longjmp() sigdelset() uname()
cfgetospeed() lseek() sigemptyset() unlink()
cfsetispeed() lstat() sigfillset() unlinkat()
cfsetospeed() memccpy() sigismember() utime()
chdir() memchr() siglongjmp() utimensat()
chmod() memcmp() signal() utimes()
chown() memcpy() sigpause() wait()
clock_gettime() memmove() sigpending() waitpid()
close() memset() sigprocmask() wcpcpy()
connect() mkdir() sigqueue() wcpncpy()
creat() mkdirat() sigset() wcscat()
dup() mkfifo() sigsuspend() wcschr()
dup2() mkfifoat() sleep() wcscmp()
execl() mknod() sockatmark() wcscpy()
execle() mknodat() socket() wcscspn()
execv() ntohl() socketpair() wcslen()
execve() ntohs() stat() wcsncat()
faccessat() open() stpcpy() wcsncmp()
fchdir() openat() stpncpy() wcsncpy()
fchmod() pause() strcat() wcsnlen()
fchmodat() pipe() strchr() wcspbrk()
fchown() poll() strcmp() wcsrchr()
fchownat() posix_trace_event() strcpy() wcsspn()
fcntl() pselect() strcspn() wcsstr()
fdatasync() pthread_kill() strlen() wcstok()
fexecve() pthread_self() strncat() wmemchr()
ffs() pthread_sigmask() strncmp() wmemcmp()
fork() raise() strncpy() wmemcpy()
fstat() read() strnlen() wmemmove()
fstatat() readlink() strpbrk() wmemset()
fsync() readlinkat() strrchr() write()
ftruncate() recv() strspn()
futimens() recvfrom() strstr()
getegid() recvmsg() strtok_r()
geteuid() rename() symlink()
getgid() renameat() symlinkat()
getgroups() rmdir() tcdrain()
getpeername() select() tcflow()
getpgrp() sem_post() tcflush()
getpid() send() tcgetattr()
その結果、 write()
を使用することになります printf()
によって提供されるフォーマットのサポートなし など、またはコードの適切な場所で(定期的に)テストするフラグを設定することになります。この手法は、Grijesh Chauhan による回答でうまく実証されています。
標準 C 関数と信号の安全性
chqrlie は興味深い質問をしますが、私には部分的な回答しかありません:
<ブロック引用>
ほとんどの文字列関数が <string.h>
から来るのはなぜですか または <ctype.h>
の文字クラス関数 さらに多くの C 標準ライブラリ関数が上記のリストにありませんか? strlen()
を作成するには、実装を意図的に悪にする必要があります。 シグナル ハンドラから呼び出すのは安全ではありません。
<string.h>
の多くの関数について 、なぜそれらが async-signal safe と宣言されなかったのか理解するのは難しいです、そして私は strlen()
に同意します strchr()
とともに代表的な例です。 、 strstr()
など。 一方、 strtok()
などの他の関数 、 strcoll()
と strxfrm()
かなり複雑であり、非同期信号に対して安全である可能性は低いです。なぜなら strtok()
呼び出し間で状態を保持し、シグナル ハンドラーはコードの一部が strtok()
を使用しているかどうかを簡単に判断できませんでした。 めちゃくちゃだろう。 strcoll()
と strxfrm()
関数はロケールに依存するデータを処理し、ロケールの読み込みにはあらゆる種類の状態設定が含まれます。
<ctype.h>
の関数 (マクロ) すべてロケールに依存するため、strcoll()
と同じ問題が発生する可能性があります と strxfrm()
.
なぜ <math.h>
の数学関数が SIGFPE (浮動小数点例外) の影響を受けない限り、非同期シグナルに対して安全ではありません。 ゼロ除算。 <complex.h>
からも同様の不確実性が生じます 、 <fenv.h>
と <tgmath.h>
.
<stdlib.h>
の関数の一部 免除される可能性があります — abs()
例えば。その他は特に問題があります:malloc()
と家族が代表的な例です。
POSIX 環境で使用される標準 C (2011) の他のヘッダーについても、同様の評価を行うことができます。 (標準 C は非常に制限的であるため、純粋な標準 C 環境でそれらを分析することに関心はありません。) 「ロケール依存」とマークされているものは、ロケールを操作するとメモリ割り当てなどが必要になる可能性があるため、安全ではありません。
<assert.h>
— おそらく安全ではない<complex.h>
— たぶん安全<ctype.h>
— 安全ではない<errno.h>
— 安全<fenv.h>
— おそらく安全ではない<float.h>
— 機能なし<inttypes.h>
— ロケールに依存する関数 (安全でない)<iso646.h>
— 機能なし<limits.h>
— 機能なし<locale.h>
— ロケールに依存する関数 (安全でない)<math.h>
— たぶん安全<setjmp.h>
— 安全ではない<signal.h>
— 許可<stdalign.h>
— 機能なし<stdarg.h>
— 機能なし<stdatomic.h>
— おそらく安全、おそらく安全ではない<stdbool.h>
— 機能なし<stddef.h>
— 機能なし<stdint.h>
— 機能なし<stdio.h>
— 安全ではない<stdlib.h>
— すべてが安全というわけではありません (許可されているものもあれば、許可されていないものもあります)<stdnoreturn.h>
— 機能なし<string.h>
— すべてが安全というわけではありません<tgmath.h>
— たぶん安全<threads.h>
— おそらく安全ではない<time.h>
— ロケール依存 (ただしtime()
明示的に許可されています)<uchar.h>
— ロケール依存<wchar.h>
— ロケール依存<wctype.h>
— ロケール依存
POSIX ヘッダーの分析は… それらがたくさんあるという点でより困難であり、一部の関数は安全かもしれませんが、多くの関数は安全ではないかもしれません…しかし、POSIX はどの関数が非同期信号に対して安全であるかを示しているため (それらの多くではない)、単純です。 <pthread.h>
のようなヘッダーに注意してください。 には 3 つの安全な関数と多くの安全でない関数があります。
注意: POSIX 環境での C 関数とヘッダーの評価のほとんどすべては、ある程度の知識に基づいた当て推量です。標準化団体からの決定的な声明は意味がありません.
<ブロック引用>
printf
の使用を避ける方法 シグナルハンドラで?
常にそれを避ける、と言う:printf()
を使用しないでください シグナルハンドラで。
少なくとも POSIX 準拠のシステムでは、 write(STDOUT_FILENO, ...)
を使用できます printf()
の代わりに .ただし、書式設定は簡単ではない場合があります:書き込みまたは非同期セーフ関数を使用して、シグナル ハンドラーから int を出力します
いくつかのフラグ変数を使用し、そのフラグをシグナルハンドラー内に設定し、そのフラグに基づいて printf()
を呼び出すことができます 通常の操作中の main() またはプログラムの他の部分の関数。
printf
などのすべての関数を呼び出すのは安全ではありません 、シグナルハンドラー内から。便利なテクニックは、シグナルハンドラーを使用して flag
を設定することです flag
を確認します メインプログラムから、必要に応じてメッセージを出力します。
以下の例では、シグナル ハンドラ ding() がフラグ alarm_fired
を設定していることに注意してください。 SIGALRMがキャッチし、メイン関数alarm_fired
で1に value を調べて、条件付きで正しく printf を呼び出します。
static int alarm_fired = 0;
void ding(int sig) // can be called asynchronously
{
alarm_fired = 1; // set flag
}
int main()
{
pid_t pid;
printf("alarm application starting\n");
pid = fork();
switch(pid) {
case -1:
/* Failure */
perror("fork failed");
exit(1);
case 0:
/* child */
sleep(5);
kill(getppid(), SIGALRM);
exit(0);
}
/* if we get here we are the parent process */
printf("waiting for alarm to go off\n");
(void) signal(SIGALRM, ding);
pause();
if (alarm_fired) // check flag to call printf
printf("Ding!\n");
printf("done\n");
exit(0);
}
リファレンス:Beginning Linux Programming、第 4 版、この本ではコードが正確に説明されています (必要なもの)、第 11 章:プロセスとシグナル、484 ページ
さらに、ハンドラー関数は非同期で呼び出される可能性があるため、ハンドラー関数の作成には特別な注意が必要です。つまり、ハンドラはプログラムの任意の時点で予期せず呼び出される可能性があります。非常に短い間隔で 2 つのシグナルが到着した場合、1 つのハンドラーが別のハンドラー内で実行できます。 volatile sigatomic_t
を宣言することをお勧めします。 、この型は常にアトミックにアクセスされるため、変数へのアクセスの中断に関する不確実性を回避できます。 (読み取り:詳細な有効期限のためのアトミック データ アクセスとシグナル処理)。
シグナルハンドラの定義を読む:signal()
で確立できるシグナルハンドラ関数の書き方を学ぶ または sigaction()
機能。
マニュアル ページの許可された関数のリスト。シグナル ハンドラ内でこの関数を呼び出すことは安全です。