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

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

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

Solutions Collecting From Web of "如何避免在信号处理程序中使用printf?"

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

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

注意在下面的例子中,signal handler ding()设置一个标志alarm_fired为1,当SIGALRM捕获并且在主函数alarm_fired值被检查时有条件地调用printf。

 static int alarm_fired = 0; void ding(int sig) // can be called asynchronousously { 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页

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

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

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

C标准对你在信号处理程序中可以做的事情非常保守:

ISO / IEC 9899:2011§7.14.1.1 signal功能

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

如果任何信号由异步信号处理程序生成,则行为是不确定的。

POSIX在信号处理程序中可以做的更加慷慨。

Signal Concepts说:

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

  • 调用abort()raise()kill()pthread_kill()sigqueue()来产生一个没有被阻塞的信号

  • 待解决的信号被解除阻塞并在解除阻塞的呼叫返回之前被传递

如果信号处理程序引用了具有静态存储持续时间的errno以外的任何对象,而不是通过为声明为volatile sig_atomic_t的对象volatile sig_atomic_t ,或者信号处理程序调用了本标准中定义的任何函数下表中列出的功能。

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

 _Exit() fexecve() posix_trace_event() sigprocmask() _exit() fork() pselect() sigqueue() abort() fstat() pthread_kill() sigset() accept() fstatat() pthread_self() sigsuspend() access() fsync() pthread_sigmask() sleep() aio_error() ftruncate() raise() sockatmark() aio_return() futimens() read() socket() aio_suspend() getegid() readlink() socketpair() alarm() geteuid() readlinkat() stat() bind() getgid() recv() symlink() cfgetispeed() getgroups() recvfrom() symlinkat() cfgetospeed() getpeername() recvmsg() tcdrain() cfsetispeed() getpgrp() rename() tcflow() cfsetospeed() getpid() renameat() tcflush() chdir() getppid() rmdir() tcgetattr() chmod() getsockname() select() tcgetpgrp() chown() getsockopt() sem_post() tcsendbreak() clock_gettime() getuid() send() tcsetattr() close() kill() sendmsg() tcsetpgrp() connect() link() sendto() time() creat() linkat() setgid() timer_getoverrun() dup() listen() setpgid() timer_gettime() dup2() lseek() setsid() timer_settime() execl() lstat() setsockopt() times() execle() mkdir() setuid() umask() execv() mkdirat() shutdown() uname() execve() mkfifo() sigaction() unlink() faccessat() mkfifoat() sigaddset() unlinkat() fchdir() mknod() sigdelset() utime() fchmod() mknodat() sigemptyset() utimensat() fchmodat() open() sigfillset() utimes() fchown() openat() sigismember() wait() fchownat() pause() signal() waitpid() fcntl() pipe() sigpause() write() fdatasync() poll() sigpending() 

所有不在上表中的功能都被认为是不安全的。 在信号出现的情况下,POSIX.1-2008定义的所有功能应该在被信号捕获功能调用或中断时定义,只有一个例外:当信号中断不安全的功能,捕捉函数调用一个不安全的函数,行为是未定义的。

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

当信号传递给线程时,如果该信号的动作指定终止,停止或继续,那么整个过程将分别终止,停止或继续。

但是, printf()系列显然不在该列表中,并且可能不会从信号处理程序安全地调用。

因此,您最终将不使用printf()等提供的格式化支持来使用write() ,或者最终设置一个标志(您定期)在代码中的适当位置进行测试。 这个技巧在Grijesh Chauhan的回答中得到了很好的证明。


标准C功能和信号安全

chrrlie 问一个有趣的问题,我只有一个部分的答案:

大多数字符串函数如何来自<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()和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><string.h>安全的
  • <tgmath.h>可能安全
  • <threads.h>可能不安全
  • <time.h> – 依赖于区域设置(但明确允许使用time()
  • <uchar.h> – 与语言环境有关
  • <wchar.h> – 与语言环境有关
  • <wctype.h> – 区域依赖

分析POSIX头文件会更困难,因为有很多这样的函数,并且一些函数可能是安全的,但很多不会是…但也更简单,因为POSIX说哪些函数是异步信号安全的(不是很多)。 请注意像<pthread.h>这样的头文件有三个安全函数和许多不安全的函数。

注意:在POSIX环境中几乎所有对C函数和头文件的评估都是半认知的猜测。 标准组织的确定性声明是毫无意义的。

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

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

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

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

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

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

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

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

如果您正在使用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。