繁体   English   中英

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

[英]sem_post, signal handlers, and 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对信号动作的处理似乎不允许引用该信号量本身:

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

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

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

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结构中的某些字段具有不同的值。

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

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

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


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

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

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

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

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

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

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

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

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

依赖于不确定的行为。 明确声明 sem_post()函数应该是异步信号安全的,并且可以从信号捕获函数调用。

在代码sem_post(&sem) 没有访问 (读或写) 变量 参数是常量 (某个地址)。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM