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

时间:2008-09-20 13:43:21

标签: c io signals broken-pipe sigpipe

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

10 个答案:

答案 0 :(得分:229)

您通常希望忽略SIGPIPE并直接在代码中处理错误。这是因为C中的信号处理程序对它们可以做的事情有很多限制。

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

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

signal(SIGPIPE, SIG_IGN);

如果您正在使用send()来电,则另一个选项是使用MSG_NOSIGNAL选项,这会在每次通话的基础上关闭SIGPIPE行为。请注意,并非所有操作系统都支持MSG_NOSIGNAL标志。

最后,您可能还想考虑在某些操作系统上可以使用SO_SIGNOPIPE设置的setsockopt()套接字标志。这样可以防止SIGPIPE只是写入它所设置的套接字引起的。{/ p>

答案 1 :(得分:148)

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

在大多数基于BSD(MacOS,FreeBSD ...)的系统上,(假设你使用的是C / C ++),你可以这样做:

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

实现此功能后,将返回EPIPE,而不是生成SIGPIPE信号。

答案 2 :(得分:111)

我参加派对已经很晚了,但是SO_NOSIGPIPE不可移植,可能无法在您的系统上运行(它似乎是BSD的事情)。

如果您使用没有SO_NOSIGPIPE的Linux系统,那么一个不错的选择是在send(2)调用上设置MSG_NOSIGNAL标志。

write(...)替换为send(...,MSG_NOSIGNAL)的示例(请参阅nobar的评论)

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

答案 3 :(得分:29)

在这个post中,当SO_NOSIGPIPE和MSG_NOSIGNAL都不可用时,我描述了Solaris案例的可能解决方案。

  

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

https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156

的示例代码

答案 4 :(得分: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信号,但在尝试使用套接字时会出现读/写错误,因此您需要检查该信号。

答案 5 :(得分:15)

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

答案 6 :(得分:6)

  

或者我应该只使用处理程序捕获SIGPIPE并忽略它?

我相信这是正确的。你想知道另一端何时关闭了他们的描述符,这就是SIGPIPE告诉你的。

萨姆

答案 7 :(得分:3)

Linux手册说:

  

EPIPE本地端已关闭面向连接                 插座。在这种情况下,该过程也将收到SIGPIPE                 除非设置了MSG_NOSIGNAL。

但对于Ubuntu 12.04来说,这是不对的。我为那个案子写了一个测试,我总是收到EPIPE withot SIGPIPE。如果我第二次尝试写入相同的断开套接字,则会生成SIGPIPE。因此,如果发生此信号,则无需忽略SIGPIPE,这意味着程序中存在逻辑错误。

答案 8 :(得分:3)

  

防止坠机的最佳做法是什么?

根据每个人禁用sigpipes,或者捕获并忽略错误。

  

有没有办法检查线的另一边是否还在读?

是的,请使用select()。

  

select()似乎没有在这里工作,因为它总是说套接字是可写的。

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

当远端关闭其文件句柄时,选择将告诉您有可供读取的数据。当你去阅读它时,你会得到0字节,这就是操作系统告诉你文件句柄已经关闭的方式。

如果您要发送大量数据,则唯一无法忽略写入位的时间,并且存在另一端积压的风险,这可能导致缓冲区填满。如果发生这种情况,那么尝试写入文件句柄会导致程序/线程阻塞或失败。在写作之前进行测试选择可以保护您免受这种影响,但它并不能保证另一端健康或者您的数据将会到达。

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

关闭刷新所有缓冲的数据。如果另一端已经关闭,则关闭将失败,您将收到一根sigpipe。

如果您使用的是缓冲TCPIP,那么成功写入只意味着您的数据已排队等待发送,并不意味着它已被发送。在您成功拨打电话之前,您不知道您的数据已被发送。

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

答案 9 :(得分: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 ...或者可能具有删除特定阻塞信号的函数。

有关详细信息,请阅读man page