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 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:
让我们首先总结一下 sem_post(&sem)
访问 在信号处理程序中 有效的情况(即,可以引用具有静态存储持续时间的对象,例如通过任何异步信号安全功能),基于 POSIX.1-2018:
当进程只有一个线程时,由于同一进程中的线程调用abort()
、raise()
、[=13=,信号处理程序将被执行]、pthread_kill()
或 sigqueue()
,并且信号 is/was 未在用于执行处理程序的线程中阻塞。
当进程只有一个线程时,信号在挂起时被阻塞,并且在解除阻塞的调用之前被传递returns.
这排除了最常见的情况:多线程进程,以及进程外部生成的信号的处理程序(例如,SIGINT,当进程在前台运行时,用户按下 Ctrl+C; 或 SIGHUP 当进程处于 运行 的会话关闭时。
我对这种情况的理解是,每个人都希望通过异步信号安全函数引用具有静态存储持续时间的对象的信号处理程序不会在任何理智的 POSIXy 架构上触发未定义的行为;如果在具有静态存储持续时间的对象上使用多线程安全(MT 安全)异步信号安全函数,那么它在多线程进程中的工作方式与在单线程进程中的工作方式完全相同; alarm()
、setitimer()
和 timer_settime()
触发的信号与 raise()
或 sigqueue()
触发的信号表现相同;并且其他进程发送的信号的行为与目标进程中 raise()
或 sigqueue()
触发的信号相同;唯一的区别是 siginfo 结构中的某些字段具有不同的值。
甚至有很小的可能性,措辞应该是accesses而不是refer to。这确实确实允许将任何具有静态存储持续时间的对象的地址传递给异步信号安全函数,如 sem_post()
即使在多线程进程中,如 posits.
但是,我认为这种措辞的原因更加微妙,并且涉及有关并发访问的硬件实现差异以及执行上下文信号处理程序的情况:某些 POSIX 操作系统可能的行为不同的行为太复杂而无法标准化,因此只是保留未定义。
我的其余回答试图描述那些确实希望生成可在所有 POSIXy 系统上运行的可靠、健壮的程序并且不理解当前措辞的微妙之处的开发人员POSIX.1 规格
信号处理程序究竟可以安全访问哪些对象的问题很复杂。 POSIX 标准起草者并没有打开整个蠕虫罐头,而是将其踢开,并宣布该行为未定义。
最难定义的部分是与并发访问和陷阱表示相关的细节。不仅由同一进程中的其他线程,而且由内核。 (因为我们只考虑具有静态存储持续时间的对象,我们可以避免共享内存和所有相关的复杂性。)特别是,如果一个对象有陷阱表示,并且该对象被非原子地修改,中间阶段可能分配导致陷阱。尽管某些架构可能存在硬件限制,但该陷阱本身可能会引发信号。
因此,与陷阱表示相关的任何内容基本上都太复杂而无法在标准中解决。
好吧,我们假设该标准会限制对具有静态存储持续时间的对象的安全读取访问,这些对象不会被中断的线程、进程中的任何其他线程或内核同时修改;对具有静态存储持续时间的对象的写访问,这些对象不会被中断的线程、进程中的任何其他线程或内核同时读取或修改。并且被访问的对象根本没有陷阱表示。
我们还有一些特定于硬件的信号需要考虑:至少 SIGSEGV
、SIGBUS
、SIGILL
和 SIGFPE
。不幸的是,一些架构此时可能有额外的信号未知,所以我们需要定义受影响的信号类型:访问内存时内核引发的信号(SIGFPE
只有当架构引发它时在加载值时,而不仅仅是在对这些值进行算术运算等时)。如果访问具有静态存储持续时间的对象可能会引发这些信号之一,则访问不安全,因为它可能导致信号处理程序级联。 (因为标准 POSIX 信号没有排队,只有每种信号的第一个信号被执行,进程状态可能会丢失,迫使内核终止进程。)
从 POSIX C 编译器的角度来看,如果您考虑一个获取指针作为有效负载的信号处理程序(si_value.sival_ptr
在 siginfo_t
): 访问是否会导致未定义的行为,具体取决于目标是否具有静态存储持续时间?
在所有当前 POSIXy 系统上,通过原子内置访问静态存储持续时间对象,或者当它们不被任何其他线程或内核和中间存储形式read/modified访问时在 POSIX 实时信号处理程序中,或在 POSIX 不是由内存访问引发的信号处理程序中,不要引发信号是安全的。这很可能 但不能保证 将来也会如此。这就是 POSIX 标准未对其进行标准化的核心原因。
冷酷的事实是,对于需要访问具有静态存储持续时间的对象的所有模式,有一个符合 POSIX 的解决方法:一个单独的线程,专门用于通过 sigwaitinfo()
处理信号,所有这些信号都在所有其他线程中被阻塞。该线程不限于使用异步信号安全函数,其他信号处理程序限制也不适用于它。 (如果我们考虑信号传递和它中断的代码之间的交互,即使使用 SA_RESTART
标志定义的处理程序,也可以争辩说基于线程的方法是两者中更好的一种。)
简而言之:由于存在已知的解决方法,并且定义安全访问案例会过于复杂并限制未来的实施,因此 POSIX 标准根本没有对这种传统用例进行标准化。这并不是因为预计它不会起作用——恰恰相反;它在所有当前的 POSIXy 系统中都能正常工作——但是因为定义安全访问情况的复杂性和可能的限制是不值得的(除了 errno
和 volatile sig_atomic_t
,它们都需要并得到 POSIX C 编译器的支持)。
这不依赖于未定义的行为。 explicitly stated sem_post() 函数应该是异步信号安全的,并且可以从信号捕获函数中调用。
在代码sem_post(&sem)
中没有访问变量(读取或写入)。参数是一个常量(某个地址)。
与问题无关,但请注意,除了访问 volatile sig_atomic_t
外,还允许访问无锁原子变量。我添加它是因为我怀疑信号量内部使用的是无锁原子而不是易失性 sig_atomic_t。后者在单线程/核心上只有"atomic";不是线程安全的。例如。存在递增非原子整数类型的单个汇编指令,因此不能被信号处理程序中断;但对于其他线程(无总线锁定),它仍然以非原子方式执行读-增量-写。由于信号量旨在从线程 A 向线程 B 发出信号,因此不太可能使用 sig_atomic_t 提供的保证:它仍然需要一个互斥锁来进行访问,这不是信号安全的。
在信号处理程序中使用 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:
让我们首先总结一下 sem_post(&sem)
访问 在信号处理程序中 有效的情况(即,可以引用具有静态存储持续时间的对象,例如通过任何异步信号安全功能),基于 POSIX.1-2018:
当进程只有一个线程时,由于同一进程中的线程调用
abort()
、raise()
、[=13=,信号处理程序将被执行]、pthread_kill()
或sigqueue()
,并且信号 is/was 未在用于执行处理程序的线程中阻塞。当进程只有一个线程时,信号在挂起时被阻塞,并且在解除阻塞的调用之前被传递returns.
这排除了最常见的情况:多线程进程,以及进程外部生成的信号的处理程序(例如,SIGINT,当进程在前台运行时,用户按下 Ctrl+C; 或 SIGHUP 当进程处于 运行 的会话关闭时。
我对这种情况的理解是,每个人都希望通过异步信号安全函数引用具有静态存储持续时间的对象的信号处理程序不会在任何理智的 POSIXy 架构上触发未定义的行为;如果在具有静态存储持续时间的对象上使用多线程安全(MT 安全)异步信号安全函数,那么它在多线程进程中的工作方式与在单线程进程中的工作方式完全相同; alarm()
、setitimer()
和 timer_settime()
触发的信号与 raise()
或 sigqueue()
触发的信号表现相同;并且其他进程发送的信号的行为与目标进程中 raise()
或 sigqueue()
触发的信号相同;唯一的区别是 siginfo 结构中的某些字段具有不同的值。
甚至有很小的可能性,措辞应该是accesses而不是refer to。这确实确实允许将任何具有静态存储持续时间的对象的地址传递给异步信号安全函数,如 sem_post()
即使在多线程进程中,如
但是,我认为这种措辞的原因更加微妙,并且涉及有关并发访问的硬件实现差异以及执行上下文信号处理程序的情况:某些 POSIX 操作系统可能的行为不同的行为太复杂而无法标准化,因此只是保留未定义。
我的其余回答试图描述那些确实希望生成可在所有 POSIXy 系统上运行的可靠、健壮的程序并且不理解当前措辞的微妙之处的开发人员POSIX.1 规格
信号处理程序究竟可以安全访问哪些对象的问题很复杂。 POSIX 标准起草者并没有打开整个蠕虫罐头,而是将其踢开,并宣布该行为未定义。
最难定义的部分是与并发访问和陷阱表示相关的细节。不仅由同一进程中的其他线程,而且由内核。 (因为我们只考虑具有静态存储持续时间的对象,我们可以避免共享内存和所有相关的复杂性。)特别是,如果一个对象有陷阱表示,并且该对象被非原子地修改,中间阶段可能分配导致陷阱。尽管某些架构可能存在硬件限制,但该陷阱本身可能会引发信号。
因此,与陷阱表示相关的任何内容基本上都太复杂而无法在标准中解决。
好吧,我们假设该标准会限制对具有静态存储持续时间的对象的安全读取访问,这些对象不会被中断的线程、进程中的任何其他线程或内核同时修改;对具有静态存储持续时间的对象的写访问,这些对象不会被中断的线程、进程中的任何其他线程或内核同时读取或修改。并且被访问的对象根本没有陷阱表示。
我们还有一些特定于硬件的信号需要考虑:至少 SIGSEGV
、SIGBUS
、SIGILL
和 SIGFPE
。不幸的是,一些架构此时可能有额外的信号未知,所以我们需要定义受影响的信号类型:访问内存时内核引发的信号(SIGFPE
只有当架构引发它时在加载值时,而不仅仅是在对这些值进行算术运算等时)。如果访问具有静态存储持续时间的对象可能会引发这些信号之一,则访问不安全,因为它可能导致信号处理程序级联。 (因为标准 POSIX 信号没有排队,只有每种信号的第一个信号被执行,进程状态可能会丢失,迫使内核终止进程。)
从 POSIX C 编译器的角度来看,如果您考虑一个获取指针作为有效负载的信号处理程序(si_value.sival_ptr
在 siginfo_t
): 访问是否会导致未定义的行为,具体取决于目标是否具有静态存储持续时间?
在所有当前 POSIXy 系统上,通过原子内置访问静态存储持续时间对象,或者当它们不被任何其他线程或内核和中间存储形式read/modified访问时在 POSIX 实时信号处理程序中,或在 POSIX 不是由内存访问引发的信号处理程序中,不要引发信号是安全的。这很可能 但不能保证 将来也会如此。这就是 POSIX 标准未对其进行标准化的核心原因。
冷酷的事实是,对于需要访问具有静态存储持续时间的对象的所有模式,有一个符合 POSIX 的解决方法:一个单独的线程,专门用于通过 sigwaitinfo()
处理信号,所有这些信号都在所有其他线程中被阻塞。该线程不限于使用异步信号安全函数,其他信号处理程序限制也不适用于它。 (如果我们考虑信号传递和它中断的代码之间的交互,即使使用 SA_RESTART
标志定义的处理程序,也可以争辩说基于线程的方法是两者中更好的一种。)
简而言之:由于存在已知的解决方法,并且定义安全访问案例会过于复杂并限制未来的实施,因此 POSIX 标准根本没有对这种传统用例进行标准化。这并不是因为预计它不会起作用——恰恰相反;它在所有当前的 POSIXy 系统中都能正常工作——但是因为定义安全访问情况的复杂性和可能的限制是不值得的(除了 errno
和 volatile sig_atomic_t
,它们都需要并得到 POSIX C 编译器的支持)。
这不依赖于未定义的行为。 explicitly stated sem_post() 函数应该是异步信号安全的,并且可以从信号捕获函数中调用。
在代码sem_post(&sem)
中没有访问变量(读取或写入)。参数是一个常量(某个地址)。
与问题无关,但请注意,除了访问 volatile sig_atomic_t
外,还允许访问无锁原子变量。我添加它是因为我怀疑信号量内部使用的是无锁原子而不是易失性 sig_atomic_t。后者在单线程/核心上只有"atomic";不是线程安全的。例如。存在递增非原子整数类型的单个汇编指令,因此不能被信号处理程序中断;但对于其他线程(无总线锁定),它仍然以非原子方式执行读-增量-写。由于信号量旨在从线程 A 向线程 B 发出信号,因此不太可能使用 sig_atomic_t 提供的保证:它仍然需要一个互斥锁来进行访问,这不是信号安全的。