【问题标题】:How are asynchronous signal handlers executed on Linux?Linux 上的异​​步信号处理程序是如何执行的?
【发布时间】:2011-10-20 09:53:55
【问题描述】:

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

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

  1. Linux 内核,作者 Andries Brouwer,§5.2 "Receiving signals" states

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

  2. StackOverflow question "Dealing With Asynchronous Signals In Multi Threaded Program" 让我认为 Linux 的行为是 like SCO Unix's

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

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

    2. 信号掩码包含捕获信号类型的线程。

    其他注意事项:

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

    另外,"The Linux Signals Handling Model" by Moshe Bar states“异步信号被传递到第一个发现没有阻塞信号的线程。”,我解释为信号被传递到具有其 sigmask not 的某个线程,包括信号。

哪个是正确的?

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

【问题讨论】:

  • This 可能会回答您的一些问题(绝对不是全部)。

标签: c linux signals signal-handling


【解决方案1】:

如果您考虑到 Linux 黑客倾向于混淆线程和进程之间的区别这一事实,这两种解释确实并不矛盾,主要是由于试图假装线程可以实现为的历史错误共享内存的进程。 :-)

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

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

【讨论】:

  • 为什么你觉得统一“进程”和“线程”是错误的?
  • @AlexD:因为这会产生明显与指定方式相反的结果。这样做的选择是(部分)未能理解由此产生的行为的错误程度,以及(大部分)假设没有人/没有人会关心错误的行为。
  • “指定”是指 POSIX 规范还是其他规范? Linux 的轻量级进程的行为从用户空间观察与那些规范有何不同?
  • 由于相关的线程 API 是 POSIX 线程(“pthreads”),是的,规范来自 POSIX。现代 Linux(自 2.6.0 起)在内核级别提供了真正的线程支持,因此以一些在旧版本中更为普遍并且在 2.6 系列结束时几乎全部修复的错误为模,并没有太多明显的错误。但术语仍然严重不匹配和不一致。进程被内核称为“线程组”,线程被称为“进程”。除了一些名字已经固定的地方。 :-) 因此这很令人困惑......
【解决方案2】:

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

哪个线程得到信号?Linux 的手册页是一个很好的参考。一个进程使用clone(2) 和 CLONE_THREAD 来创建多个线程。这些线程属于一个“线程组”并共享一个进程 ID。 clone(2) 的手册说,

信号可以作为一个整体发送到一个线程组(即,一个 TGID)使用kill(2),或使用特定线程(即TID) tgkill(2).

信号处置和行动是全过程的:如果 未处理的信号被传递到一个线程,然后它会影响 (终止、停止、继续、被忽略)所有成员 线程组。

每个线程都有自己的信号掩码,由sigprocmask(2) 设置, 但信号可以是未决的:对于整个过程 (即,可交付给线程组的任何成员),当 与 kill(2) 一起发送;或对于单个线程,当发送时 tgkill(2)。对sigpending(2) 的调用会返回一个信号集,该信号集 是整个过程中未决信号的联合,并且 等待调用线程的信号。

如果使用 kill(2) 向线程组发送信号,并且 线程组已经为信号安装了一个处理程序,那么 处理程序将被调用一个,任意选择 没有阻塞信号的线程组的成员。 如果组中的多个线程正在等待接受相同的 信号使用sigwaitinfo(2),内核会任意 选择这些线程之一来接收使用发送的信号 杀死(2)。

Linux 不是 SCO Unix,因为 Linux 可能会向任何线程发出信号,即使某些线程正在等待信号(使用 sigwaitinfo、sigtimedwait 或 sigwait)而某些线程没有。 sigwaitinfo(2) 的手册警告,

在正常使用中,调用程序通过一个 之前调用 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,为了调用 handler(signum),内核将寄存器 r3 设置为 signum。内核还将处理程序的返回地址设置为信号蹦床。返回地址按惯例放在堆栈或寄存器中。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-09-01
    • 2018-06-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多