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

我有一个小的服务器程序,它接受TCP或本地UNIX套接字上的连接,读取一个简单的命令并(取决于命令)发送一个应答。

问题是客户可能对答案不感兴趣,有时会提前退出。因此写入该套接字将导致SIGPIPE并使我的服务器崩溃。

在这里防止崩溃的最佳实践是什么?有没有办法检查另一边是否还在阅读?(select()在这里似乎不起作用,因为它总是说套接字是可写的)。或者我应该用处理程序捕获SIGPIPE并忽略它?

231020 次浏览

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

我相信这是对的。您想知道另一端什么时候关闭了它们的描述符,这就是SIGPIPE告诉您的。

山姆

你通常想要忽略SIGPIPE并直接在你的代码中处理错误。这是因为C语言中的信号处理程序对它们能做的事情有很多限制。

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

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

signal(SIGPIPE, SIG_IGN);

如果你正在使用send()调用,另一个选项是使用MSG_NOSIGNAL选项,它将在每次调用的基础上关闭SIGPIPE行为。注意,并非所有操作系统都支持MSG_NOSIGNAL标志。

最后,你可能还想考虑可以在某些操作系统上用setsockopt()设置的SO_SIGNOPIPE套接字标志。这将防止SIGPIPE仅由写入它所设置的套接字引起。

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

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

在大多数基于bsd (MacOS, FreeBSD…)的系统上,(假设你使用的是C/ c++),你可以通过以下方法做到这一点:

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

这样,就不会生成SIGPIPE信号,而是返回EPIPE信号。

我很晚才加入这个派对,但是SO_NOSIGPIPE是不可移植的,并且可能无法在您的系统上工作(这似乎是BSD的事情)。

如果你在一个没有SO_NOSIGPIPE的Linux系统上,一个很好的替代方法是在send(2)调用上设置MSG_NOSIGNAL标志。

示例:用send(...,MSG_NOSIGNAL)替换write(...)(参见nobar的注释)

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

在这个帖子中,我描述了SO_NOSIGPIPE和MSG_NOSIGNAL都不可用的Solaris情况下可能的解决方案。

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

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

本地处理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信号,但是在尝试使用套接字时将会得到一个读/写错误,因此需要检查这一点。

Linux手册说:

EPIPE本端在面向连接的情况下被关闭 套接字。在这种情况下,进程还将接收一个SIGPIPE 除非MSG_NOSIGNAL被设置

但是对于Ubuntu 12.04来说,这是不对的。我为这种情况编写了一个测试,我总是收到没有SIGPIPE的EPIPE。如果我试图第二次写入同一个损坏的套接字,就会生成SIGPIPE。所以你不需要忽略SIGPIPE,如果这个信号发生了,这意味着你的程序中有逻辑错误。

在这里防止崩溃的最佳实践是什么?

要么像每个人一样禁用sigpipe,要么捕获并忽略错误。

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

是的,使用select()。

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

你需要选择位。你可以忽略位。

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

唯一不能忽略写位的情况是,如果您正在发送大量数据,那么另一端有积压的风险,这可能会导致缓冲区被填满。如果发生这种情况,那么尝试写入文件句柄可能会导致程序/线程阻塞或失败。在写入之前测试select可以避免这种情况,但它不能保证另一端是健康的,也不能保证数据会到达。

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

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

如果您正在使用缓冲的TCPIP,那么成功的写入仅仅意味着您的数据已经排队发送,而不意味着它已经发送。直到成功调用close,您才知道数据已经发送。

Sigpipe会告诉你哪里出了问题,但不会告诉你出了什么问题,也不会告诉你应该怎么做。

在现代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…或者有一个函数可以移除特定的阻塞信号。

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