当我从信号处理程序调用非异步安全函数时,它总是不安全吗?

Is it always unsafe when I call a non-async-safe function from a signal handler?

我只是想知道是否可以在信号处理程序中调用非异步安全函数。
来自 Linux 手册页 signal(7) 的引述:

If a signal interrupts the execution of an unsafe function, and handler calls an unsafe function, then the behavior of the program is undefined.

TLPI

SUSv3 notes that all functions not listed in Table 21-1 (list of async-safe functions) are considered to be unsafe with respect to signals, but points out that a function is unsafe only when invocation of a signal handler interrupts the execution of an unsafe function, and the handler itself also calls an unsafe function.

我对以上引述的解释是 只有在信号处理程序不中断非异步-安全功能.

例如,我为 SIGINT 安装了一个处理程序,它调用一个假设为 crypt(3) 的不安全函数,它是不可重入的,即不安全的。

sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = handler;
sigaction(SIGINT, &sa, NULL);

而且我也在main()中无限循环调用了printf(),而且我只有主线程运行。

问题是针对这个简单的示例,当处理程序中断 printf() 的执行并调用不安全函数时,我没有看到任何不好的事情发生。 AFAK,printf() 将获取控制台锁并有一个内部缓冲区来执行缓冲 I/O,但在本例中其状态为 consistent。虽然 crypt() returns 是静态分配的字符串,但它不与其他函数或线程共享。

我是不是误会了什么?我希望有人向我澄清,让信号处理程序中断主程序中不安全函数的执行并且它本身也调用不安全函数或者这样做是安全的 总是不安全在某些情况下(例如上面的简单示例)?

确实如此,所有 情况中从信号处理程序内部调用非异步信号安全函数是不安全的(除非您深入研究您的实现代码 - 例如 libc 并且您的编译器可能会为其生成代码);那么你也许可以证明调用这样一个这样的函数实际上是安全的;但是这样的证明可能是不可能的,或者需要花费数月或数年的时间,即使有静态分析器 Frama-C 的帮助,...并且需要研究所有的实现细节。

具体来说,crypt 很可能是 内部 调用 malloc (对于某些内部数据等...)。并且标准 malloc 函数显然具有一些全局状态(例如,与先前 free-d 内存区域相关的存储桶链表的向量,将被未来对 malloc 的调用重用)。

记住 Unix signals can occur at every machine instruction (not only at C sequence points, which have some well defined semantics) in user space (a system call is then a single SYSENTER instruction). With bad luck, a signal might occur in the few machine instructions which are updating the global state of malloc. Then future calls to malloc -e.g. indirectly from your signal handler, might break havoc (i.e. undefined behavior)。这种不幸可能不太可能发生(但评估其概率实际上是不可能的),但您应该针对它进行编码。

详细信息在很大程度上是特定于实现的,具体取决于您的编译器和优化标志、libc、内核、处理器架构等...

你可能不关心异步信号安全函数,打赌灾难不会发生。这对于调试目的可能是可以接受的(例如,信号处理程序中的 printf 通常 实际上 大多数时候都可以工作;并且 GCC compiler is internally using its "async-unsafe" libbacktrace 信号处理程序中的库)用于用 #ifndef NDEBUG 包装的代码,但它对生产代码不利;如果你真的必须在处理程序中添加这样的代码,请在评论中提及你知道你错误地调用了非异步信号安全函数,并准备好被未来从事相同工作的同事诅咒代码库。

处​​理这种情况的典型技巧是简单地在信号处理程序中设置一个 volatile sig_atomic_t 标志(读取 POSIX signal.h documentation) and check that flag in some safe place in some loop -outside of the handler-, or to write(2) one -or a few- bytes to a pipe(7) previously setup at application initialization, and have the read end of that pipe be periodically poll(2)-ed 并稍后由您的事件循环读取 - 或者一些其他线程-)。

(我以 malloc 为例,但您可以想到其他广泛使用的非异步信号安全函数,甚至实现特定例程,例如 64 位32 位处理器上的算术等...)。

如果信号中断 any 异步不安全函数,则在信号处理程序中调用 any 异步不安全函数是不安全的主程序。异步不安全函数不需要彼此有任何关系——结果是未定义的。

因此,在信号处理程序中安全调用异步不安全函数的唯一方法几乎是确保在调用 aysnc 不安全函数时信号永远不会发生。一种方法是用适当的 sigblock/sigsetmask 调用包装对任何异步不安全函数的 每个 调用,以确保在不安全函数运行时不会传递信号。另一个是让主程序 set/clear 在调用异步不安全函数时有一个 sigatomic 标志,并让信号处理程序在尝试调用异步不安全函数之前检查该标志。

使用 同步 信号(SIGFPESIGSEGV 之类的信号)可能会好一些,因为您可以确保异步- 不安全的函数永远不会触发这些信号,并且您不允许(或关心)使用 kill 异步发送这些信号。然而,这需要一些注意——如果你有一个 SIGSEGV 的信号处理程序来捕获对写保护内存的写入,你需要确保你永远不会以一种方式将写保护内存传递给异步不安全函数它可能会触发您的处理程序。