我有一个小型服务器程序,它接受 TCP 或本地 UNIX 套接字上的连接,读取一个简单的命令并(取决于命令)发送回复。
问题是客户可能对答案不感兴趣,有时会提前退出。因此,写入该套接字将导致 aSIGPIPE
并使我的服务器崩溃。
在这里防止崩溃的最佳做法是什么?有没有办法检查线路的另一端是否仍在阅读?(select()
似乎在这里不起作用,因为它总是说套接字是可写的)。还是我应该SIGPIPE
用处理程序捕获并忽略它?
我有一个小型服务器程序,它接受 TCP 或本地 UNIX 套接字上的连接,读取一个简单的命令并(取决于命令)发送回复。
问题是客户可能对答案不感兴趣,有时会提前退出。因此,写入该套接字将导致 aSIGPIPE
并使我的服务器崩溃。
在这里防止崩溃的最佳做法是什么?有没有办法检查线路的另一端是否仍在阅读?(select()
似乎在这里不起作用,因为它总是说套接字是可写的)。还是我应该SIGPIPE
用处理程序捕获并忽略它?
您通常希望忽略SIGPIPE
并直接在代码中处理错误。这是因为 C 中的信号处理程序对它们的功能有很多限制。
最便携的方法是将SIGPIPE
处理程序设置为SIG_IGN
. 这将防止任何套接字或管道写入导致SIGPIPE
信号。
要忽略该SIGPIPE
信号,请使用以下代码:
signal(SIGPIPE, SIG_IGN);
如果您正在使用send()
呼叫,另一种选择是使用该MSG_NOSIGNAL
选项,这将SIGPIPE
在每次呼叫的基础上关闭行为。请注意,并非所有操作系统都支持该MSG_NOSIGNAL
标志。
最后,您可能还需要考虑SO_SIGNOPIPE
可以setsockopt()
在某些操作系统上设置的套接字标志。这将防止SIGPIPE
仅由对其设置的套接字的写入引起。
另一种方法是更改套接字,使其永远不会在 write() 上生成 SIGPIPE。这在库中更方便,您可能不需要 SIGPIPE 的全局信号处理程序。
在大多数基于 BSD(MacOS、FreeBSD...)的系统上,(假设您使用的是 C/C++),您可以这样做:
int set = 1;
setsockopt(sd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));
这样一来,将返回 EPIPE,而不是生成 SIGPIPE 信号。
我迟到了,但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 );
在这篇文章中,我描述了当 SO_NOSIGPIPE 和 MSG_NOSIGNAL 都不可用时 Solaris 情况的可能解决方案。
相反,我们必须在当前执行库代码的线程中暂时抑制 SIGPIPE。下面是如何做到这一点:为了抑制 SIGPIPE,我们首先检查它是否处于挂起状态。如果是这样,这意味着它被阻塞在这个线程中,我们不必做任何事情。如果库生成额外的 SIGPIPE,它将与待处理的合并,这是一个空操作。如果 SIGPIPE 没有挂起,那么我们在这个线程中阻塞它,并检查它是否已经被阻塞。然后我们可以自由地执行我们的写入。当我们要将 SIGPIPE 恢复到其原始状态时,我们执行以下操作:如果 SIGPIPE 最初处于挂起状态,我们什么也不做。否则,我们检查它现在是否处于挂起状态。如果是这样(这意味着 out 操作已经生成了一个或多个 SIGPIPE),那么我们在这个线程中等待它,从而清除其挂起状态(为此,我们使用零超时的 sigtimedwait();这是为了避免在恶意用户手动向整个进程发送 SIGPIPE 的情况下阻塞:在这种情况下,我们将看到它挂起,但其他线程可能在我们有更改等待它之前处理它)。在清除挂起状态后,我们在这个线程中解除阻塞 SIGPIPE,但前提是它最初没有被阻塞。
示例代码位于https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156
通常最好在本地而不是在全局信号事件处理程序中处理错误,因为在本地您将有更多关于正在发生的事情和采取什么追索权的上下文。
我的一个应用程序中有一个通信层,允许我的应用程序与外部附件通信。当发生写入错误时,我在通信层中抛出异常并让它冒泡到 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 信号被引发,但在尝试使用套接字时会出现读/写错误,因此您需要进行检查。
您无法阻止管道远端的进程退出,如果它在您完成写入之前退出,您将收到一个 SIGPIPE 信号。如果您 SIG_IGN 信号,那么您的写入将返回一个错误 - 您需要注意并对该错误做出反应。仅仅在处理程序中捕获和忽略信号并不是一个好主意——您必须注意管道现在已经失效并修改程序的行为,以便它不会再次写入管道(因为信号将再次生成,并被忽略再次,你会再试一次,整个过程可能会持续很长时间,并且会浪费大量的 CPU 资源)。
还是我应该用处理程序捕获 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,要么捕获并忽略错误。
有没有办法检查线路的另一端是否仍在阅读?
是的,使用 select()。
select() 似乎在这里不起作用,因为它总是说套接字是可写的。
您需要在读取位上进行选择。您可能可以忽略写入位。
当远端关闭其文件句柄时,select 将告诉您有数据可供读取。当你去读它时,你会得到 0 个字节,这就是操作系统告诉你文件句柄已关闭的方式。
唯一不能忽略写入位的情况是,如果您要发送大量数据,并且另一端存在积压的风险,这可能会导致您的缓冲区被填满。如果发生这种情况,那么尝试写入文件句柄可能会导致您的程序/线程阻塞或失败。在写入之前测试 select 可以保护您免受这种情况的影响,但它不能保证另一端是健康的或者您的数据将会到达。
请注意,您可以从 close() 以及编写时获取 sigpipe。
关闭刷新所有缓冲数据。如果另一端已经关闭,则关闭将失败,您将收到一个 sigpipe。
如果您使用的是缓冲 TCPIP,则成功写入仅意味着您的数据已排队等待发送,并不意味着已发送。在您成功调用 close 之前,您不知道您的数据已经发送。
Sigpipe 会告诉您出现了问题,但不会告诉您发生了什么,或者您应该怎么做。
Linux手册说:
EPIPE 本地端已在面向连接的套接字上关闭。在这种情况下,除非设置了 MSG_NOSIGNAL,否则进程也会收到 SIGPIPE。
但是对于 Ubuntu 12.04 是不对的。我为那个案例写了一个测试,我总是收到没有 SIGPIPE 的 EPIPE。如果我第二次尝试写入同一个损坏的套接字,则会生成 SIGPIPE。因此,如果发生此信号,您无需忽略 SIGPIPE,这意味着您的程序中存在逻辑错误。