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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/6949025/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-05 05:28:59  来源:igfitidea点击:

How are asynchronous signal handlers executed on Linux?

clinuxsignalssignal-handling

提问by Daniel Trebbien

I would like to know exactly how the execution of asynchronous signal handlers works on Linux. First, I am unclear as to whichthread executes the signal handler. Second, I would like to know the steps that are followed to make the thread execute the signal handler.

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

On the first matter, I have read two different, seemingly conflicting, explanations:

关于第一个问题,我读过两种不同的、看似相互矛盾的解释:

  1. The Linux Kernel, by Andries Brouwer, §5.2 "Receiving signals" states:

    When a signal arrives, the process is interrupted, the current registers are saved, and the signal handler is invoked. When the signal handler returns, the interrupted activity is continued.

  2. The StackOverflow question "Dealing With Asynchronous Signals In Multi Threaded Program"leads me to think that Linux's behavior is like SCO Unix's:

    When a signal is delivered to a process, if it is being caught, it will be handled by one, and only one, of the threads meeting either of the following conditions:

    1. A thread blocked in a sigwait(2)system call whose argument doesinclude the type of the caught signal.

    2. A thread whose signal mask does notinclude the type of the caught signal.

    Additional considerations:

    • A thread blocked in sigwait(2)is given preference over a thread not blocking the signal type.
    • If more than one thread meets these requirements (perhaps two threads are calling sigwait(2)), then one of them will be chosen. This choice is not predictable by application programs.
    • If no thread is eligible, the signal will remain ``pending'' at the process level until some thread becomes eligible.

    Also, "The Linux Signals Handling Model" by Moshe Bar states"Asynchronous signals are delivered to the first thread found not blocking the signal.", which I interpret to mean that the signal is delivered to some thread having its sigmask notincluding the signal.

  1. Linux 内核,由 Andries Brouwer 撰写,第5.2 节“接收信号”指出

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

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

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

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

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

    其他注意事项:

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

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

Which one is correct?

哪一个是正确的?

On the second matter, what happens to the stack and register contents for the selected thread? Suppose the thread-to-run-the-signal-handler Tis in the middle of executing a do_stuff()function. Is thread T's stack used directly to execute the signal handler (i.e. the address of the signal trampoline is pushed onto T's stack and control flow goes to the signal handler)? Alternatively, is a separate stack used? How does it work?

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

采纳答案by George Koehler

Source #1 (Andries Brouwer) is correct for a single-threaded process. Source #2 (SCO Unix) is wrong for Linux, because Linux does not prefer threads in sigwait(2). Moshe Bar is correct about the first available thread.

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

Which thread gets the signal?Linux's manual pages are a good reference. A process uses clone(2)with CLONE_THREAD to create multiple threads. These threads belong to a "thread group" and share a single process ID. The manual for clone(2) says,

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

Signals may be sent to a thread group as a whole (i.e., a TGID) using kill(2), or to a specific thread (i.e., TID) using tgkill(2).

Signal dispositions and actions are process-wide: if an unhandled signal is delivered to a thread, then it will affect (terminate, stop, continue, be ignored in) all members of the thread group.

Each thread has its own signal mask, as set by sigprocmask(2), but signals can be pending either: for the whole process (i.e., deliverable to any member of the thread group), when sent with kill(2); or for an individual thread, when sent with tgkill(2). A call to sigpending(2)returns a signal set that is the union of the signals pending for the whole process and the signals that are pending for the calling thread.

If kill(2) is used to send a signal to a thread group, and the thread group has installed a handler for the signal, then the handler will be invoked in exactly one, arbitrarily selected member of the thread group that has not blocked the signal. If multiple threads in a group are waiting to accept the same signal using sigwaitinfo(2), the kernel will arbitrarily select one of these threads to receive a signal sent using kill(2).

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

信号处置和动作是进程范围的:如果一个未处理的信号被传递给一个线程,那么它将影响(终止、停止、继续、被忽略)线程组的所有成员。

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

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

Linux is not SCO Unix, because Linux might give the signal to any thread, even if some threads are waiting for a signal (with sigwaitinfo, sigtimedwait, or sigwait) and some threads are not. The manual for sigwaitinfo(2)warns,

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

In normal usage, the calling program blocks the signals in set via a prior call to sigprocmask(2) (so that the default disposition for these signals does not occur if they become pending between successive calls to sigwaitinfo() or sigtimedwait()) and does not establish handlers for these signals. In a multithreaded program, the signal should be blocked in all threads, in order to prevent the signal being treated according to its default disposition in a thread other than the one calling sigwaitinfo() or sigtimedwait()).

在正常使用中,调用程序通过对 sigprocmask(2) 的先前调用来阻止 set 中的信号(因此,如果这些信号在连续调用 sigwaitinfo() 或 sigtimedwait() 之间变为挂起状态,则不会发生这些信号的默认处置)和不为这些信号建立处理程序。在多线程程序中,信号应在所有线程中被阻塞,以防止信号在调用 sigwaitinfo() 或 sigtimedwait() 的线程之外的线程中根据其默认处置进行处理。

The code to pick a thread for the signal lives in linux/kernel/signal.c(the link points to GitHub's mirror). See the functions wants_signal() and completes_signal(). The code picks the first available thread for the signal. An available thread is one that doesn't block the signal and has no other signals in its queue. The code happens to check the main thread first, then it checks the other threads in some order unknown to me. If no thread is available, then the signal is stuck until some thread unblocks the signal or empties its queue.

为信号选择线程的代码位于linux/kernel/signal.c(链接指向 GitHub 的镜像)。请参阅函数 Wants_signal() 和 completes_signal()。代码为信号选择第一个可用线程。可用线程是不阻塞信号且队列中没有其他信号的线程。代码碰巧首先检查主线程,然后它以某种我不知道的顺序检查其他线程。如果没有线程可用,则信号会被卡住,直到某个线程解除对信号的阻塞或清空其队列。

What happens when a thread gets the signal?If there is a signal handler, then the kernel causes the thread to call the handler. Most handlers run on the thread's stack. A handler can run on an alternate stack if the process uses sigaltstack(2)to provide the stack, and sigaction(2)with SA_ONSTACK to set the handler. The kernel pushes some things onto the chosen stack, and sets some of the thread's registers.

当线程收到信号时会发生什么?如果存在信号处理程序,则内核会导致线程调用该处理程序。大多数处理程序在线程的堆栈上运行。如果进程使用sigaltstack(2)提供堆栈,并使用sigaction(2)和 SA_ONSTACK 来设置处理程序,则处理程序可以在备用堆栈上运行。内核将一些东西压入选定的堆栈,并设置一些线程的寄存器。

To run the handler, the thread must be running in userspace. If the thread is running in the kernel (perhaps for a system call or a page fault), then it does not run the handler until it goes to userspace. The kernel can interrupt some system calls, so the thread runs the handler now, without waiting for the system call to finish.

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

The signal handler is a C function, so the kernel obeys the architecture's convention for calling C functions. Each architecture, like arm, i386, powerpc, or sparc, has its own convention. For powerpc, to call handler(signum), the kernel sets the register r3 to signum. The kernel also sets the handler's return address to the signal trampoline. The return address goes on the stack or in a register by convention.

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

The kernel puts one signal trampoline in each process. This trampoline calls sigreturn(2)to restore the thread. In the kernel, sigreturn(2) reads some information (like saved registers) from the stack. The kernel had pushed this information on the stack before calling the handler. If there was an interrupted system call, the kernel might restart the call (only if the handler used SA_RESTART), or fail the call with EINTR, or return a short read or write.

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

回答by R.. GitHub STOP HELPING ICE

These two explanations really aren't contradictory if you take into account the fact that Linux hackers tend to be confused about the difference between a thread and a process, mainly due to the historical mistake of trying to pretend threads could be implemented as processes that share memory. :-)

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

With that said, explanation #2 is much more detailed, complete, and correct.

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

As for the stack and register contents, each thread can register its own alternate signal-handling stack, and the process can choose on a per-signal basis which signals will be delivered on alternate signal-handling stacks. The interrupted context (registers, signal mask, etc.) will be saved in a ucontext_tstructure on the (possibly alternate) stack for the thread, along with the trampoline return address. Signal handlers installed with the SA_SIGINFOflag are able to examine this ucontext_tstructure if they like, but the only portable thing they can do with it is examine (and possibly modify) the saved signal mask. (I'm not sure if modifying it is sanctioned by the standard, but it's very useful because it allows the signal handler to atomically replace the interrupted code's signal mask upon return, for instance to leave the signal blocked so it can't happen again.)

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