93

由于printf不可重入,因此在信号处理程序中使用它不应该是安全的。但是我已经看到很多使用printf这种方式的示例代码。

所以我的问题是:我们什么时候需要避免printf在信号处理程序中使用,是否有推荐的替代品?

4

8 回答 8

61

您可以使用一些标志变量,在信号处理程序中设置该标志,并printf()在正常操作期间基于 main() 或程序的其他部分中的该标志调用函数。

printf从信号处理程序中调用所有函数(例如 )是不安全的。一个有用的技术是使用信号处理程序来设置 aflag然后flag 从主程序中检查它并在需要时打印一条消息。

请注意,在下面的示例中,信号处理程序 ding() 将标志设置alarm_fired为 1,因为 SIGALRM 被捕获,并且在主函数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);
}

参考:Beginning Linux Programming,第 4 版,在本书中准确地解释了您的代码(您想要什么),第 11 章:进程和信号,第 484 页

此外,您在编写处理函数时需要特别小心,因为它们可以异步调用。也就是说,处理程序可能会在程序中的任何位置被调用,这是不可预测的。如果两个信号在很短的时间间隔内到达,一个处理程序可以在另一个处理程序内运行。并且认为更好的做法是声明volatile sigatomic_t,这种类型总是以原子方式访问,避免中断访问变量的不确定性。(阅读:原子数据访问和信号处理详细说明)。

阅读定义信号处理程序:了解如何编写可以使用signal()或函数建立的信号处理程序sigaction()函数。手册页
中的授权函数列表,在信号处理程序中调用此函数是安全的。

于 2013-06-03T06:27:49.070 回答
61

主要问题是,如果信号中断malloc()或某些类似功能,内部状态可能会在空闲和已用列表之间移动内存块或其他类似操作时暂时不一致。如果信号处理程序中的代码调用一个函数,然后调用malloc(),这可能会完全破坏内存管理。

C 标准对您可以在信号处理程序中执行的操作持非常保守的看法:

ISO/IEC 9899:2011 §7.14.1.1signal功能

¶5 如果信号的出现不是调用abortorraise函数的结果,则如果信号处理程序引用具有静态或线程存储持续时间的任何对象,该对象不是无锁原子对象,则行为未定义,而不是通过赋值到声明为 的对象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,或者如果信号处理程序调用本标准中定义的任何函数而不是列出的函数之一,则行为未定义下表。

下表定义了一组异步信号安全的功能。因此,应用程序可以不受限制地从信号捕获函数调用它们:

_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()没有 et al 提供的格式支持的情况下使用,要么最终printf()在代码中的适当位置设置了一个您(定期)测试的标志。Grijesh Chauhan的回答巧妙地证明了这种技术。


标准 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(浮点异常)的影响,尽管这些天我看到其中一个是整数被零除。类似的不确定性来自<complex.h>和。<fenv.h><tgmath.h>

例如,<stdlib.h>可以免除其中的一些功能。abs()其他人则特别有问题:malloc()家庭就是最好的例子。

可以对 POSIX 环境中使用的 Standard 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 说哪些功能是异步信号安全的(不是很多)。请注意,头文件 like<pthread.h>具有三个安全功能和许多不安全功能。

注意:几乎所有在 POSIX 环境中对 C 函数和头文件的评估都是半受过教育的猜测。标准机构的明确声明毫无意义。

于 2013-06-03T07:20:47.537 回答
14

如何避免printf在信号处理程序中使用?

  1. 总是避免它,会说:只是不要printf()在信号处理程序中使用。

  2. 至少在符合 POSIX 的系统上,您可以write(STDOUT_FILENO, ...)使用printf(). 然而,格式化可能并不容易:使用 write 或 async-safe 函数从信号处理程序中打印 int

于 2013-06-03T08:44:44.167 回答
7

出于调试目的,我编写了一个工具来验证您实际上只是在调用async-signal-safe列表中的函数,并为在信号上下文中调用的每个不安全函数打印一条警告消息。虽然它不能解决想要从信号上下文调用非异步安全函数的问题,但它至少可以帮助您找到您不小心这样做的情况。

源代码在 GitHub 上。它的工作原理是重载signal/sigaction,然后临时劫持PLT不安全函数的入口;这会导致对不安全函数的调用被重定向到包装器。

于 2016-03-27T00:00:20.510 回答
2

实现您自己的异步信号安全snprintf("%d并使用write

它没有我想象的那么糟糕,如何在 C 中将 int 转换为字符串?有几个实现。

由于信号处理程序只能访问两种有趣的数据类型:

  • sig_atomic_t全局变量
  • int信号参数

这基本上涵盖了所有有趣的用例。

strcpy这也是信号安全的事实使事情变得更好。

下面的 POSIX 程序将它迄今为止收到 SIGINT 的次数打印到标准输出,您可以使用Ctrl + C、 和 信号 ID 来触发。

Ctrl + \您可以使用(SIGQUIT)退出程序。

主.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 十五次后,终端显示:

^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

在 Ubuntu 18.04 上测试。GitHub 上游.

于 2018-09-03T07:38:20.440 回答
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 */
    }
}

如果您关心它是哪个信号,那么管道下方的字节可以是信号编号。

于 2017-05-25T14:40:09.630 回答
0

您也可以write()直接使用异步信号安全功能。

#include <unistd.h> 
 
int main(void) { 
    write(1,"Hello World!", 12); 
    return 0; 
}
于 2020-12-19T15:58:11.310 回答
-1

如果您使用 pthread 库,则可以在信号处理程序中使用 printf。unix/posix 指定 printf 对于线程来说是原子的 cf Dave Butenhof 在此处回复: https ://groups.google.com/forum/#!topic/comp.programming.threads/1-bU71nYgqw 请 注意,为了获得更清晰的图片对于 printf 输出, 您应该在控制台中运行您的应用程序(在 linux 上使用 ctl+alt+f1 来启动控制台 1),而不是由 GUI 创建的伪 tty。

于 2017-10-17T15:29:31.530 回答