web-dev-qa-db-ja.com

シグナルハンドラでprintfを使用しないようにする方法は?

printfはリエントラントではないため、シグナルハンドラで使用しても安全ではないはずです。しかし、このようにprintfを使用する多くのサンプルコードを見てきました。

だから私の質問は次のとおりです。シグナルハンドラでprintfの使用を避ける必要があるのはいつですか。推奨される代替品はありますか。

78
Yu Hao

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

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

次の例では、シグナルハンドラDing()がフラグ_alarm_fired_をSIGALRMがキャッチしたときに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_を宣言することをお勧めします。この型は常にアトミックにアクセスされ、変数へのアクセスの中断に関する不確実性を回避します。 (読み取り: 原子データアクセスと信号処理 詳細な説明)。

Defining Signal Handlers を読んで、signal()またはsigaction()関数で確立できるシグナルハンドラー関数を書く方法を学びます。
manual page の許可された関数のリスト。シグナルハンドラー内でこの関数を呼び出すことは安全です。

53
Grijesh Chauhan

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

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

ISO/IEC 9899:2011§7.14.1.1signal関数

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

252) 非同期シグナルハンドラーによってシグナルが生成される場合、動作は未定義です。

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

Signal Concepts POSIX 2008エディションの説明:

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

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

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

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

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

_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()

その結果、write()などによって提供されるフォーマットサポートなしでprintf()を使用するか、コードの適切な場所でテストするフラグを(定期的に)設定することになります。この手法は answerGrijesh Chauhan で実証されています。


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

chqrlieasks おもしろい質問で、部分的な答えしかありません:

<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(浮動小数点例外)の影響を受ける可能性がある場合を除いて、非同期信号に対して安全ではない理由を理解するのは難しいと思います。 integerゼロによる除算。 <complex.h><fenv.h><tgmath.h>からも同様の不確実性が生じます。

<stdlib.h>の一部の関数は、たとえばabs()のように除外できます。その他には特に問題があります:malloc()とfamilyは主な例です。

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ヘッダーの分析は…それらの多くが存在するという点で困難であり、一部の機能は安全であるかもしれませんが、多くは安全ではないかもしれません... <pthread.h>のようなヘッダーには、3つの安全な関数と多くの安全でない関数があることに注意してください。

NB:POSIX環境でのC関数とヘッダーの評価のほとんどすべては、半学歴の推測に基づいています。標準化団体からの決定的な声明ではありません。

49

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

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

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

13
alk

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

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

6
dwks

選択ループを持つプログラムで特に有用なテクニックの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 */
    }
}

which signalを気にした場合、パイプの下のバイトはシグナル番号になります。

0
John Hascall

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

0
drlolly

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

Cでintを文字列に変換する方法 にはいくつかの実装があります。

シグナルハンドラがアクセスできるデータは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:
 * https://stackoverflow.com/questions/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: https://stackoverflow.com/questions/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

ここで、2SIGINTの信号番号です。

Ubuntu 18.04でテスト済み。 GitHubアップストリーム