如何防止SIGPIPE(或正确处理)


259

我有一个小型服务器程序,该程序接受TCP或本地UNIX套接字上的连接,读取一个简单命令,并根据命令发送回复。问题是客户端有时可能对答案没有兴趣,并且提早退出,因此写入该套接字将导致SIGPIPE并使服务器崩溃。防止此处崩溃的最佳实践是什么?有没有办法检查行的另一端是否仍在读取?(select()似乎在这里不起作用,因为它总是说套接字是可写的)。还是应该仅使用处理程序来捕获SIGPIPE并忽略它?

Answers:


253

通常,您希望忽略SIGPIPE并直接在代码中处理错误。这是因为C语言中的信号处理程序对其功能有很多限制。

最方便的方法是将SIGPIPE处理程序设置为SIG_IGN。这将防止任何套接字或管道写入引起SIGPIPE信号。

要忽略SIGPIPE信号,请使用以下代码:

signal(SIGPIPE, SIG_IGN);

如果您正在使用send()呼叫,则另一个选项是使用该MSG_NOSIGNAL选项,它将SIGPIPE基于每个呼叫关闭行为。请注意,并非所有操作系统都支持该MSG_NOSIGNAL标志。

最后,您可能还需要考虑SO_SIGNOPIPE可以setsockopt()在某些操作系统上设置的套接字标志。这将防止SIGPIPE仅由于对其设置的套接字进行写操作而引起的。


75
为了使之明确:signal(SIGPIPE,SIG_IGN);
jhclark 2012年

5
如果您获得退出返回码141(128 + 13:SIGPIPE),则可能有必要。SIG_IGN是忽略信号处理程序。
velcrow 2012年

6
对于套接字,@ talash表示,将send()与MSG_NOSIGNAL一起使用确实更容易。
Pawel Veselov

3
如果我的程序写入损坏的管道(在我的情况下是套接字),会发生什么情况?SIG_IGN使程序忽略SIG_PIPE,但是这是否会导致send()做一些不良的事情?
sudo 2014年

2
值得一提的是,自从我找到它一次以来,后来就忘记了它并感到困惑,然后又发现了它。调试器(我知道gdb确实如此)会覆盖您的信号处理,因此忽略的信号不会被忽略!在调试器外部测试您的代码,以确保SIGPIPE不再出现。stackoverflow.com/questions/6821469/...
水上摩托S型

155

另一种方法是更改​​套接字,以便它永远不会在write()上生成SIGPIPE。这在库中更为方便,在库中您可能不需要SIGPIPE的全局信号处理程序。

在大多数基于BSD的(MacOS,FreeBSD ...)系统上(假设您使用的是C / C ++),可以使用以下方法执行此操作:

int set = 1;
setsockopt(sd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));

启用此功能后,将不返回SIGPIPE信号,而是返回EPIPE。


45
听起来不错,但是SO_NOSIGPIPE在Linux中似乎并不存在-因此它不是一种可移植的解决方案。
nobar 2010年

1
大家好,您能告诉我我必须在哪里放置此代码吗?我不明白谢谢的
R. Dewi

9
在Linux中,我在发送
Aadishri

在Mac OS X上完美运行
kirugan

116

我参加聚会的时间太晚了,但是SO_NOSIGPIPE不方便携带,可能无法在您的系统上运行(这似乎是BSD的事情)。

例如,如果您使用的是不带Linux系统的不错选择,那就是在send(2)调用上SO_NOSIGPIPE设置MSG_NOSIGNAL标志。

示例替换write(...)send(...,MSG_NOSIGNAL)(请参阅nobar的注释)

char buf[888];
//write( sockfd, buf, sizeof(buf) );
send(    sockfd, buf, sizeof(buf), MSG_NOSIGNAL );

5
换句话说,使用send(...,MSG_NOSIGNAL)代替write(),您将不会获得SIGPIPE。这对于套接字(在受支持的平台上)应该能很好地工作,但是send()似乎仅限于与套接字(而不是管道)一起使用,因此这不是SIGPIPE问题的一般解决方案。
nobar 2011年

@ Ray2k到底谁在为Linux <2.2开发应用程序?这实际上是一个半严肃的问题。大多数发行版至少带有2.6版本。
Parthian Shot

1
@Parthian Shot:您应该重新考虑您的答案。维护旧嵌入式系统是维护旧Linux版本的有效理由。
kirsche40 2014年

1
@ kirsche40我不是OP,我只是好奇。
Parthian Shot

@sklnd这对我来说非常有效。我使用Qt QTcpSocket对象包装套接字用法,我不得不用writeOS send(使用socketDescriptor方法)替换方法调用。有谁知道可以在QTcpSocket课堂上设置此选项的更清洁的选项?
EmilioGonzálezMontaña'2

29

在这种我描述了可能的解决方案适用于Solaris情况下,当既不SO_NOSIGPIPE也不MSG_NOSIGNAL是可用的。

相反,我们必须在执行库代码的当前线程中暂时禁止SIGPIPE。方法如下:为了抑制SIGPIPE,我们首先检查它是否未决。如果是这样,则意味着它已在该线程中被阻塞,我们无需执行任何操作。如果该库生成其他SIGPIPE,它将与未决的SIGPIPE合并,这是无操作的。如果SIGPIPE没有挂起,则我们在此线程中将其阻止,并检查它是否已被阻止。这样我们就可以自由地执行我们的写操作了。当我们要将SIGPIPE恢复到原始状态时,请执行以下操作:如果SIGPIPE最初处于挂起状态,则不执行任何操作。否则,我们检查它是否正在等待处理。如果确实如此(这意味着out操作已生成一个或多个SIGPIPE),那么我们在此线程中等待,从而清除其挂起状态(为此,我们使用sigtimedwait()并使用零超时;这是为了避免在恶意用户手动将SIGPIPE发送给整个进程的情况下进行阻塞:在这种情况下,我们将看到它处于挂起状态,但是其他线程可能在我们进行更改之前等待它)。清除挂起状态后,我们将在此线程中解除对SIGPIPE的阻止,但前提是原本没有阻止它。

https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156上的示例代码


22

本地处理SIGPIPE

通常最好是在本地处理错误,而不是在全局信号事件处理程序中处理错误,因为在本地,您将对正在发生的事情和采取的措施有更多的了解。

我的一个应用程序中有一个通信层,该层允许我的应用程序与外部附件进行通信。当发生写错误时,我在通信层中引发了异常,并使其冒泡到try catch块中进行处理。

码:

忽略SIGPIPE信号以便可以在本地处理的代码是:

// We expect write failures to occur but we want to handle them where 
// the error occurs rather than in a SIGPIPE handler.
signal(SIGPIPE, SIG_IGN);

该代码将防止SIGPIPE信号升高,但是在尝试使用套接字时会出现读/写错误,因此需要进行检查。


应该在连接插座之后还是之前进行此调用?最好放在哪里。感谢
Ahmed

@Ahmed我亲自将其放在applicationDidFinishLaunching:中。最主要的是应该在与套接字连接进行交互之前。
2013年

但是尝试使用套接字时会出现读/写错误,因此需要进行检查。-请问这怎么可能?signal(SIGPIPE,SIG_IGN)为我工作,但在调试模式下会返回错误,是否可以忽略该错误?
jongbanaag

@ Dreyfus15我相信我只是使用套接字将调用包装在try / catch块中以本地处理它。自从我看到这个以来已经有一段时间了。
2013年

15

您不能阻止管道远端的进程退出,如果在完成写入之前退出该进程,您将收到SIGPIPE信号。如果您对信号SIG_IGN进行了写操作,则您的写操作将返回一个错误-您需要注意并对该错误做出反应。仅捕获并忽略处理程序中的信号不是一个好主意-您必须注意,管道现在已经解散并修改程序的行为,因此它不会再次写入管道(因为信号将再次生成并被忽略再次,您将重试,整个过程可能会持续长时间,并浪费大量CPU能力。


6

还是应该仅使用处理程序来捕获SIGPIPE并忽略它?

我相信那是对的。您想知道另一端何时关闭其描述符,而这正是SIGPIPE告诉您的。

山姆


3

Linux手册说:

EPIPE本端已在面向连接的套接字上关闭。在这种情况下,除非设置了MSG_NOSIGNAL,否则该过程还将收到SIGPIPE。

但是对于Ubuntu 12.04,这是不对的。我为该案例编写了一个测试,并且始终收到带有SIGPIPE的EPIPE。如果我第二次尝试写入同一个损坏的套接字,则会生成SIGPIPE。因此,如果发生此信号,则无需忽略SIGPIPE,这意味着程序中存在逻辑错误。


1
我绝对可以在没有首先在Linux的套接字上看到EPIPE的情况下接收SIGPIPE。这听起来像一个内核错误。
davmac

3

防止此处崩溃的最佳实践是什么?

要么按照每个人的方式禁用sigpipe,要么捕获并忽略该错误。

有没有办法检查行的另一端是否仍在读取?

是的,使用select()。

select()似乎在这里不起作用,因为它总是说套接字是可写的。

您需要选择读取位。您可能会忽略位。

当远端关闭其文件句柄时,select会告诉您有准备读取的数据。当您阅读这些内容时,将获得0字节,这是操作系统告诉您文件句柄已关闭的方式。

唯一不能忽略写位的情况是,如果要发送大量数据,并且另一端存在积压的风险,这可能导致缓冲区填满。如果发生这种情况,那么尝试写入文件句柄可能会导致程序/线程阻塞或失败。在编写之前测试select可以保护您免受此伤害,但不能保证另一端运行状况良好或您的数据即将到达。

请注意,您可以从close()以及编写时获取一个sigpipe。

Close刷新所有缓冲的数据。如果另一端已经关闭,则关闭将失败,并且您将收到一个信号管道。

如果您使用的是缓冲的TCPIP,则成功写入仅表示您的数据已排队等待发送,并不意味着它已发送。在成功调用close之前,您不知道数据已发送。

Sigpipe告诉您出了什么问题,它没有告诉您什么,或者您应该怎么做。


Sigpipe告诉您出了什么问题,它没有告诉您什么,或者您应该怎么做。究竟。实际上,SIGPIPE的唯一目的是在下一个阶段不再需要输入时关闭管道命令行实用程序。如果您的程序正在联网,通常应该在整个程序范围内阻止或忽略SIGPIPE。
DepressedDaniel

恰恰。SIGPIPE适用于“查找XXX |头”之类的情况。一旦head匹配了其10条线,就没有意义继续搜索了。因此,头部退出,下一次find尝试与之对话时,它得到了一个信号,并且知道它也可以退出。
本·阿弗林

确实,SIGPIPE的唯一目的是允许程序草率化,而不检查写入错误!
威廉·珀塞尔

2

在现代POSIX系统(即Linux)下,可以使用该sigprocmask()功能。

#include <signal.h>

void block_signal(int signal_to_block /* i.e. SIGPIPE */ )
{
    sigset_t set;
    sigset_t old_state;

    // get the current state
    //
    sigprocmask(SIG_BLOCK, NULL, &old_state);

    // add signal_to_block to that existing state
    //
    set = old_state;
    sigaddset(&set, signal_to_block);

    // block that signal also
    //
    sigprocmask(SIG_BLOCK, &set, NULL);

    // ... deal with old_state if required ...
}

如果以后要恢复以前的状态,请确保将其保存在old_state安全的地方。如果多次调用该函数,则需要使用堆栈或仅保存第一个或最后一个old_state...,或者可能具有删除特定受阻信号的函数。

有关更多信息,请阅读手册页


不必像这样对一组阻塞信号进行读写修改。sigprocmask会添加到一组受阻信号中,因此您只需一个电话就可以完成所有这些操作。
露丝

我不读取-修改-写入,而是读取要保存的当前状态,old_state以便以后选择时可以将其还原。如果您知道不需要恢复状态,则确实不需要以这种方式读取和存储状态。
亚历克西斯·威尔克

1
但是您可以这样做:第一个调用sigprocmask()读取旧状态,sigaddset修改它的调用,第二个sigprocmask()写入旧状态的调用。您可以删除第一个通话,使用进行初始化,sigemptyset(&set)然后将第二个通话更改为sigprocmask(SIG_BLOCK, &set, &old_state)
Luchs

啊! 哈哈!你是对的。我做。好吧...在我的软件中,我不知道哪些信号已经被阻止或未被阻止。所以我这样做。但是,我同意,如果您只有一个“以这种方式发出信号”,那么简单的clear + set就足够了。
亚历克西斯·威尔克
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.