提问人:Yu Hao 提问时间:6/3/2013 最后编辑:Yu Hao 更新时间:12/20/2020 访问量:47845
如何避免在信号处理程序中使用 printf?
How to avoid using printf in a signal handler?
问:
由于不是可重入的,因此在信号处理程序中使用它应该是不安全的。但是我见过很多使用这种方式的示例代码。printf
printf
所以我的问题是:我们什么时候需要避免在信号处理程序中使用,是否有推荐的替代品?printf
答:
您可以使用一些标志变量,在信号处理程序中设置该标志,并在正常操作期间基于该标志调用函数在 main() 或程序的其他部分。printf()
从信号处理程序中调用所有函数(如 )是不安全的。 一个有用的技术是使用信号处理程序来设置一个,然后从主程序中检查它,并在需要时打印一条消息。
printf
flag
flag
请注意,在下面的示例中,信号处理程序 ding() 将标志设置为 1,因为 SIGALRM 被捕获,并在 main 函数值中检查以有条件地正确调用 printf。alarm_fired
alarm_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()
评论
volatile sigatomic_t alarm_fired;
主要问题是,如果信号中断或某些类似功能,则在空闲列表和已用列表之间移动内存块或其他类似操作时,内部状态可能会暂时不一致。如果信号处理程序中的代码调用一个函数,然后调用 ,这可能会完全破坏内存管理。malloc()
malloc()
C 标准对信号处理程序中可以执行的操作采取了非常保守的观点:
ISO/IEC 9899:2011 §7.14.1.1 函数
signal
¶5 如果信号不是由于调用 or 函数的结果而发生的,则 如果信号处理程序引用任何具有静态或线程的对象,则行为未定义 不是无锁原子对象的存储持续时间,除非通过将值分配给 对象声明为 ,或者信号处理程序调用任何函数 在除函数之外的标准库中,函数、函数或第一个参数等于 与导致调用处理程序的信号相对应的信号编号。 此外,如果对函数的这种调用导致返回,则 的值不确定。252)
abort
raise
volatile sig_atomic_t
abort
_Exit
quick_exit
signal
signal
SIG_ERR
errno
252) 如果任何信号是由异步信号处理程序生成的,则行为是未定义的。
POSIX对你在信号处理程序中可以做的事情要慷慨得多。
POSIX 2008 版中的信号概念 说:
如果进程是多线程的,或者如果进程是单线程的,并且执行信号处理程序不是由于以下原因:
调用 、 、 、 或 以生成未被阻塞的信号的进程
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()
因此,您最终要么在没有 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 函数和标头的评估都是半有根据的猜测。这是标准机构的明确声明,这是没有意义的。
评论
<string.h>
<ctype.h>
strlen()
<ctype.h>
<ctype.h>
trap
rm
trap
trap
如何避免在信号处理程序中使用?
printf
总是避免它,会说:只是不要在信号处理程序中使用。
printf()
至少在符合 POSIX 标准的系统上,您可以使用 .但是,格式化可能并不容易: 使用写入或异步安全函数从信号处理程序打印 int
write(STDOUT_FILENO, ...)
printf()
评论
Always avoid it.
printf()
printf()
2
printf()
出于调试目的,我编写了一个工具,用于验证您实际上是否只调用列表中的函数,并为在信号上下文中调用的每个不安全函数打印警告消息。虽然它不能解决想要从信号上下文调用非异步安全函数的问题,但它至少可以帮助您找到意外调用的情况。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。
评论
实现您自己的 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
其中 是 的信号编号。2
SIGINT
在 Ubuntu 18.04 上测试。GitHub 上游。
您也可以直接使用异步信号安全函数。write()
#include <unistd.h>
int main(void) {
write(1,"Hello World!", 12);
return 0;
}
评论
printf