我想确切地知道在Linux上异步信号处理程序的执行是如何工作的。首先,我不清楚哪个线程执行信号处理程序。其次,我想知道使线程执行信号处理程序所遵循的步骤。
关于第一个问题,我读过两种不同的、似乎相互矛盾的解释:
-
Linux Kernel, by Andries browwer,§5.2 "Receiving signals" state:
当信号到达时,进程被中断,当前寄存器被保存,并调用信号处理程序。当信号处理程序返回时,被中断的活动将继续。
-
StackOverflow问题"处理多线程程序中的异步信号"使我认为Linux的行为类似于SCO Unix的:
当一个信号被传递给一个进程时,如果它被捕获,它将被一个且只有一个满足以下条件之一的线程处理:
-
在sigwait(2)系统调用中阻塞的线程,其参数不包含捕获信号的类型。
-
信号掩码不包含捕获信号类型的线程。
额外的注意事项:
- 阻塞在sigwait(2)中的线程优先于未阻塞信号类型的线程。
- 如果不止一个线程满足这些要求(也许两个线程正在调用sigwait(2)),那么将选择其中一个。这个选择是应用程序无法预测的。
- 如果没有符合条件的线程,信号将在进程级保持"挂起"状态,直到有线程符合条件。
此外,Moshe Bar的"Linux信号处理模型"指出"异步信号被传递给第一个发现没有阻塞信号的线程",我将其解释为信号被传递给某些具有其sigmask 而不是的线程,包括信号。
-
哪个是正确的?
关于第二个问题,所选线程的堆栈和寄存器内容发生了什么?假设要运行信号处理程序的线程T正在执行do_stuff()
函数。线程T的堆栈是否直接用于执行信号处理程序(即信号蹦床的地址被推到T的堆栈上,控制流进入信号处理程序)?或者,是否使用单独的堆栈?它是如何工作的?
如果考虑到Linux黑客倾向于混淆线程和进程之间的区别这一事实,这两种解释实际上并不矛盾,这主要是由于试图假装线程可以作为共享内存的进程来实现的历史错误。: -)
话虽如此,解释#2更加详细、完整和正确。
对于堆栈和寄存器内容,每个线程可以注册自己的备用信号处理堆栈,并且进程可以根据每个信号选择哪些信号将在备用信号处理堆栈上传递。中断的上下文(寄存器,信号掩码等)将与蹦床返回地址一起保存在线程堆栈(可能是备用的)的ucontext_t
结构中。安装了SA_SIGINFO
标志的信号处理程序能够检查这个ucontext_t
结构,如果他们喜欢,但唯一可移植的事情,他们可以做的是检查(并可能修改)保存的信号掩码。(我不确定修改它是否被标准认可,但它非常有用,因为它允许信号处理程序在返回时自动替换被中断代码的信号掩码,例如离开信号阻塞,因此它不会再次发生。)
Source #1 (Andries browser)对于单线程进程是正确的。源代码#2 (SCO Unix)对于Linux来说是错误的,因为Linux不喜欢sigwait(2)中的线程。Moshe Bar关于第一个可用线程的判断是正确的。
哪个线程得到信号? Linux的手册页是一个很好的参考。一个进程使用clone(2)和CLONE_THREAD创建多个线程。这些线程属于"线程组"。并共享一个进程ID。克隆(2)的手册说,
信号可以作为一个整体发送到一个线程组(即,aTGID)使用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()和completes_signal()。代码为信号选择第一个可用线程。可用线程是指不阻塞信号且其队列中没有其他信号的线程。代码恰好先检查主线程,然后以某种我不知道的顺序检查其他线程。如果没有线程可用,则信号被卡住,直到某个线程解除对信号的阻塞或清空其队列。
当线程获得信号时会发生什么?如果有一个信号处理程序,那么内核会让线程调用这个处理程序。大多数处理程序在线程的堆栈上运行。如果进程使用sigaltstack(2)来提供堆栈,而sigaction(2)使用SA_ONSTACK来设置处理程序,则处理程序可以在备用堆栈上运行。内核将一些东西压入所选堆栈,并设置线程的一些寄存器。
要运行处理程序,线程必须在用户空间中运行。如果线程在内核中运行(可能是为了系统调用或页面错误),那么它在进入用户空间之前不会运行处理程序。内核可以中断一些系统调用,因此线程现在运行处理程序,而不必等待系统调用完成。
信号处理程序是一个C函数,因此内核遵循体系结构调用C函数的约定。每种体系结构(如arm、i386、powerpc或sparc)都有自己的约定。对于powerpc,要调用处理程序(signum),内核将寄存器r3设置为signum。内核还将处理程序的返回地址设置为信号蹦床。返回地址按约定放在堆栈中或寄存器中。
内核在每个进程中放置一个信号蹦床。这个trampoline调用sigreturn(2)来恢复线程。在内核中,sigreturn(2)从堆栈中读取一些信息(如保存的寄存器)。内核在调用处理程序之前已经将这些信息推送到堆栈上。如果有一个中断的系统调用,内核可能会重新启动调用(仅当处理程序使用SA_RESTART时),或者使用EINTR使调用失败,或者返回一个简短的读或写操作。