【发布时间】:2023-03-30 11:36:03
【问题描述】:
如果一个发送导致一个 SIGPIPE 信号,哪个头会处理它?发送的线程还是随机线程?也就是说Linux系统通过kill或者pthread_kill发送信号?
【问题讨论】:
如果一个发送导致一个 SIGPIPE 信号,哪个头会处理它?发送的线程还是随机线程?也就是说Linux系统通过kill或者pthread_kill发送信号?
【问题讨论】:
像SIGPIPE 这样的异步信号可以发送到任何线程。您可以使用信号掩码来限制哪些线程符合条件。
SIGSEGV 等同步信号将在导致它们的线程上传递。
【讨论】:
这个问题的答案有两个方面:所讨论的系统应该如何运行以及它实际上如何运行。
由于大多数程序员希望 Linux 大部分与 POSIX 兼容,我们可以研究该标准,它实际上明确地指定了行为——信号直接发送到执行写入的线程。但是 Linux 是否坚持这一点尚不清楚,Linux 文档在这里也没有帮助。对 Linux 行为的检查表明它符合 POSIX,但并未证明这一点,阅读源代码为我们提供了有关当前 Linux 版本的必要证据。
tl;dr:它总是由执行写入的线程处理。
POSIX 标准要求(自 IEEE 标准 1003.1-2001/Cor 2-2004 起)将由于写入没有读取器的管道而生成的 SIGPIPE 传送到执行写入的线程。见EPIPE in the ERRORS section of the description of write()(强调我的):
[EPIPE] 尝试写入未打开以供任何进程读取的管道或 FIFO,或者仅打开一端。 SIGPIPE 信号也应发送到线程。
也就是说,目前尚不清楚 Linux 是否正确处理了这个问题。 man 7 signal 页面没有给出线程和进程导向信号的具体列表,只是示例,它对线程导向信号的定义不包括 SIGPIPE:
信号可能是线程导向的,因为它是由于执行触发硬件异常的特定机器语言指令而生成的 […]
SIGPIPE 不是特定指令的结果,也不是由硬件异常触发的。
Glibc 文档根本没有讨论内核生成的同步线程导向信号(即,甚至 SIGSEGV 或 SIGBUS 都没有被讨论为线程导向的),并且有多年前的 bugs in NPTL 报告,尽管这些可能在此期间已修复。
我编写了一个程序,它产生一个线程,它使用pthread_sigmask 阻塞 SIGPIPE,创建一个管道对,关闭读取端并将一个字节写入写入端。如果信号是线程导向的,那么在信号再次解除阻塞之前什么都不会发生。如果信号是进程导向的,主线程应该处理信号并且进程应该终止。原因再次来自 POSIX:如果有一个线程的(进程导向的)信号未阻塞,它should be delivered there instead of queueing:
为进程生成的信号应准确地传送到进程中 [...] 没有阻止信号传送的那些线程之一。如果 [...] 进程内的所有线程都阻塞了信号的传递,则信号应在进程上保持挂起状态,直到 […] 线程解除对信号的传递的阻塞,或与信号相关的操作设置为忽略信号。
我的实验表明,在具有最新 Glibc 的现代 (2020) Linux 上,信号确实被定向到执行写入的线程,因为在写入线程中使用 pthread_sigmask 阻止它会阻止 SIGPIPE 被传递,直到它被解除阻塞。
上面观察到的行为并不能证明什么,因为 Linux 完全有可能只是在几个地方违反了 POSIX,并且信号传递取决于我没有考虑到的一些因素。为了获得我们寻求的证据,我们可以阅读源代码。当然,这只会告诉我们当前的行为,而不是预期的行为——但如果我们发现当前的行为符合 POSIX,它可能会继续存在。
免责声明:我不是内核黑客,以下是粗略阅读源代码的结果。我可能错过了一些重要的事情。
在kernel/signal.c 中,有一个SYNCHRONOUS_MASK 列出了特殊处理的同步信号。它们是 SIGSEGV、SIGBUS、SIGILL、SIGTRAP、SIGFPE 和 SIGSYS——SIGPIPE 不在列表中。然而,这并不能回答这个问题——它可以是线程导向的而不是同步的。
那么 SIGPIPE 是如何发送的呢?它源自fs/pipe.c 中的pipe_write(),它在task_struct current 上调用send_sig()。 current 的使用已经暗示该信号是线程导向的,但让我们继续。 send_sig() 函数在 kernel/signal.c 中定义,并通过一些间接调用最终使用 pid_type type = PIDTYPE_PID 调用 __send_signal()。
在 Linux 术语中,PID refers to a single thread。果然,有了这些参数,挂起的信号列表是线程特定的,而不是共享的;而complete_signal()(在函数末尾调用)甚至不会尝试找到要唤醒的线程,它只是返回,因为已经选择了线程。我不完全了解信号队列是如何工作的,但似乎队列是每个线程的,因此当前线程是获取信号的线程。
【讨论】: