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

pil*_*row 5 c posix signals 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

Nom*_*mal 5

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

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

POSIX.1标准将其定义为未定义行为,不是因为它期望程序避免这种访问,而是因为定义安全访问情况会过于复杂并且可能限制将来的实现,因为有一个井很少甚至没有增益所有此类访问的已知解决方法:捕获信号的专用线程.


添加于2018-06-21:

让我们先来总结情况下sem_post(&sem)访问有效的信号处理(即,一个可以通过任何异步信号是指具有静态存储持续时间的对象,例如安全功能)的基础上,POSIX.1-2018:

  • 当处理仅具有一个螺纹,所述信号处理程序作为在同一过程调用线程的结果来执行abort(),raise(),kill(),pthread_kill(),或sigqueue(),并且该信号被/没有被阻挡在被用来执行处理程序的线程.

  • 当进程只有一个线程时,信号在挂起时被阻塞,并且在取消阻塞信号返回的调用之前传递.

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

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

措辞应该有访问而不是指的可能性很小.确实这确实允许将具有静态存储持续时间的任何对象的地址传递给async-signal安全函数,就像sem_post()在多线程进程中一样,例如Carlo Wood的回答假设.

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

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


确切地说信号处理程序可以安全访问的对象的问题很复杂.POSIX标准起草人不是打开整个蠕虫病毒,而是抨击它,并声明行为未定义.

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

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

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

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

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

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

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

简单地说:由于众所周知的变通方法存在,并且定义了安全的访问情况太复杂,限制未来的实现,POSIX标准完全不规范这种传统的用例.这不是因为预计它不起作用 - 恰恰相反; 它在所有当前的POSIXy系统中都能正常工作 - 但是因为定义安全访问案例(除了errnovolatile sig_atomic_t需要并得到POSIX C编译器的支持之外)并不值得复杂和可能的限制.