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

时间:2013-06-03 06:23:45

标签: c linux signals

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

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

8 个答案:

答案 0 :(得分:52)

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

  

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

请注意,在下面的示例中,信号处理程序ding()在捕获SIGALRM时将标志alarm_fired设置为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, 4th Edition,在本书中,您的代码将被解释(您想要的是什么),第11章:流程和信号,第484页

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

阅读Defining Signal Handlers:了解如何编写可以使用signal()sigaction()函数建立的信号处理函数。
manual page中的授权函数列表,在信号处理程序中调用此函数是安全的。

答案 1 :(得分:13)

  

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

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

  2. 至少在符合POSIX标准的系统上,您可以使用write(STDOUT_FILENO, ...)代替printf()。但格式化可能并不容易:Print int from signal handler using write or async-safe functions

答案 2 :(得分:6)

出于调试目的,我编写了一个工具,用于验证您实际上只调用async-signal-safe列表中的函数,并为信号上下文中调用的每个不安全函数输出警告消息。虽然它没有解决想要从信号上下文中调用非异步安全函数的问题,但它至少可以帮助您找到意外完成的情况。

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

答案 3 :(得分: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 */
    }
}

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

答案 4 :(得分:0)

如果您使用的是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。

答案 5 :(得分:0)

实施自己的异步信号安全write并使用sig_atomic_t

How to convert an int to string in C?并没有我想象的那么糟。

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

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

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

Ctrl + C也是安全信号这一事实使事情变得更好。

下面的POSIX程序会打印以输出到目前为止接收到SIGINT的次数,您可以使用Ctrl + \以及和信号ID进行触发。

您可以使用#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; } (SIGQUIT)退出程序。

main.c:

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

编译并运行:

^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

按Ctrl + C 15次后,终端显示:

2

其中SIGINT是{{1}}的信号号。

在Ubuntu 18.04上测试。 GitHub upstream

答案 6 :(得分:0)

您也可以直接使用 write(),这是一个异步信号安全函数。

#include <unistd.h> 
 
int main(void) { 
    write(1,"Hello World!", 12); 
    return 0; 
}

答案 7 :(得分:-1)

AFAI的代码非常可靠,请尽量缩短信号处理程序的代码块(主体)。我认为这就是为什么他们设计出称为sig_atomic_t的类型的原因。因此,应该遵循的方法是上面公认的答案。