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

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

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


当前回答

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

Instead, we have to temporarily suppress SIGPIPE in the current thread that executes library code. Here's how to do this: to suppress SIGPIPE we first check if it is pending. If it does, this means that it is blocked in this thread, and we have to do nothing. If the library generates additional SIGPIPE, it will be merged with the pending one, and that's a no-op. If SIGPIPE is not pending then we block it in this thread, and also check whether it was already blocked. Then we are free to execute our writes. When we are to restore SIGPIPE to its original state, we do the following: if SIGPIPE was pending originally, we do nothing. Otherwise we check if it is pending now. If it does (which means that out actions have generated one or more SIGPIPEs), then we wait for it in this thread, thus clearing its pending status (to do this we use sigtimedwait() with zero timeout; this is to avoid blocking in a scenario where malicious user sent SIGPIPE manually to a whole process: in this case we will see it pending, but other thread may handle it before we had a change to wait for it). After clearing pending status we unblock SIGPIPE in this thread, but only if it wasn't blocked originally.

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

其他回答

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

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

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

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

是的,使用select()。

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

您需要在读取位上进行选择。你可以忽略写的部分。

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

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

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

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

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

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

Linux手册说:

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

但是对于Ubuntu 12.04来说,这是不对的。我为这种情况编写了一个测试,我总是收到没有SIGPIPE的EPIPE。如果我试图第二次写入同一个损坏的套接字,就会生成SIGPIPE。所以你不需要忽略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…或者有一个函数可以移除特定的阻塞信号。

更多信息请阅读手册页。

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

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

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

signal(SIGPIPE, SIG_IGN);

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

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