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

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

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


当前回答

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

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

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

signal(SIGPIPE, SIG_IGN);

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

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

其他回答

我加入这个聚会非常晚,但是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 );

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

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

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

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

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

更多信息请阅读手册页。