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