源 #1 (Andries Brouwer) 对于单线程进程是正确的。Source #2 (SCO Unix) 对于 Linux 是错误的,因为 Linux 不喜欢 sigwait(2) 中的线程。Moshe Bar 关于第一个可用线程是正确的。
哪个线程得到信号? Linux 的手册页是一个很好的参考。一个进程使用带有 CLONE_THREAD 的clone(2)来创建多个线程。这些线程属于一个“线程组”并共享一个进程 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,为了调用 handler(signum),内核将寄存器 r3 设置为 signum。内核还将处理程序的返回地址设置为信号蹦床。按照惯例,返回地址位于堆栈或寄存器中。
内核在每个进程中放置一个信号蹦床。这个蹦床调用sigreturn(2)来恢复线程。在内核中, sigreturn(2) 从堆栈中读取一些信息(如保存的寄存器)。内核在调用处理程序之前已将此信息推送到堆栈上。如果有一个中断的系统调用,内核可能会重新启动调用(仅当处理程序使用 SA_RESTART 时),或者使用 EINTR 使调用失败,或者返回一个短读或写。