シグナルハンドラでprintfを使用しないようにするにはどうすればよいですか?


86

printfは再入可能ではないため、シグナルハンドラーで使用しても安全ではないはずです。しかし、私はprintfこの方法を使用する多くのサンプルコードを見てきました。

だから私の質問は:printfシグナルハンドラーでの使用を避ける必要があるのはいつですか、そして推奨される代替品はありますか?


12
あなたのタイトルの質問に対する単純であまり役に立たない答え:printfそのシグナルハンダーでその呼び出しを参照してください?消して。
キーストンプソン

6
こんにちはユハオ!リンクを読むのはとても面白いと思います。「より安全な信号処理のためにリエントラント関数を使用する」久しぶりに読んだので、ここで人工的なものをあなたと共有したいと思います。楽しんでいただければ幸いです。
Grijesh Chauhan 2014

回答:


58

いくつかのフラグ変数を使用し、シグナルハンドラー内でそのフラグを設定し、printf()通常の操作中にmain()またはプログラムの他の部分でそのフラグ呼び出し関数に基づいて設定できます。

printfシグナルハンドラ内から、などのすべての関数を呼び出すことは安全ではありません。便利なテクニックは、シグナルハンドラーを使用してを設定 し、メインプログラムからflagそれをチェックしflagて、必要に応じてメッセージを出力することです。

以下の例では、シグナルハンドラding()がalarm_firedSIGALRMがキャッチされたときにフラグを1に設定し、メイン関数のalarm_fired値が調べられて、条件付きで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);
}

参照:Linuxプログラミングの開始、第4版、この本では、コードが正確に説明されています(必要なもの)、第11章:プロセスとシグナル、484ページ

さらに、ハンドラー関数は非同期で呼び出すことができるため、ハンドラー関数の記述には特別な注意が必要です。つまり、ハンドラーは、プログラムの任意の時点で、予期せずに呼び出される可能性があります。非常に短い間隔で2つのシグナルが到着した場合、1つのハンドラーが別のハンドラー内で実行できます。また、宣言することをおvolatile sigatomic_t勧めします。このタイプは常にアトミックにアクセスされ、変数へのアクセスの中断に関する不確実性を回避します。(詳細な説明については、アトミックデータアクセスとシグナル処理を参照してください)。

シグナルハンドラーの定義を読んでください:signal()またはsigaction()関数で確立できるシグナルハンドラー関数を作成する方法を学びます。マニュアルページ
の許可された関数のリスト。シグナルハンドラ内でこの関数を呼び出すのは安全です。


18
宣言することをおvolatile sigatomic_t alarm_fired;
勧め


1
@GrijeshChauhan:製品のコードで作業している場合、一時停止関数を呼び出すことはできません。シグナルが発生するとフローはどこにでもある可能性があるため、その場合、「if(alarm_fired)printf( "Ding! \ n ");" コードで。
pankaj kushwaha 2016年

@pankajkushwahaはい、あなたは正しいです、それは競合状態に苦しんでいます
Grijesh Chauhan 2016

@GrijeshChauhan、私が理解できなかったことが2つあります。1.フラグをいつチェックするかをどうやって知るのですか?したがって、印刷するほぼすべてのポイントで、コードに複数のチェックポイントがあります。2.シグナル登録の前にシグナルが呼び出されたり、チェックポイントの後にシグナルが発生したりする競合状態が確実に発生します。これは特定の条件での印刷に役立つだけで、問題を完全に解決するわけではないと思います。
Darshanb19年

52

主な問題は、信号が中断しmalloc()たり、同様の機能が発生したりすると、メモリのブロックを空きリストと使用済みリストの間で移動している間、または他の同様の操作中に、内部状態が一時的に不整合になる可能性があることです。シグナルハンドラーのコードが関数を呼び出してから呼び出すmalloc()場合、これはメモリ管理を完全に破壊する可能性があります。

C標準は、シグナルハンドラーで何ができるかについて非常に保守的な見方をしています。

ISO / IEC 9899:2011§7.14.1.1signal機能

¶5信号が呼び出しの結果として以外に発生した場合abort、またはraise機能をシグナルハンドラが値を割り当てることによって以外ロックフリーアトミックオブジェクトでない静的またはスレッド貯蔵期間を持つ任意のオブジェクトを参照する場合、動作は未定義ですとして宣言されたオブジェクトに対してvolatile sig_atomic_t、またはシグナルハンドラーが、abort関数、_Exit関数、 quick_exit関数、またはsignal関数の呼び出しを引き起こしたシグナルに対応するシグナル番号に等しい最初の引数を持つ関数以外の標準ライブラリ内の関数を呼び出すハンドラ。さらに、そのようなsignal関数の呼び出しが結果としてSIG_ERR返される場合、の値errnoは不確定です。252)

252)非同期シグナルハンドラーによってシグナルが生成された場合、動作は定義されていません。

POSIXは、シグナルハンドラーで何ができるかについてはるかに寛大です。

信号の概念POSIX 2008年版では述べています:

プロセスがマルチスレッドの場合、またはプロセスがシングルスレッドであり、次の結果以外の結果としてシグナルハンドラーが実行される場合:

  • プロセスが呼び出しabort()raise()kill()pthread_kill()、またはsigqueue()ブロックされていない信号を生成します

  • ブロックが解除され、ブロックが解除された呼び出しが戻る前に配信される保留中の信号

errnoとして宣言されたオブジェクトに値を割り当てる以外の静的ストレージ期間以外のオブジェクトをシグナルハンドラーが参照するvolatile sig_atomic_t場合、またはシグナルハンドラーが、にリストされている関数の1つ以外のこの標準で定義された関数を呼び出す場合、動作は未定義です。次の表。

次の表は、非同期シグナルセーフである必要がある一連の関数を定義しています。したがって、アプリケーションは、シグナルキャッチ関数から制限なしにそれらを呼び出すことができます。

_Exit()             fexecve()           posix_trace_event() sigprocmask()
_exit()             fork()              pselect()           sigqueue()
…
fcntl()             pipe()              sigpause()          write()
fdatasync()         poll()              sigpending()

上記の表にないすべての機能は、信号に関して安全でないと見なされます。シグナルが存在する場合、POSIX.1-2008のこのボリュームで定義されているすべての関数は、シグナルキャッチ関数から呼び出されたとき、またはシグナルキャッチ関数によって中断されたときに定義されたとおりに動作します。ただし、シグナルが安全でない関数とシグナルを中断した場合は例外です。キャッチ関数は安全でない関数を呼び出します。動作は定義されていません。

の値を取得するerrno操作と値を割り当てる操作errnoは、非同期シグナルセーフでなければなりません。

シグナルがスレッドに配信されるときに、そのシグナルのアクションが終了、停止、または続行を指定している場合、プロセス全体がそれぞれ終了、停止、または継続されるものとします。

ただし、printf()関数のファミリーはそのリストに特に含まれておらず、シグナルハンドラーから安全に呼び出されない可能性があります。

POSIX 2016アップデートが含まれるように安全な機能のリストを拡張し、特に、より多くの機能<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()

その結果、et alがwrite()提供するフォーマットのサポートなしで使用するかprintf()、コードの適切な場所で(定期的に)テストするフラグを設定することになります。このテクニックは、GrijeshChauhanによる回答うまく実証されています


標準のC機能と信号の安全性

chqrlie 興味深い質問をしますが、私には部分的な答えしかありません。

からのほとんどの文字列関数<string.h>またはからの文字クラス関数、<ctype.h>およびさらに多くのC標準ライブラリ関数が上記のリストにないのはなぜですか?strlen()シグナルハンドラーからの呼び出しを危険にさらすには、実装を意図的に悪にする必要があります。

関数の多くにとって<string.h>、なぜ彼らが宣言されていなかった非同期シグナルの安全を確認することは困難である、と私は同意するだろうstrlen()と一緒に、典型的な例であるstrchr()strstr()などなど一方、他の機能strtok()strcoll()およびstrxfrm()かなり複雑で、非同期信号に対して安全ではない可能性があります。そのためstrtok()の呼び出しの間で状態を保持し、シグナルハンドラを簡単に使用しているコードの一部かどうかを言うことができませんでしたstrtok()めちゃめちゃにされるだろう。strcoll()及びstrxfrm()機能は、ロケールに依存するデータを操作し、ロケールをロードすると、状態設定のすべての種類を含みます。

の関数(マクロ)<ctype.h>はすべてロケールに依存するため、およびと同じ問題が発生する可能性がstrcoll()ありstrxfrm()ます。

<math.h>SIGFPE(浮動小数点例外)の影響を受ける可能性があるためでない限り、からの数学関数が非同期信号に対して安全でない理由を理解するのは難しいと思いますが、最近のそのうちの1つが整数の場合のみです。ゼロによる除算。同様の不確実性から生じ<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関数とヘッダーの評価のほとんどすべては、半ば知識に基づいた推測です。標準化団体からの決定的な声明は意味がありません。


からのほとんどの文字列関数<string.h>またはからの文字クラス関数、<ctype.h>およびさらに多くのC標準ライブラリ関数が上記のリストにないのはなぜですか?strlen()シグナルハンドラーからの呼び出しを安全でないものにするために、実装は意図的に悪である必要があります。
chqrlie 2016

@chqrlie:興味深い質問—更新を参照してください(コメントにそれほど多くを適切に収める方法はありませんでした)。
ジョナサンレフラー2016

詳細な分析をありがとうございます。<ctype.h>もの、それは、ロケール固有のものであり、ロケール設定機能信号の割り込みであれば問題を引き起こす可能性がありますが、ロケールがロードされると、それらを使用することは安全でなければなりません。複雑な状況では、ロケールデータの読み込みが段階的に行われる可能性があるため、関数が<ctype.h>安全でなくなる可能性があると思います。結論は残っています:疑わしいときは、控えてください。
chqrlie 2016

@chqrlie:話の教訓は、疑わしいときは棄権するべきだということに同意します。それは素晴らしい要約です。
Jonathan Leffler 2016

13

printfシグナルハンドラーでの使用を避ける方法は?

  1. 常にそれを避けてください、と言うでしょう:ただprintf()シグナルハンドラーで使用しないでください。

  2. 少なくともPOSIX準拠のシステムでは、のwrite(STDOUT_FILENO, ...)代わりにを使用できますprintf()。ただし、フォーマットは簡単ではない場合があります。書き込み関数または非同期セーフ関数を使用して、シグナルハンドラからintを出力します。


1
アルクとAlways avoid it.は?避けprintf()ますか?
Grijesh Chauhan 2013

2
@GrijeshChauhan:はい、OPがprintf()シグナルハンドラーでの使用を避ける時期を尋ねていたので。
2013年

2ポイントのAlk + 1 、シグナルハンドラーでの使用を回避する方法を尋ねるOPを確認してくださいprintf()
Grijesh Chauhan 2013

7

デバッグの目的で、実際にはasync-signal-safeリスト上の関数のみを呼び出していることを確認し、シグナルコンテキスト内で呼び出された安全でない関数ごとに警告メッセージを出力するツールを作成しました。シグナルコンテキストから非同期セーフでない関数を呼び出したいという問題は解決しませんが、少なくとも、誤って呼び出した場合を見つけるのに役立ちます。

ソースコードは GitHubにあります。これは、オーバーロードしてからsignal/sigactionPLT安全でない関数のエントリを一時的にハイジャックすることで機能します。これにより、安全でない関数の呼び出しがラッパーにリダイレクトされます。



1

独自の非同期シグナルセーフsnprintf("%dを実装して使用するwrite

思ったほど悪くはありません。Cでintをstringに変換する方法は?いくつかの実装があります。

シグナルハンドラがアクセスできる興味深いタイプのデータは2つしかないため、次のようになります。

  • sig_atomic_t グローバル
  • int シグナル引数

これは基本的にすべての興味深いユースケースをカバーしています。

strcpyシグナルセーフでもあるという事実は、物事をさらに良くします。

以下のPOSIXプログラムは、これまでにSIGINTを受信した回数を標準出力に出力します。これは、、Ctrl + CおよびシグナルIDでトリガーできます。

Ctrl + \(SIGQUIT)を使用してプログラムを終了できます。

main.c:

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>

/* Calculate the minimal buffer size for a given type.
 *
 * Here we overestimate and reserve 8 chars per byte.
 *
 * With this size we could even print a binary string.
 *
 * - +1 for NULL terminator
 * - +1 for '-' sign
 *
 * A tight limit for base 10 can be found at:
 * /programming/8257714/how-to-convert-an-int-to-string-in-c/32871108#32871108
 *
 * TODO: get tight limits for all bases, possibly by looking into
 * glibc's atoi: /programming/190229/where-is-the-itoa-function-in-linux/52127877#52127877
 */
#define ITOA_SAFE_STRLEN(type) sizeof(type) * CHAR_BIT + 2

/* async-signal-safe implementation of integer to string conversion.
 *
 * Null terminates the output string.
 *
 * The input buffer size must be large enough to contain the output,
 * the caller must calculate it properly.
 *
 * @param[out] value  Input integer value to convert.
 * @param[out] result Buffer to output to.
 * @param[in]  base   Base to convert to.
 * @return     Pointer to the end of the written string.
 */
char *itoa_safe(intmax_t value, char *result, int base) {
    intmax_t tmp_value;
    char *ptr, *ptr2, tmp_char;
    if (base < 2 || base > 36) {
        return NULL;
    }

    ptr = result;
    do {
        tmp_value = value;
        value /= base;
        *ptr++ = "ZYXWVUTSRQPONMLKJIHGFEDCBA9876543210123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[35 + (tmp_value - value * base)];
    } while (value);
    if (tmp_value < 0)
        *ptr++ = '-';
    ptr2 = result;
    result = ptr;
    *ptr-- = '\0';
    while (ptr2 < ptr) {
        tmp_char = *ptr;
        *ptr--= *ptr2;
        *ptr2++ = tmp_char;
    }
    return result;
}

volatile sig_atomic_t global = 0;

void signal_handler(int sig) {
    char key_str[] = "count, sigid: ";
    /* This is exact:
     * - the null after the first int will contain the space
     * - the null after the second int will contain the newline
     */
    char buf[2 * ITOA_SAFE_STRLEN(sig_atomic_t) + sizeof(key_str)];
    enum { base = 10 };
    char *end;
    end = buf;
    strcpy(end, key_str);
    end += sizeof(key_str);
    end = itoa_safe(global, end, base);
    *end++ = ' ';
    end = itoa_safe(sig, end, base);
    *end++ = '\n';
    write(STDOUT_FILENO, buf, end - buf);
    global += 1;
    signal(sig, signal_handler);
}

int main(int argc, char **argv) {
    /* Unit test itoa_safe. */
    {
        typedef struct {
            intmax_t n;
            int base;
            char out[1024];
        } InOut;
        char result[1024];
        size_t i;
        InOut io;
        InOut ios[] = {
            /* Base 10. */
            {0, 10, "0"},
            {1, 10, "1"},
            {9, 10, "9"},
            {10, 10, "10"},
            {100, 10, "100"},
            {-1, 10, "-1"},
            {-9, 10, "-9"},
            {-10, 10, "-10"},
            {-100, 10, "-100"},

            /* Base 2. */
            {0, 2, "0"},
            {1, 2, "1"},
            {10, 2, "1010"},
            {100, 2, "1100100"},
            {-1, 2, "-1"},
            {-100, 2, "-1100100"},

            /* Base 35. */
            {0, 35, "0"},
            {1, 35, "1"},
            {34, 35, "Y"},
            {35, 35, "10"},
            {100, 35, "2U"},
            {-1, 35, "-1"},
            {-34, 35, "-Y"},
            {-35, 35, "-10"},
            {-100, 35, "-2U"},
        };
        for (i = 0; i < sizeof(ios)/sizeof(ios[0]); ++i) {
            io = ios[i];
            itoa_safe(io.n, result, io.base);
            if (strcmp(result, io.out)) {
                printf("%ju %d %s\n", io.n, io.base, io.out);
                assert(0);
            }
        }
    }

    /* Handle the signals. */
    if (argc > 1 && !strcmp(argv[1], "1")) {
        signal(SIGINT, signal_handler);
        while(1);
    }

    return EXIT_SUCCESS;
}

コンパイルして実行します。

gcc -std=c99 -Wall -Wextra -o main main.c
./main 1

Ctrl + Cを15回押すと、ターミナルに次のように表示されます。

^Ccount, sigid: 0 2
^Ccount, sigid: 1 2
^Ccount, sigid: 2 2
^Ccount, sigid: 3 2
^Ccount, sigid: 4 2
^Ccount, sigid: 5 2
^Ccount, sigid: 6 2
^Ccount, sigid: 7 2
^Ccount, sigid: 8 2
^Ccount, sigid: 9 2
^Ccount, sigid: 10 2
^Ccount, sigid: 11 2
^Ccount, sigid: 12 2
^Ccount, sigid: 13 2
^Ccount, sigid: 14 2

どこ 2で、はの信号番号ですSIGINT

Ubuntu18.04でテスト済み。GitHubアップストリーム


0

選択ループを持つプログラムで特に役立つ1つの手法は、信号の受信時にパイプに1バイトを書き込み、選択ループで信号を処理することです。これらの線に沿った何か(簡潔にするためにエラー処理およびその他の詳細は省略されています)

static int sigPipe[2];

static void gotSig ( int num ) { write(sigPipe[1], "!", 1); }

int main ( void ) {
    pipe(sigPipe);
    /* use sigaction to point signal(s) at gotSig() */

    FD_SET(sigPipe[0], &readFDs);

    for (;;) {
        n = select(nFDs, &readFDs, ...);
        if (FD_ISSET(sigPipe[0], &readFDs)) {
            read(sigPipe[0], ch, 1);
            /* do something about the signal here */
        }
        /* ... the rest of your select loop */
    }
}

それがどの信号であったかを気する場合は、パイプの下のバイトを信号番号にすることができます。


-1

pthreadライブラリを使用している場合は、シグナルハンドラでprintfを使用できます。:printfのはデイブButenhof CFのスレッドがここに答えるためにアトミックであることを、UNIX / POSIXの指定 https://groups.google.com/forum/#!topic/comp.programming.threads/1-bU71nYgqwを 順番に鮮明な画像を取得することに注意してくださいprintf出力の 場合、GUIによって作成された疑似ttyではなく、コンソールでアプリケーションを実行する必要があります(Linuxではctl + alt + f1を使用してコンソール1を起動します)。


3
シグナルハンドラーは、別のスレッドでは実行されず、シグナル割り込みが発生したときに実行されていたスレッドのコンテキストで実行されます。この答えは完全に間違っています。
itaych 2018
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.