为啥存在 SIGPIPE?

Posted

技术标签:

【中文标题】为啥存在 SIGPIPE?【英文标题】:Why does SIGPIPE exist?为什么存在 SIGPIPE? 【发布时间】:2011-12-03 17:27:46 【问题描述】:

据我了解,SIGPIPE 只能作为 write() 的结果出现,它可以(并且确实)返回 -1 并将 errno 设置为 EPIPE... 那么为什么我们有额外的信号的开销?每次我使用管道时,我都会忽略SIGPIPE,因此从未感到任何痛苦,是我错过了什么吗?

【问题讨论】:

【参考方案1】:

我不买以前接受的答案。 SIGPIPE 是在 writeEPIPE 失败时生成的,而不是事先生成的 - 事实上,在不更改全局信号配置的情况下避免 SIGPIPE 的一种安全方法是用 pthread_sigmask 临时屏蔽它,执行 write ,然后执行sigtimedwait(超时为零)以消耗任何待处理的SIGPIPE 信号(发送到调用线程,而不是进程),然后再次取消屏蔽。

我相信SIGPIPE 存在的原因要简单得多:为不断读取输入、以某种方式转换并写入输出的纯“过滤器”程序建立健全的默认行为。如果没有SIGPIPE,除非这些程序显式处理写入错误并立即退出(无论如何,这可能不是所有写入错误的预期行为),即使它们的输出管道已关闭,它们仍将继续运行直到输入用完。当然,您可以通过显式检查 EPIPE 并退出来复制 SIGPIPE 的行为,但 SIGPIPE 的全部目的是在程序员懒惰时默认实现此行为。

【讨论】:

+1。线索在于 SIGPIPE 默认会杀死你——它不是为了中断系统调用,而是为了终止你的程序!如果您能够在信号处理程序中处理信号,那么您也可以处理 write 的返回码。 你说得对,我不知道我为什么一开始就接受了。这个答案是有道理的,尽管 IMO 很奇怪,例如在 Linux 上,这种惰性是由内核而不是 libc 实现的。 听起来这个答案基本上可以归结为:“因为我们没有例外”。然而,人们忽略 C 中的返回码是一个比 write() 调用更广泛的问题。是什么让 write 如此特别以至于需要自己的信号?也许纯粹的过滤程序比我想象的要普遍得多。 @Arvid SIGPIPE 是 Unix 人发明的,为了解决他们在过滤程序非常普遍的环境中遇到的问题,我们所要做的就是阅读启动系统的启动脚本. @SheaLevy 哪些 Unix 系统仅在其 libc 中实现 SIGPIPE?【参考方案2】:

因为您的程序可能正在等待 I/O 或以其他方式挂起。 SIGPIPE 会异步中断您的程序,终止系统调用,因此可以立即处理。

更新

考虑一个管道A | B | C

为了明确起见,我们假设 B 是规范复制循环:

while((sz = read(STDIN,bufr,BUFSIZE))>=0)
    write(STDOUT,bufr,sz);
C 终止时,

B 在等待来自A 的数据的read(2) 调用中被阻塞。如果等待 write(2) 的返回码,B 什么时候才能看到呢?答案当然是直到 A 写入更多数据(这可能需要很长时间——如果 A 被其他东西阻塞了怎么办?)。注意,顺便说一下,这也让我们的程序更简单、更干净。如果你依赖于 write 的错误代码,你需要这样的东西:

while((sz = read(STDIN,bufr,BUFSIZE))>=0)
    if(write(STDOUT,bufr,sz)<0)
        break;

另一个更新

啊哈,您对写入的行为感到困惑。您会看到,当带有挂起写入的文件描述符关闭时,SIGPIPE 随即发生。虽然写入将返回 -1 最终,但信号的全部意义在于异步通知您写入不再可能。这是使整个优雅的管道协同程序结构在 UNIX 中工作的部分原因。

现在,我可以为您指出几本 UNIX 系统编程书籍中的任何一本的完整讨论,但有一个更好的答案:您可以自己验证这一点。编写一个简单的B 程序[1]——你已经有了胆量,你只需要一个main 和一些包括——并为SIGPIPE 添加一个信号处理程序。运行类似的管道

cat | B | more

在另一个终端窗口中,将调试器附加到 B 并在 B 信号处理程序中放置一个断点。

现在,杀死 more 并且 B 应该在您的信号处理程序中中断。检查堆栈。您会发现 read 仍在等待中。让信号处理程序继续执行并返回,然后查看 write 返回的结果 -- 然后 为 -1。

[1] 当然,你会用 C 编写你的 B 程序。:-)

【讨论】:

为什么 B 会通过 SIGPIPE 更快地看到 C 的终止? B 在读取时将保持阻塞状态,直到将某些内容写入其 STDIN,此时它将调用 write(),然后才会引发 SIGPIPE/-1 被返回。 我真的很喜欢这个答案:SIGPIPE 让死亡立即从管道的输出端传播回来。如果没有这个,管道的 N 个元素中的每一个都需要一个复制程序循环来终止管道,并导致输入端生成永远不会到达末尾的 N 行。 这个答案不正确。 SIGPIPE不是在读取期间传递,而是在 write 期间传递。您不需要编写 C 程序来测试它,只需在单独的终端中运行 cat | headpkill head。你会看到cat 快乐地在它的read() 中等待——只有当你输入一些东西并按下回车键时,cat 才会因管道损坏而死,这正是因为它试图写入输出。 -1 SIGPIPE 无法传送到 B,而 Bread 上被阻止,因为在 B 尝试 write 之前不会生成 SIGPIPE。在同时调用write 时,任何线程都不能“等待 I/O 或以其他方式挂起”。 您能否发布一个完整的程序,显示SIGPIPE 在被read 阻止时被提升?我根本无法重现这种行为(实际上我不确定为什么我首先接受了这种行为)【参考方案3】:

https://www.gnu.org/software/libc/manual/html_mono/libc.html

这个链接说:

管道或 FIFO 必须同时在两端打开。如果您从没有任何进程写入的管道或 FIFO 文件中读取(可能是因为它们都已关闭文件或退出),则读取将返回文件结尾。 写入没有读取过程的管道或 FIFO 被视为错误条件;它会生成一个 SIGPIPE 信号, 如果信号被处理或阻止,则会失败并返回错误代码 EPIPE。

——宏:int SIGPIPE

管道破裂。如果使用管道或 FIFO,则必须设计应用程序,以便一个进程在另一个进程开始写入之前打开管道进行读取。如果读取过程从未启动或意外终止,写入管道或 FIFO 会引发 SIGPIPE 信号。如果 SIGPIPE 被阻塞、处理或忽略,则违规调用将失败并改为使用 EPIPE。

管道和 FIFO 特殊文件在管道和 FIFO 中有更详细的讨论。

【讨论】:

【参考方案4】:

我认为在写入管道的所有内容中不需要大量代码即可正确处理错误。

有些程序会忽略write()的返回值;如果没有SIGPIPE,它们将毫无用处地生成所有输出。

检查write() 的返回值的程序可能会在失败时打印错误消息;这对于损坏的管道是不合适的,因为它对于整个管道来说并不是真正的错误。

【讨论】:

【参考方案5】:

机器信息:

Linux 3.2.0-53-generic #81-Ubuntu SMP Thu Aug 22 21:01:03 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux

gcc 版本 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)

我在下面写了这段代码:

// Writes characters to stdout in an infinite loop, also counts 
// the number of characters generated and prints them in sighandler
// writestdout.c

# include <unistd.h>
# include <stdio.h>
# include <signal.h>
# include <string.h>

int writeCount = 0;    
void sighandler(int sig) 
    char buf1[30] ;
    sprintf(buf1,"signal %d writeCount %d\n", sig, writeCount);
    ssize_t leng = strlen(buf1);
    write(2, buf1, leng);
    _exit(1);



int main() 

    int i = 0;
    char buf[2] = "a";

    struct sigaction ss;
    ss.sa_handler = sighandler;

    sigaction(13, &ss, NULL);

    while(1) 

        /* if (writeCount == 4) 

            write(2, "4th char\n", 10);

         */

        ssize_t c = write(1, buf, 1);
        writeCount++;

    



// Reads only 3 characters from stdin and exits
// readstdin.c

# include <unistd.h>
# include <stdio.h>

int main() 

    ssize_t n ;        
    char a[5];        
    n = read(0, a, 3);
    printf("read %zd bytes\n", n);
    return(0);



输出:

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 11486

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 429

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 281

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 490

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 433

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 318

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 468

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 11866

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 496

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 284

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 271

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 416

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 11268

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 427

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 8812

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 394

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 10937

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 10931

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 3554

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 499

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 283

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 11133

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 451

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 493

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 233

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 11397

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 492

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 547

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 441

您可以看到,在每个实例中,SIGPIPE 仅在写入过程(尝试)写入超过 3 个字符后才会收到。

这不是证明SIGPIPE不是在读取过程终止后而是在尝试将更多数据写入封闭管道之后立即生成的吗?

【讨论】:

以上是关于为啥存在 SIGPIPE?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的进程总是收到信号 SIGPIPE,然后管道坏了。我在gdb看到的

什么信号(SIGPIPE,SIG_IGN);做?

SIGPIPE

调用了signal(SIGPIPE, SIG_IGN)仍然出现Program received signal SIGPIPE, Broken pipe.

调用了signal(SIGPIPE, SIG_IGN)仍然出现Program received signal SIGPIPE, Broken pipe.

避免SIGPIPE导致的iOS应用闪退/Avoiding SIGPIPE signal crash in iOS(mach_msg_trapSIGPIPE信号)