60

我想确切地知道异步信号处理程序的执行如何在 Linux 上工作。首先,我不清楚哪个线程执行信号处理程序。其次,我想知道使线程执行信号处理程序所遵循的步骤。

关于第一件事,我读过两种不同的、看似矛盾的解释:

  1. Linux 内核,作者 Andries Brouwer,第 5.2 节“接收信号”指出

    当信号到达时,进程被中断,当前寄存器被保存,信号处理程序被调用。当信号处理程序返回时,被中断的活动继续进行。

  2. StackOverflow 问题“处理多线程程序中的异步信号”让我认为 Linux 的行为就像 SCO Unix 的

    当一个信号被传递给一个进程时,如果它被捕获,它将由一个且只有一个满足以下任一条件的线程处理:

    1. 在sigwait (2)系统调用中阻塞的线程,其参数确实包含捕获信号的类型。

    2. 一个线程,其信号掩码包括捕获信号的类型。

    其他注意事项:

    • sigwait (2)中阻塞的线程优先于不阻塞信号类型的线程。
    • 如果多个线程满足这些要求(可能有两个线程正在调用sigwait (2)),那么将选择其中一个。这种选择是应用程序无法预测的。
    • 如果没有线程符合条件,则信号将在进程级别保持“未决”,直到某个线程符合条件。

    此外,Moshe Bar 的“Linux 信号处理模型”指出“异步信号被传递到第一个发现不阻塞信号的线程。”,我解释为信号被传递到其 sigmask包括信号的某个线程.

哪一个是正确的?

关于第二个问题,所选线程的堆栈和寄存器内容会发生什么变化?假设 thread-to-run-the-signal-handler T正在执行一个do_stuff()函数。线程T的堆栈是否直接用于执行信号处理程序(即信号蹦床的地址被推入T的堆栈并且控制流转到信号处理程序)?或者,是否使用了单独的堆栈?它是如何工作的?

4

2 回答 2

26

如果您考虑到 Linux 黑客往往对线程和进程之间的区别感到困惑,这两种解释确实并不矛盾,这主要是由于试图假装线程可以实现为共享进程的历史错误。记忆。:-)

话虽如此,解释#2 更加详细、完整和正确。

至于堆栈和寄存器内容,每个线程都可以注册自己的备用信号处理堆栈,并且进程可以根据每个信号选择哪些信号将在备用信号处理堆栈上传递。中断的上下文(寄存器、信号掩码等)将ucontext_t与蹦床返回地址一起保存在线程(可能是替代的)堆栈上的结构中。与标志一起安装的信号处理程序SA_SIGINFO能够检查这个ucontext_t如果他们愿意,可以使用结构,但他们可以用它做的唯一可移植的事情是检查(并可能修改)保存的信号掩码。(我不确定修改它是否受到标准的认可,但它非常有用,因为它允许信号处理程序在返回时自动替换被中断代码的信号掩码,例如让信号被阻塞,这样它就不会再次发生.)

于 2011-08-04T22:28:17.203 回答
9

源 #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 使调用失败,或者返回一个短读或写。

于 2018-01-11T03:28:41.080 回答