【问题标题】:Is it always unsafe when I call a non-async-safe function from a signal handler?当我从信号处理程序调用非异步安全函数时,它总是不安全吗?
【发布时间】:2015-08-24 15:14:46
【问题描述】:

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

如果信号中断了不安全函数的执行,并且处理程序调用了不安全函数,则程序的行为是未定义

TLPI

SUSv3 指出表 21-1(异步安全函数列表)中未列出的所有函数都被认为对信号不安全,但指出只有在调用信号处理程序时函数才不安全 中断不安全函数的执行,并且处理程序本身也调用不安全函数

我对上述引用的解释是只有在信号处理程序没有中断非异步安全函数的情况下,才可以安全地从信号处理程序调用非异步安全函数

例如,我为 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,但在此示例中其状态是一致。而且虽然crypt()返回的是一个静态分配的字符串,但它并不与其他函数或线程共享。

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

【问题讨论】:

  • 不安全并不意味着你会总是被烧毁。
  • 只要标准说不安全,就是不安全。我们不知道是否会一直如此。 “不安全”的意思是“可能会发生坏事”,而不是“……会发生”。
  • 你在编写什么样的应用程序?它是关键的嵌入式软件还是无用的游戏演示?
  • @BasileStarynkevitch 实际上,我正在编写一个实用函数,例如 gdb 中的 backtrace 命令,它将打印调用堆栈,并将从 SIGSEGV 处理程序中调用。这是一个操作系统课程项目。
  • 这个相关的问题可能很有趣stackoverflow.com/questions/14421951/…

标签: c linux signals async-safe


【解决方案1】:

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

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

请记住,Unix signals 可以出现在用户空间中的每个 machine instruction(不仅在 C 序列点,它们具有一些明确定义的语义)处(系统调用然后是单个 SYSENTER 指令)。如果运气不好,可能会在更新malloc 的全局状态的少数机器指令中出现信号。然后将来调用malloc -e.g. 间接来自您的信号处理程序,可能会造成严重破坏(即undefined behavior)。这种不幸可能不太可能发生(但评估其概率实际上是不可能的),但您应该针对它编写代码。

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

您可能不关心异步信号安全功能,因为您认为灾难不会发生。这对于调试目的可能是可以接受的(例如,信号处理程序中的printf 经常但并非总是如此,实际上大部分时间都可以工作;GCC 编译器在内部使用它的“ async-unsafe" libbacktrace 信号处理程序中的库)用于使用 #ifndef NDEBUG 包装的代码,但它不适用于生产代码;如果您确实必须在处理程序中添加此类代码,请在评论中提及您知道您错误地调用了非异步信号安全函数,并准备好被未来从事相同工作的同事诅咒代码库。

处理这种情况的一个典型技巧是在信号处理程序中简单地设置一个volatile sig_atomic_t 标志(阅读 POSIX signal.h 文档)并在某个循环中的某个安全位置检查该标志 - 在处理程序之外 - 或到write(2) 一个或几个字节到先前在应用程序初始化时设置的pipe(7),并让该管道的读取端定期poll(2)-ed 并稍后由您的事件循环读取 - 或其他一些线程 - )。

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

【讨论】:

  • "No, ..." 应该是 "Yes, ..." 不是吗?至少如果引用默认为第一个,即标题的问题。 :-S
  • @alk:我更正为“是的”。抱歉,英语不是我的母语。
  • @BasileStarynkevitch:信号处理程序在应用程序中调用非异步安全函数是否安全,其中每个非异步安全函数都通过屏蔽适当的信号(也许使用包装函数)?只是在这里扮演魔鬼的拥护者。
  • @rici:因为许多非异步安全是系统函数。例如,您不会想要修补 malloc
  • @BasileStarynkevitch:我没有说我想这样做 :) 只是有可能。 (并且包装并不意味着修补,至少在我的使用中。我经常包装 malloc,出于某种原因 - 通常是错误检测 - 我认为 xmalloc 是一个很常见的习惯用法。)当然,它会需要包装很多函数...
【解决方案2】:

如果信号中断主程序中的 any async-unsafe 函数,则在信号处理程序中调用 any async-unsafe 函数是不安全的。 async-unsafe 函数不需要相互关联——结果是未定义的。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-03-31
    • 2018-11-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多