sem_post,信号处理程序和未定义的行为

时间:2018-02-02 14:31:40

标签: c signals posix semaphore undefined-behavior

在信号处理程序中使用 sem_post()是否依赖于未定义的行为?

/* 
 * excerpted from the 2017-09-15 Linux man page for sem_wait(3)
 * http://man7.org/linux/man-pages/man3/sem_wait.3.html
 */
...
sem_t sem;
...
static void
handler(int sig)
{
    write(STDOUT_FILENO, "sem_post() from handler\n", 24);
    if (sem_post(&sem) == -1) {
        write(STDERR_FILENO, "sem_post() failed\n", 18);
        _exit(EXIT_FAILURE);
    }
}

信号量 sem 具有静态存储持续时间。虽然对 sem_post ()的调用是异步信号安全的,但POSIX.1-2008 treatment of signal actions似乎不允许引用该信号量本身:

  

如果信号处理程序引用静态存储持续时间以外的任何对象,而不是通过为声明为 volatile sig_atomic_t的对象赋值 errno ,则行为未定义,或者如果信号处理程序调用此标准中定义的任何函数而不是[明确的异步信号安全函数]之一。

2 个答案:

答案 0 :(得分:5)

技术上,是的;有些情况下行为未定义。

我自己使用这种模式很多,几乎所有我已经看过的信号感知程序也是如此。它有望在实践中运行,并且可以跨系统移植,即使没有任何标准规定。

POSIX.1标准将其定义为未定义行为,不是因为它期望程序避免这种访问,而是因为定义安全访问情况会过于复杂并且可能限制将来的实现,因为那里几乎没有收益对于所有这些访问来说,这是一个众所周知的解决方法:捕获信号的专用线程。

于2018-06-21添加:

让我们首先总结信号处理程序中sem_post(&sem) access 有效的情况(即,可以引用具有静态存储持续时间的对象,例如通过任何异步信号安全功能),基于POSIX.1-2018

  • 当进程只有一个线程时,信号处理程序作为同一进程中线程的结果执行,调用abort()raise()kill(),{{ 1}}或pthread_kill(),并且在用于执行处理程序的线程中未阻止信号。

  • 当进程只有一个线程时,信号在挂起时被阻止,并且在解锁信号返回的调用之前传递。

这省略了最常见的情况:多线程进程,以及在进程外部生成的信号的处理程序(例如,进程在前台运行时的SIGINT,用户按 Ctrl + C ;或运行进程的会话关闭时的SIGHUP。)

我对这种情况的理解是,每个人都希望通过异步信号安全功能引用具有静态存储持续时间的对象的信号处理程序不会在任何理智的POSIXy架构上触发未定义的行为。如果在具有静态存储持续时间的对象上使用多线程安全(MT安全)异步信号安全函数,它将在多线程进程中与在单线程进程中完全相同;由sigqueue()alarm()setitimer()触发的信号与由timer_settime()raise()触发的信号相同;并且其他进程发送的信号与目标进程中sigqueue()raise()触发的信号的行为相同;唯一的区别是siginfo结构中的某些字段具有不同的值。

措辞应该有访问而不是引用的可能性很小。这确实允许将具有静态存储持续时间的任何对象的地址传递给异步信号安全函数,如sigqueue(),即使在多线程进程中,如Carlo Wood's answer posits。

但是,我认为这种措辞的原因更为微妙,并且涉及有关并发访问的硬件实现的差异以及执行上下文信号处理程序:在某些POSIX操作系统可能表现不同的情况下的行为太复杂了标准化,所以只是简单地保留未定义。

我的答案的其余部分试图描述这些,对于那些希望生成可用于所有POSIXy系统的可靠,强大的程序的开发人员,并且不理解POSIX.1规范中当前措辞的微妙性。

信号处理程序可以安全访问哪些对象的问题很复杂。 POSIX标准起草人不是打开整个蠕虫病毒,而是对它进行了抨击,并宣称行为未定义。

最难定义的部分是与并发访问和陷阱表示相关的细节。不仅是同一进程中的其他线程,还有内核。 (因为我们只考虑具有静态存储持续时间的对象,所以我们可以避免共享内存和所有相关的复杂性。)特别是,如果一个对象具有陷阱表示,并且该对象是非原子地修改的,则可能是中间阶段任务导致陷阱。尽管某些架构可能存在硬件限制,但陷阱本身可能会导致信号上升。

因此,与陷阱表示相关的任何内容基本上都太复杂,无法在标准中解决。

好吧,让我们假设标准会限制对具有静态存储持续时间的对象的安全读取访问,这些对象不会被中断的线程,进程中的任何其他线程以及内核同时修改;写访问具有静态存储持续时间的对象,这些对象不会被中断的线程,进程中的任何其他线程以及内核同时读取或修改。并且被访问的对象根本没有陷阱表示。

我们仍需要考虑一些特定于硬件的信号:sem_post()SIGSEGVSIGBUSSIGILL至少。遗憾的是,一些架构可能还有其他未知的信号,因此我们需要定义受影响的信号类型:内核在访问内存时引发的信号(SIGFPE仅在架构时在加载值时引发它,而不仅仅是在对这些值进行算术等时)。如果访问具有静态存储持续时间的对象可能会引发其中一个信号,则访问不安全,因为它可能导致级联的信号处理程序。 (因为标准POSIX信号没有排队,所以每种类型的第一个信号都会被执行,并且进程状态可能会丢失,从而迫使内核终止进程。)

从POSIX C编译器的角度来看,如果您考虑将指针作为有效负载(SIGFPE中的si_value.sival_ptr)获取的信号处理程序,则整个情况会变得复杂得多:访问引导到未定义的行为,取决于目标是否具有静态存储持续时间?

在所有当前的POSIXy系统上,通过原子内置函数访问静态存储持续时间对象,或者当它们没有被任何其他线程读取/修改或内核和中间存储形式不会引起信号时,在POSIX实时信号处理程序中,或在不由内存访问引发的POSIX信号处理程序中,是安全的。这可能是但不保证,将来也是如此。这就是为什么POSIX标准没有将其标准化的核心。

冷酷的事实是,对于需要访问具有静态存储持续时间的对象的所有模式,存在符合POSIX的解决方法:专用于通过siginfo_t处理信号的单独线程,所有这些信号都被阻止所有其他线程。该线程不仅限于使用异步信号安全功能,其他信号处理程序限制也不适用于它。 (如果我们考虑信号传递与它中断的代码之间的相互作用,即使使用sigwaitinfo()标志定义的处理程序,也可以认为基于线程的方法是两者中较好的一种。)

简单地说:因为存在已知的变通方法,并且定义安全访问案例会过于复杂并限制未来的实现,所以POSIX标准根本不会标准化这个传统的用例。这不是因为预计它不起作用 - 恰恰相反;它在所有当前的POSIXy系统中都能正常工作 - 但是因为定义安全访问案例(SA_RESTARTerrno除外)并不值得复杂和可能的限制,这两者都需要并得到POSIX C编译器。)

答案 1 :(得分:1)

依赖于未定义的行为。 explicitly stated sem_post()函数应该是异步信号安全的,可以从信号捕获函数中调用。

在代码sem_post(&sem)

未访问任何变量(读取或写入)。参数是常量(某个地址)。

与问题无关,但请注意,除了访问volatile sig_atomic_t之外,还允许访问无锁原子变量。我补充说,因为我怀疑信号量在内部使用无锁原子而不是易失性sig_atomic_t。后者只是" atomic"在一个线程/核心上;不是线程安全的。例如。存在单个汇编指令,它们递增非原子积分类型,因此不能被信号处理程序中断;但是对于其他线程(没有总线锁定),它仍然以非原子方式执行读取 - 增量 - 写入。由于信号量旨在从线程A发送到线程B,因此不太可能使用sig_atomic_t给出的保证:它仍然需要一个互斥锁用于访问,这不是信号安全的。