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

How to avoid using printf in a signal handler?

提问人:Yu Hao 提问时间:6/3/2013 最后编辑:Yu Hao 更新时间:12/20/2020 访问量:47845

问:

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

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

C Linux 信号

评论

14赞 Keith Thompson 6/7/2013
对于标题中的问题,一个简单但不太有用的答案:看到那个信号手中的那个电话了吗?删除它。printf
6赞 Grijesh Chauhan 4/26/2014
你好,余浩!我想你会发现链接非常有趣。“使用可重入功能实现更安全的信号处理”这么久都看完了,想在这里和大家分享一下人工。希望你喜欢。
1赞 jww 1/28/2018
另请参阅使用写入或异步安全函数从信号处理程序打印 int
0赞 Simon Sobisch 6/13/2022
由于上面发布的好文章已经消失了 - 从 web.archive.org/web/20210121185226/http://www.ibm.com/... (当然,我们都直接将errno存储在设置它的函数之后......

答:

62赞 Grijesh Chauhan 6/3/2013 #1

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

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

请注意,在下面的示例中,信号处理程序 ding() 将标志设置为 1,因为 SIGALRM 被捕获,并在 main 函数值中检查以有条件地正确调用 printf。alarm_firedalarm_fired

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

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

评论

24赞 Basile Starynkevitch 6/3/2013
声明被认为是更好的做法volatile sigatomic_t alarm_fired;
0赞 Grijesh Chauhan 4/17/2014
添加链接“在此信号处理程序期间会发生什么?
1赞 pankaj kushwaha 10/25/2016
@GrijeshChauhan:如果我们在做一个代码的产品,那么我们就不能调用暂停函数,当信号出现时,流可以在任何地方,所以在这种情况下,我们真的不知道在代码中把“if (alarm_fired) printf(”Ding!\n“);”放在哪里。
0赞 Grijesh Chauhan 10/26/2016
@pankajkushwaha是的,你是对的,它正在遭受竞争条件
0赞 Darshan b 3/5/2019
@GrijeshChauhan,有两件事我无法理解。1.你怎么知道什么时候检查标志?因此,代码中几乎每个要打印的点都会有多个检查点。2.肯定会有竞争条件,在信号注册之前可能会调用信号,或者信号可能会在检查点之后发生。我认为这只会在某些情况下帮助打印,但并不能完全解决问题。
71赞 Jonathan Leffler 6/3/2013 #2

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

C 标准对信号处理程序中可以执行的操作采取了非常保守的观点:

ISO/IEC 9899:2011 §7.14.1.1 函数signal

¶5 如果信号不是由于调用 or 函数的结果而发生的,则 如果信号处理程序引用任何具有静态或线程的对象,则行为未定义 不是无锁原子对象的存储持续时间,除非通过将值分配给 对象声明为 ,或者信号处理程序调用任何函数 在除函数之外的标准库中,函数、函数或第一个参数等于 与导致调用处理程序的信号相对应的信号编号。 此外,如果对函数的这种调用导致返回,则 的值不确定。252)abortraisevolatile sig_atomic_tabort_Exitquick_exitsignalsignalSIG_ERRerrno

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

POSIX对你在信号处理程序中可以做的事情要慷慨得多。

POSIX 2008 版中的信号概念 说:

如果进程是多线程的,或者如果进程是单线程的,并且执行信号处理程序不是由于以下原因:

  • 调用 、 、 、 或 以生成未被阻塞的信号的进程abort()raise()kill()pthread_kill()sigqueue()

  • 正在解除阻止并在解除阻止的呼叫之前传递的待处理信号,该呼叫返回

如果信号处理程序引用静态存储持续时间以外的任何对象,而不是通过将值分配给声明为 的对象,或者如果信号处理程序调用本标准中定义的任何函数,而不是下表中列出的函数之一,则该行为未定义。errnovolatile sig_atomic_t

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

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

上表中未列出的所有功能都被认为对信号不安全。在存在信号的情况下,POSIX.1-2008 本卷定义的所有函数在从信号捕获函数调用或被信号捕获函数中断时的行为应与定义相同,但有一个例外:当信号中断不安全功能时,信号捕获函数调用不安全函数时,行为是未定义的。

获取 的值的操作和为其赋值的操作应是异步信号安全的。errnoerrno

当一个信号被传递到一个线程时,如果该信号的动作指定了终止、停止或继续,则整个进程应分别终止、停止或继续。

但是,该列表中明显缺少函数系列,并且可能无法从信号处理程序安全地调用。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 提供的格式支持的情况下使用,要么最终设置了一个标志,并在代码的适当位置(定期)进行测试。Grijesh Chauhan回答很好地证明了这种技术。write()printf()


标准 C 函数和信号安全

chqrlie 提出了一个有趣的问题,对此我只有一个部分答案:

为什么大多数字符串函数 from 或字符类函数 from 以及更多的 C 标准库函数不在上面的列表中?一个实现需要故意邪恶,以使从信号处理程序调用变得不安全。<string.h><ctype.h>strlen()

对于 中的许多函数,很难看出为什么它们没有被声明为异步信号安全,我同意这是一个典型的例子,还有 、 等。另一方面,其他函数(如 和 )相当复杂,不太可能是异步信号安全的。因为在调用之间保留状态,并且信号处理程序无法轻易判断正在使用的代码的某些部分是否会被搞砸。和 函数处理区分区域设置的数据,加载区域设置涉及各种状态设置。<string.h>strlen()strchr()strstr()strtok()strcoll()strxfrm()strtok()strtok()strcoll()strxfrm()

函数(宏)都对区域设置敏感,因此可能会遇到与 和 相同的问题。<ctype.h>strcoll()strxfrm()

我发现很难理解为什么数学函数不是异步信号安全的,除非是因为它们可能受到 SIGFPE(浮点异常)的影响,尽管这些天我唯一一次看到其中之一是整数除以零。类似的不确定性来自 和 。<math.h><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 会说明哪些函数是异步信号安全的(数量不多)。请注意,标头 like 具有三个安全函数和许多不安全函数。<pthread.h>

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

评论

0赞 chqrlie 12/10/2016
为什么大多数字符串函数 from 或字符类函数 from 以及更多的 C 标准库函数不在上面的列表中?一个实现需要故意邪恶,以使从信号处理程序调用变得不安全。<string.h><ctype.h>strlen()
0赞 Jonathan Leffler 12/11/2016
@chqrlie:有趣的问题——请参阅更新(没有办法明智地将这么多内容放入评论中)。
0赞 chqrlie 12/11/2016
感谢您的深入分析。关于这些东西,它是特定于语言环境的,如果信号中断语言环境设置功能可能会导致问题,但是一旦加载了语言环境,使用它们应该是安全的。我想,在一些复杂的情况下,加载语言环境数据可以以增量方式完成,从而使函数变得不安全。结论仍然是:如有疑问,请弃权。<ctype.h><ctype.h>
0赞 Jonathan Leffler 12/11/2016
@chqrlie:我同意这个故事的寓意应该是:当有疑问时,弃权。这是一个很好的总结。
1赞 Jonathan Leffler 5/31/2023
如果你问你是否要担心你在 shell 脚本中做什么和捕捉信号,答案是“你不必担心”。shell 为您处理这一切。您需要执行整个命令(例如 删除临时文件)中,这与在 C 代码中捕获信号的工作水平截然不同。当然,shell 是 C 语言,但它会仔细检测到信号到达,然后在信号处理程序之外执行操作。traprmtraptrap
14赞 alk 6/3/2013 #3

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

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

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

评论

1赞 Grijesh Chauhan 6/3/2013
Alk 是什么意思?避免?Always avoid it.printf()
2赞 alk 6/3/2013
@GrijeshChauhan:是的,因为 OP 询问何时避免在信号处理程序中使用。printf()
2赞 Grijesh Chauhan 6/3/2013
Alk +1 表示点,检查 OP 询问如何避免在信号处理程序中使用?2printf()
7赞 dwks 3/27/2016 #4

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

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

评论

0赞 Ciro Santilli OurBigBook.com 9/20/2017
GCC 功能请求:gcc.gnu.org/ml/gcc-help/2012-03/msg00210.html
2赞 John Hascall 5/25/2017 #5

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

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 */
    }
}

如果你关心它是哪个信号,那么管道上的字节可以是信号号。

-2赞 drlolly 10/17/2017 #6

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

评论

5赞 itaych 4/11/2018
信号处理程序不会在某个单独的线程中运行,而是在发生信号中断时正在运行的线程的上下文中运行。这个答案是完全错误的。
3赞 Ciro Santilli OurBigBook.com 9/3/2018 #7

实现您自己的 async-signal-safe snprintf(“%d 并使用 write

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

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

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

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

事实上,信号安全也使事情变得更好。strcpy

下面的 POSIX 程序打印到到目前为止它接收 SIGINT 的次数,您可以使用 和 和 信号 ID 触发该次数。Ctrl + C

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

主.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 上游

0赞 Samuel Rodríguez 12/19/2020 #8

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

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