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


86

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

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


12
一个简单但不是非常有用的答案,用于回答标题中的问题:printf在信号处理程序中看到该呼叫了吗?删除它。
Keith Thompson

6
您好于浩!我认为您会发现链接非常有趣,易于阅读。我已经阅读了很长时间,“使用可重入函数来更安全地处理信号”,在这里我想与您分享仿真的内容。希望你喜欢。
Grijesh Chauhan 2014年

Answers:


58

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

printf从信号处理程序中调用所有函数(例如)是不安全的。一种有用的技术是使用信号处理程序设置a 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);
}

参考:Beginning Linux Programming,第4版,在本书中,准确说明了您的代码(所需的内容),第11章:进程和信号,第484页

此外,在编写处理程序函数时需要特别小心,因为它们可以异步调用。也就是说,处理程序可能在程序中的任何地方被意外地调用。如果两个信号在很短的间隔内到达,则一个处理程序可以在另一个处理程序中运行。并且,最好声明为volatile sigatomic_t,始终以原子方式访问此类型,以避免不确定中断对变量的访问。(有关详细信息,请阅读:原子数据访问和信号处理)。

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


18
声明是更好的做法volatile sigatomic_t alarm_fired;
Basile Starynkevitch 2013年


1
@GrijeshChauhan:如果我们正在使用产品代码工作,那么我们将无法调用暂停功能,信号出现时流可以在任何地方,因此在那种情况下,我们真的不知道在哪里保存“ if(alarm_fired)printf(” Ding! \ n“);” 在代码中。
pankaj kushwaha

@pankajkushwaha是的,您是正确的,它正在遭受比赛状况的
困扰

@GrijeshChauhan,有两件事我无法理解。1.您如何知道何时检查标志?因此,几乎在每个打印点上的代码中都会有多个检查点。2.肯定会有竞争情况,在信号注册之前可能会在信号中调用信号,或者在检查点之后可能会出现信号。我认为这只会在某些情况下帮助打印,但不能完全解决问题。
达山b

52

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

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

ISO / IEC 9899:2011§7.14.1.1signal功能

¶5如果信号不是通过调用abortraise函数的结果发生的,则如果信号处理程序引用具有静态或线程存储持续时间的任何对象,而该对象不是非锁定原子对象,则该行为是不确定的,除非通过分配值声明为的对象volatile sig_atomic_t,或者信号处理程序调用标准库中的abort函数,该_Exit函数, quick_exit函数,函数或signal函数的第一个参数等于导致调用该函数的信号的信号号处理程序。此外,如果对signal函数的这种调用导致SIG_ERR返回,则的值errno不确定。252)

252)如果异步信号处理程序生成任何信号,则该行为未定义。

POSIX对您可以在信号处理程序中执行的操作更加慷慨。

POSIX 2008版中的Signal Concepts表示:

如果进程是多线程的,或者进程是单线程的,并且执行了信号处理程序,则其结果不是:

  • 的过程调用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()没有使用printf()et al提供的格式支持的情况下最终使用,要么最终在代码的适当位置设置了(定期)测试的标志。该技术巧妙地展现在回答通过Grijesh肖汉


标准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环境中使用的标准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>具有三个安全功能和许多不安全功能。

注意:在POSIX环境中,几乎所有对C函数和标头的评估都是半有根据的猜测。这是毫无意义的标准机构声明。


上面的列表为什么没有大多数字符串函数<string.h>或字符类函数<ctype.h>以及更多C标准库函数呢?为了使strlen()从信号处理程序进行的调用变得不安全,实现必须故意是邪恶的。
chqrlie

@chqrlie:一个有趣的问题—查看更新(没有任何方法可以使注释合理地适应这么多)。
乔纳森·莱夫勒

感谢您的深入分析。关于这些<ctype.h>东西,它是特定于语言环境的,如果信号中断语言环境设置功能,可能会引起问题,但是一旦语言环境被加载,使用它们就应该是安全的。我猜在某些复杂的情况下,可以增量地加载语言环境数据,从而使功能<ctype.h>不安全。结论仍然是:有疑问时,弃权。
chqrlie

@chqrlie:我同意这个故事的寓意应该是:有疑问时,弃权。这是一个很好的总结。
乔纳森·莱夫勒

13

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

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

  2. 至少在符合POSIX的系统上,您可以使用write(STDOUT_FILENO, ...)代替printf()。但是,格式化可能并不容易:使用写入或异步安全功能从信号处理程序中打印int


1
阿尔克Always avoid it.是什么意思?避免printf()
Grijesh Chauhan

2
@GrijeshChauhan:是的,因为OP询问何时避免printf()在信号处理程序中使用。
2013年

Alk +1表示2点,检查OP询问如何避免printf()在信号处理程序中使用?
Grijesh Chauhan 2013年

7

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

源代码在GitHub上。它通过重载来工作signal/sigaction,然后暂时劫持PLT不安全功能的条目;这会导致对不安全函数的调用被重定向到包装器。



1

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

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

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

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

^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上游


0

在具有选择循环的程序中特别有用的一种技术是在接收到信号后将单个字节写入管道,然后在选择循环中处理信号。这些内容(错误处理和其他细节,为了简洁起见)

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。unix / posix指定printf对于线程是原子的,请参阅Dave Butenhof的答复:https ://groups.google.com/forum/#! topic /comp.programming.threads/1-bU71nYgqw 注意,为了获得更清晰的图片对于printf输出, 您应该在控制台中运行应用程序(在Linux上,使用ctl + alt + f1启动控制台1),而不是由GUI创建的伪tty。


3
信号处理程序不在某个单独的线程中运行,它们在发生信号中断时正在运行的线程的上下文中运行。这个答案是完全错误的。
itaych
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.