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

标签 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似乎不允许引用该信号量本身:

[T]he behavior is undefined if the signal handler refers to any object other than errno with static storage duration other than by assigning a value to an object declared as volatile sig_atomic_t, or if the signal handler calls any function defined in this standard other than one of the [explicitly async-signal-safe functions].

最佳答案

从技术上讲,是的;在某些情况下,行为是不确定的。

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

POSIX.1标准将其定义为“未定义的行为”,不是因为它希望程序避免这种访问,而是因为定义安全访问的情况太复杂了,并且可能会限制将来的实现,因为这样做的好处很小甚至没有好处。所有此类访问的已知解决方法:捕获信号的专用线程。

添加于2018-06-21:

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

  • 当进程只有一个线程时,信号处理程序将作为线程的结果在同一进程中执行,该进程调用abort()raise()kill()pthread_kill()sigqueue(),并且信号已/未在使用的线程中被阻塞执行处理程序。
  • 当进程只有一个线程时,信号变为挂起状态时将被阻止,并且在取消阻止该信号的调用返回之前将其传递。

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

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

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

    但是,我相信此措辞的原因比较微妙,涉及并发访问和上下文信号处理程序在以下硬件实现方面的差异:在某些POSIX OS可能表现不同的情况下的行为过于复杂,无法标准化;因此,只需将其保留为Undefined即可。

    我的其余答案试图为那些希望生成可在所有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编译器并获得其支持)并不值得付出复杂性和可能的​​限制。

    关于c - sem_post,信号处理程序和未定义的行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48584862/

    相关文章:

    c - 传递打开的目录作为参数

    linux - 删除类 UNIX 系统上的所有 SYSTEM V 共享内存和信号量

    c - parent 应该收到有关孙子工作完成的信息

    c - 确定 *char < INT_MAX 是否

    c++ - 如何释放绑定(bind)参数 boost::signals2::signal 保留的引用?

    c - 从 pthread 到 main 的信号

    c - libpcap - pcap_findalldevs 在 MacOSx 上返回 NULL

    c - 使用 MPI 发送和接收派生数据类型

    matlab - 为什么如果我在输出上放置滤波器,我会修改源信号?这是一个 simulink 错误吗?

    c - 如果 posix 关闭调用失败怎么办?