在Linux上如何执行异步信号处理程序?
我想知道在Linux上如何执行异步信号处理程序。 首先,我不清楚哪个线程执行信号处理程序。 其次,我想知道让线程执行信号处理程序的步骤。
关于第一个问题,我读了两个不同的,看似矛盾的解释:
由Andries Brouwer撰写的Linux Kernel第5.2节“接收信号”指出:
当信号到达时,过程中断,当前寄存器被保存,并且调用信号处理程序。 当信号处理程序返回时,中断的活动将继续。
StackOverflow问题“在多线程程序中处理异步信号”让我认为Linux的行为就像SCO Unix的行为:
当一个信号被传递给一个进程时,如果它被捕获,它将被满足以下任一条件的线程中的一个(并且只有一个)处理:
在sigwait (2)系统调用中阻塞的线程,其参数确实包含捕获信号的类型。
信号掩码不包含捕获信号类型的线程。
其他注意事项
另外,Moshe Bar的“Linux信号处理模型”指出:“异步信号被传递到第一个线程,而不是阻塞信号”,我解释这意味着信号被传递到某个线程,其信号的sigmask不包括信号。
哪一个是正确的?
关于第二个问题,所选线程的堆栈和寄存器内容会发生什么变化? 假设线程运行信号处理程序T正在执行do_stuff()
函数的中间。 线程T的堆栈是否直接用于执行信号处理程序(即,信号蹦床的地址是否被推送到T的栈中,并且控制流是否到达信号处理程序)? 或者,是否使用单独的堆栈? 它是如何工作的?
源代码#1(Andries Brouwer)对于单线程进程是正确的。 源码#2(SCO Unix)对于Linux来说是错误的,因为Linux不喜欢sigwait(2)中的线程。 Moshe Bar对第一个可用线程是正确的。
哪个线程获取信号? Linux的手册页是一个很好的参考。 一个进程使用克隆(2)和CLONE_THREAD来创建多个线程。 这些线程属于“线程组”并共享一个进程ID。 克隆手册(2)说,
可以使用kill(2)将信号作为整体(即,TGID)发送到线程组,或者使用tgkill(2)将信号发送到特定线程(即,TID)。
信号处置和动作是全过程的:如果一个未处理的信号被传递给一个线程,那么它将影响线程组的所有成员(终止,停止,继续,被忽略)。
每个线程都有它自己的信号掩码,由sigprocmask(2)设置,但信号可以挂起:整个过程(即可交付给线程组的任何成员),当使用kill(2)发送时; 或者在用tgkill(2)发送时针对单个线程。 对sigpending(2)的调用返回一个信号集合,该信号集合是整个过程的待处理信号和待调用线程的信号的并集。
如果使用kill(2)向一个线程组发送信号,并且线程组已经为该信号安装了一个处理程序,那么该处理程序将在线程组中任意选定的一个成员中被调用,信号。 如果一个组中的多个线程正在使用sigwaitinfo(2)等待接收相同的信号,那么内核将任意选择其中一个线程来接收使用kill(2)发送的信号。
Linux不是SCO Unix,因为Linux可能会向任何线程发出信号,即使某些线程正在等待信号(使用sigwaitinfo,sigtimedwait或sigwait),而某些线程则不会。 sigwaitinfo(2)手册警告说,
在正常使用情况下,调用程序通过先前调用sigprocmask(2)来阻塞set中的信号(这样,如果在连续调用sigwaitinfo()或sigtimedwait())之间变为挂起,则不会发生这些信号的默认处置,不建立这些信号的处理程序。 在多线程程序中,信号应该在所有线程中被阻塞,以防止信号按照其调用sigwaitinfo()或sigtimedwait())的线程中的默认处置来处理。
选择信号线程的代码位于linux / kernel / signal.c中(链接指向GitHub的镜像)。 请参阅函数wants_signal()和completes_signal()。 代码为信号选择第一个可用线程。 可用线程是不阻塞信号并且在其队列中没有其他信号的线程。 代码碰巧首先检查主线程,然后以某种未知的顺序检查其他线程。 如果没有线程可用,那么信号将卡住,直到某个线程解除阻塞信号或清空其队列。
线程获取信号时会发生什么? 如果有一个信号处理程序,那么内核会使线程调用处理程序。 大多数处理程序都在线程的堆栈上运行。 如果进程使用sigaltstack(2)提供堆栈,并且使用SA_ONSTACK设置sigaction(2)来设置处理程序,则处理程序可以在备用堆栈上运行。 内核将一些东西推到选定的堆栈上,并设置一些线程的寄存器。
要运行该处理程序,该线程必须在用户空间中运行。 如果线程正在内核中运行(可能是系统调用或页面错误),那么在处理程序进入用户空间之前它不会运行处理程序。 内核可以中断一些系统调用,所以线程立即运行处理程序,而不用等待系统调用完成。
信号处理程序是一个C函数,因此内核服从调用C函数的架构惯例。 每个架构,如arm,i386,powerpc或sparc都有自己的约定。 对于powerpc,要调用处理程序(signum),内核将寄存器r3设置为signum。 内核还将处理程序的返回地址设置为信号蹦床。 返回地址按照惯例在堆栈或寄存器中进行。
内核在每个进程中放入一个信号蹦床。 这个蹦床叫sigreturn(2)恢复线程。 在内核中,sigreturn(2)从栈中读取一些信息(如保存的寄存器)。 内核在调用处理程序之前已将这些信息推送到堆栈上。 如果系统调用中断,内核可能会重新启动调用(仅当处理程序使用SA_RESTART时),或者使用EINTR调用失败,或者返回一个简短的读或写操作。
如果考虑到Linux黑客倾向于对线程和进程之间的差异感到困惑,这两个解释确实不矛盾,主要是由于尝试假装线程可能被实现为共享进程的历史错误记忆。 :-)
就这样说,解释#2更加详细,完整和正确。
至于堆栈和寄存器内容,每个线程都可以注册自己的备用信号处理堆栈,并且该进程可以根据每个信号选择哪些信号将在交替的信号处理堆栈上交付。 中断的上下文(寄存器,信号掩码等)将被保存在线程堆栈上的ucontext_t
结构中,以及蹦床返回地址。 使用SA_SIGINFO
标志安装的信号处理程序能够检查此ucontext_t
结构(如果他们喜欢),但唯一可以使用的便携式功能是检查(并可能修改)保存的信号掩码。 (我不确定修改它是否受标准认可,但它非常有用,因为它允许信号处理程序在返回时自动替换中断的代码的信号掩码,例如让信号保持阻塞状态,以免它再次发生。)
上一篇: How are asynchronous signal handlers executed on Linux?