如何在Linux上执行异步信号处理程序?

Dan*_*ien 51 c linux signals signal-handling

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

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

  1. Linux内核,由Andries Brouwer撰写,§5.2"接收信号"状态:

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

  2. StackOverflow上的问题"处理异步信号在多线程程序"使我认为,Linux的行为是像SCO Unix的:

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

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

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

    其他考虑因素

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

    此外,Moshe Bar的"Linux信号处理模型"指出 "异步信号被传送到发现没有阻塞信号的第一个线程.",我解释为这意味着信号被传送到某个线程,其信号包括信号.

哪一个是正确的?

关于第二个问题,堆栈会发生什么,并为所选线程注册内容?假设线程到运行信号处理程序T正在执行do_stuff()函数.线程T的堆栈是否直接用于执行信号处理程序(即信号trampoline的地址被压入T的堆栈并且控制流程转到信号处理程序)?或者,是否使用单独的堆栈?它是如何工作的?

R..*_*R.. 24

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

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

对于堆栈和寄存器内容,每个线程可以注册其自己的备用信号处理堆栈,并且该过程可以基于每个信号选择哪些信号将在备用信号处理堆栈上传送.中断的上下文(寄存器,信号掩码等)将保存在ucontext_t线程的(可能是备用的)堆栈中的结构中,以及蹦床返回地址.安装有SA_SIGINFO标志的信号处理程序ucontext_t如果愿意,可以检查这个结构,但是他们可以用它做的唯一便携式事情是检查(并可能修改)保存的信号掩码.(我不确定修改它是否受到标准的批准,但它非常有用,因为它允许信号处理程序在返回时原子地替换被中断的代码的信号掩码,例如让信号被阻止以便它不会再发生.)

  • 为什么你觉得统一"进程"和"线程"是一个错误? (3认同)
  • 由于相关的线程API是*POSIX线程*("pthreads"),是的,规范来自POSIX.现代Linux(自2.6.0开始)在内核级别提供了真正的线程支持,因此模拟了一些在旧版本中更为普遍的错误,并且在2.6系列的末尾几乎全部修复了,没有太多明显错误.但术语仍然严重不匹配和不一致.进程被内核称为"线程组",线程称为"进程".除了在一些名称已被修复的地方.:-)因此它很混乱...... (3认同)
  • @AlexD:因为这会产生明显与其指定方式相反的结果.这样做的选择是(部分)无法理解所产生的行为将会出现多么错误的范围,并且(大多数情况下)假设没有/没有人会关心行为是错误的. (2认同)

Geo*_*ler 8

Source #1 (Andries Brouwer) 对于单线程进程是正确的。Source #2 (SCO Unix) 对于 Linux 是错误的,因为 Linux 不喜欢 sigwait(2) 中的线程。Moshe Bar 关于第一个可用线程是正确的。

哪个线程得到信号? Linux 的手册页是一个很好的参考。一个进程使用带有 CLONE_THREAD 的clone(2)来创建多个线程。这些线程属于一个“线程组”并共享一个进程 ID。clone(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)提供堆栈,并使用sigaction(2)和 SA_ONSTACK 来设置处理程序,则处理程序可以在备用堆栈上运行。内核将一些东西压入选定的堆栈,并设置一些线程的寄存器。

要运行处理程序,线程必须在用户空间中运行。如果线程在内核中运行(可能是系统调用或页面错误),那么它不会运行处理程序,直到它进入用户空间。内核可以中断一些系统调用,因此线程现在运行处理程序,无需等待系统调用完成。

信号处理程序是一个 C 函数,因此内核遵守调用 C 函数的体系结构约定。每种架构,如 arm、i386、powerpc 或 sparc,都有自己的约定。对于powerpc,为了调用handler(signum),内核将寄存器r3 设置为signum。内核还将处理程序的返回地址设置为信号蹦床。按照惯例,返回地址在堆栈中或寄存器中。

内核在每个进程中放置一个信号蹦床。这个蹦床调用sigreturn(2)来恢复线程。在内核中,sigreturn(2) 从堆栈中读取一些信息(如保存的寄存器)。内核在调用处理程序之前已将此信息推送到堆栈上。如果系统调用中断,内核可能会重新启动调用(仅当处理程序使用 SA_RESTART 时),或者使用 EINTR 使调用失败,或者返回一个简短的读或写。