如何防止SIGPIPE(或正确处理)
我有一个小型的服务器程序,它接受TCP或本地UNIX套接字上的连接,读取一个简单的命令,并根据命令发送一个回复。 问题是客户端有时候可能对这个答案没有兴趣,并且会提前退出,所以写入这个套接字将导致一个SIGPIPE,并使我的服务器崩溃。 最好的做法是防止这里的崩溃? 有没有办法检查线路的另一边是否仍在读取? (select()在这里似乎没有工作,因为它总是说套接字是可写的)。 还是应该用一个处理程序来捕获SIGPIPE并忽略它?
您通常要忽略SIGPIPE
并直接在您的代码中处理错误。 这是因为C中的信号处理程序对它们可以执行的操作有很多限制。
最简单的方法是将SIGPIPE
处理程序设置为SIG_IGN
。 这将防止任何套接字或pipe道写入引起SIGPIPE
信号。
要忽略SIGPIPE
信号,请使用以下代码:
signal(SIGPIPE, SIG_IGN);
如果使用send()
调用,则另一个选项是使用MSG_NOSIGNAL
选项,该选项将closures每个调用的SIGPIPE
行为。 请注意,并非所有操作系统都支持MSG_NOSIGNAL
标志。
最后,您可能还想考虑在某些操作系统上可以使用setsockopt()
设置的SO_SIGNOPIPE
套接字标志。 这将防止SIGPIPE
被写入到它所设置的套接字中。
另一种方法是更改套接字,使其不会在write()上生成SIGPIPE。 这在库中更方便,你可能不需要SIGPIPE的全局信号处理程序。
在大多数系统上(假设你正在使用C / C ++),你可以这样做:
int set = 1; setsockopt(sd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));
实际上,不是生成SIGPIPE信号,而是返回EPIPE。
我对这个晚会太晚了,但是SO_NOSIGPIPE
不是可移植的,可能不适合你的系统(这似乎是一个BSD的东西)。
一个不错的select,如果你在一个没有SO_NOSIGPIPE
的Linux系统上, SO_NOSIGPIPE
就是在你的send(2)调用中设置MSG_NOSIGNAL
标志。
用send(...,MSG_NOSIGNAL)
替代write(...)
的send(...,MSG_NOSIGNAL)
(见nobar的评论)
char buf[888]; //write( sockfd, buf, sizeof(buf) ); send( sockfd, buf, sizeof(buf), MSG_NOSIGNAL );
在这篇文章中,我描述了Solaris可能的解决scheme,即SO_NOSIGPIPE和MSG_NOSIGNAL都不可用。
相反,我们必须暂时禁止执行库代码的当前线程中的SIGPIPE。 以下是如何做到这一点:为了抑制SIGPIPE,我们首先检查它是否处于挂起状态。 如果是这样,这意味着它在这个线程被阻塞,我们不得不做任何事情。 如果库生成额外的SIGPIPE,它将与待处理的合并,这是一个无操作。 如果SIGPIPE没有挂起,那么我们在这个线程中阻塞它,并且检查它是否已经被阻塞。 然后我们可以自由地执行我们的写作。 当我们要将SIGPIPE恢复到原来的状态时,我们做了以下的事情:如果SIGPIPE本来是挂起的,我们什么都不做。 否则,我们检查是否现在挂起。 如果它(这意味着out操作已经产生了一个或多个SIGPIPE),那么我们在这个线程中等待它,从而清除它的挂起状态(为了做到这一点,我们使用sigtimedwait()以零超时;这是为了避免阻塞在这种情况下,恶意用户手动发送SIGPIPE到整个进程:在这种情况下,我们会看到它挂起,但其他线程可能会处理它,然后再等待它。 在清除挂起状态之后,我们在这个线程中解除SIGPIPE,但只有在原来没有被阻塞的情况下。
示例代码https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156
本地处理SIGPIPE
通常情况下,最好在本地处理错误,而不是在全局信号事件处理程序中处理,因为在本地情况下,您将掌握更多的上下文以及采取何种处理方式。
我在我的一个应用程序中有一个通信层,允许我的应用程序与外部附件进行通信。 当发生写入错误时,我会在通信层中抛出exception,并让它冒泡到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信号被提出,但是当你尝试使用套接字的时候你会得到一个读/写错误,所以你需要检查这个。
您不能阻止pipe道远端的进程退出,如果在写完之前退出,您将收到SIGPIPE信号。 如果你SIG_IGN信号,那么你的写将返回一个错误 – 你需要注意并对这个错误作出反应。 只是在处理程序中捕获和忽略信号并不是一个好主意 – 您必须注意,pipe道现在已经不存在了,并且修改了程序的行为,所以它不会再次写入pipe道(因为信号会再次生成,并被忽略再次尝试,整个过程可能会持续很长时间,浪费大量的CPU资源)。
还是应该用一个处理程序来捕获SIGPIPE并忽略它?
我相信这是对的。 你想知道什么时候另一端closures了它们的描述符,这就是SIGPIPE告诉你的。
山姆
Linux手册说:
EPIPE本地端已经在面向连接的套接字上closures了。 在这种情况下,除非设置了MSG_NOSIGNAL,否则该进程还将收到SIGPIPE。
但是对于Ubuntu 12.04,这是不对的。 我为这种情况写了一个testing,我总是收到EPIPE SIGPIPE。 如果我第二次尝试写入同一个破坏的套接字,则SIGPIPE会得到生成。 所以你不需要忽略SIGPIPE,如果这个信号发生,这意味着你的程序中的逻辑错误。
最好的做法是防止这里的崩溃?
要么根据每个人禁用sigpipes,要么忽略错误。
有没有办法检查线路的另一边是否仍在读取?
是的,使用select()。
select()在这里似乎不起作用,因为它总是说套接字是可写的。
您需要select读取位。 你可以忽略写入位。
当远端closures文件句柄时,select会告诉你有数据准备好读取。 当你去读这个,你会得到0字节,这是操作系统告诉你,文件句柄已经closures。
唯一不能忽略写入位的是,如果你正在发送大量数据,并且存在另一端可能会积压的风险,这会导致你的缓冲区被填满。 如果发生这种情况,那么试图写入文件句柄会导致程序/线程阻塞或失败。 在写作之前进行testingselect可以保护您免受这种情况的侵害,但是不能保证另一端是健康的或您的数据将会到达。
请注意,您可以从close()中获取sigpipe,也可以在写入时获取sigpipe。
closures刷新任何缓冲的数据。 如果另一端已经closures,则closures将失败,并且您将收到一个sigpipe。
如果你正在使用缓冲的TCPIP,那么写入成功只是意味着你的数据已经排队发送,并不意味着它已经发送。 在您成功致电closures之前,您不知道您的数据已发送。
Sigpipe告诉你出了什么问题,它不会告诉你什么,或者你应该怎么做。
在现代的POSIX系统下(例如Linux),可以使用sigprocmask()
函数。
#include <signal.h> void block_signal(int signal_to_block /* ie SIGPIPE */ ) { sigset_t set; sigemptyset(&set); sigaddset(&set, signal_to_block); // block the signal // sigset_t old_state; sigprocmask(SIG_BLOCK, &set, &old_state); // ... deal with old_state if required ... }
如果您想稍后恢复以前的状态,请确保将old_state
保存在安全的地方。 如果你不关心旧状态,你可能只是传递NULL
。
欲了解更多信息阅读手册页 。