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

Yu *_*Hao 80 c linux signals

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

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

Gri*_*han 54

您可以使用一些标志变量,在信号处理程序内设置该标志,并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);
}
Run Code Online (Sandbox Code Playgroud)

参考:Linux初始化编程,第4版,在本书中,您的代码将被解释(您想要的是什么),第11章:进程和信号,第484页

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

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

  • 声明`volatile sigatomic_t alarm_fired;`被认为是更好的做法 (16认同)

Jon*_*ler 51

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

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

ISO/IEC 9899:2011§7.14.1.1 signal功能

5如果信号的出现不是调用abortor raise函数的结果,那么如果信号处理程序引用具有静态或线程存储持续时间但不是无锁原子对象的任何对象,则行为是未定义的,除非通过赋值到声明为的对象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()
Run Code Online (Sandbox Code Playgroud)

上表中未包含的所有功能都被认为对信号不安全.在存在信号的情况下,POSIX.1-2008的这个卷所定义的所有函数在被信号捕获函数调用或中断时应该按照定义运行,但有一个例外:当信号中断不安全的函数和信号时 - catch函数调用一个不安全的函数,行为是未定义的.

获得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()
Run Code Online (Sandbox Code Playgroud)

因此,您最终使用时write()没有printf()等人提供的格式化支持,或者您最终设置了一个标记,您可以在代码中的适当位置(定期)进行测试.该技术巧妙地展现在回答通过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函数和头文件的评估都是半教育的猜测.标准组织的明确声明是没有意义的.


alk*_*alk 13

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

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

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

  • @GrijeshChauhan:是的,因为OP要求何时避免在信号处理程序中使用`printf()`. (2认同)
  • Alk +1 表示“2”点,检查 OP 询问**如何**避免在信号处理程序中使用“printf()”? (2认同)

小智 6

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

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