为什么关闭TCP套接字比读取套接字更糟糕?

14

当你读取一个关闭的TCP套接字时,会得到一个常规错误,即它要么返回0表示EOF,要么返回-1并在errno中返回一个错误代码,该错误代码可以使用perror打印。

然而,当你写一个关闭的TCP套接字时,操作系统会向你的应用程序发送SIGPIPE信号,如果未被捕获,将终止应用程序。

为什么写关闭的TCP套接字比读取更糟糕?


这里还有一些相当微妙的事情:TCP连接可以是半关闭状态,这意味着一方已经关闭了套接字(发送了一个FIN数据包),但另一方仍然有数据要发送。如果您要在这个级别上进行探索,请阅读:http://superuser.com/questions/298919/what-is-tcp-half-open-connection-and-tcp-half-closed-connection - rbp
4个回答

12

感谢Greg Hewgill帮助我正确地找到答案。

在Unix系统中,套接字和管道都会出现SIGPIPE的真正原因是过滤器惯用语法/模式。

从管道开始。像grep这样的过滤程序通常写入STDOUT并从STDIN读取,这可能会被shell重定向到管道中。例如:

cat someVeryBigFile | grep foo | doSomeThingErrorProne

当shell分叉并执行这些程序时,可能使用dup2系统调用将STDINSTDOUTSTDERR重定向到适当的管道。由于过滤程序grep不知道并且没有办法知道它的输出已被重定向,因此如果doSomeThingErrorProne崩溃,则告诉它停止写入破损的管道的唯一方法是使用信号,因为很少检查对STDOUT的写入返回值。
与套接字的类比是inetd服务器代替shell。例如,我假设您可以将grep转换为通过TCP套接字运行的网络服务。例如,如果要在TCP端口8000上拥有grep服务器,则将其添加到/etc/services中。
grep     8000/tcp   # grep server

然后将以下内容添加到/etc/inetd.conf中:

grep  stream tcp nowait root /usr/bin/grep grep foo

发送SIGHUPinetd,并使用telnet连接到8000端口。这应该会导致inetd分叉,将套接字复制到STDINSTDOUTSTDERR,然后使用foo作为参数执行grep。如果您开始在telnet中键入行,则grep将回显包含foo的行。

现在用一个名为ticker的程序替换telnet,例如将实时股票报价流写入STDOUT并在STDIN上获取命令。有人通过telnet连接到8000端口并键入"start java"以获取Sun Microsystems的报价。然后他们起身去吃午饭。telnet莫名其妙地崩溃了。如果没有SIGPIPE要发送,那么ticker将永远发送报价,从未知道另一端的进程已经崩溃,并且不必要地浪费系统资源。


10
通常情况下,如果您要写入一个套接字,您会期望另一端正在监听。这有点像电话 - 如果您在说话,您不会期望对方挂断通话。
如果您从套接字中读取数据,则期望另一端要么(a)发送给您某些内容,要么(b)关闭该套接字。情况(b)发生的原因是您刚刚向另一端发送了诸如QUIT命令之类的内容。

但这并没有真正告诉我为什么writesend不能像readrecv一样直接返回错误。为什么要用SIGPIPE来打断应用程序呢?操作系统做出如此极端的响应肯定有更深层次的原因。比如说,如果我有一个套接字刚刚收到了一个RST,如果我使用read读取它,我会得到ECONNRESET的-1,那么为什么在写入时不直接得到相同的结果呢?在这两种情况下,我都期望进行协商I/O,而没有得到我期望的结果。 - Robert S. Barnes
6
在Unix系统中,管道输入和输出的典型用例是用于“过滤”程序。这些程序从输入管道读取数据并将结果写入输出管道(例如grep程序)。为了使这样的过滤器在输出不再监听时立即终止,SIGPIPE信号的默认行为被设置为终止程序。如果没有这个功能,过滤器将继续向输出写入数据,直到其输入被耗尽(这可能需要一段时间)。 - Greg Hewgill
2
告诉我这是否听起来正确:SIGPIPE的真正原因是,像grep这样的过滤程序通常会写入STDOUT,而shell可能会将其重定向到一个管道。由于过滤程序不知道也无法知道其输出已经被重定向,所以唯一的方式就是通过信号告诉它停止向破损的管道写入,因为很少有人检查对STDOUT的写入返回值。与套接字类似的情况是inetd接受连接,生成服务器并将套接字复制到STDINSTDOUTSTDERR上! - Robert S. Barnes
@Robert:是的,听起来你已经明白了。 - Greg Hewgill

7

把socket想象成发送和接收进程之间的一个大数据管道。现在想象一下,这个管道有一个关闭的阀门(socket连接已关闭)。

如果你从socket中读取数据(试图从管道中获取内容),那么尝试读取不存在的数据是没有害处的;你只是无法获得任何数据。实际上,你可能会像你所说的那样收到EOF信号,因为没有更多的数据可以读取了。

然而,写入到这个关闭的连接就不同了。数据将无法通过,你可能会丢失一些重要的通信内容。(如果你试图向关闭阀门的管道中注入水,可能会导致某个地方爆炸,或者至少会产生反向压力将水喷洒到各个地方。)这就是为什么有一个更强大的工具来提醒你这种情况,即SIGPIPE信号。

你总是可以忽略或阻止该信号,但你自己承担风险。


3
我认为答案的很大一部分是“使套接字的行为与经典的Unix(匿名)管道相似”。它们也表现出相同的行为-请看信号的名称。因此,合理地问为什么管道会表现出这种行为。Greg Hewgill的答案概括了情况。另一种看待它的方式是-还有什么选择?如果没有写入程序的管道上执行“read()”会发出SIGPIPE信号吗?当然,SIGPIPE的含义必须更改为“在没有人读取它的管道上写入”,但这是微不足道的。没有特别的理由认为它会更好;EOF指示(零字节可读;已读零字节)是管道状态的完美描述,因此read的行为很好。那么“write()”呢?好吧,一个选项是返回写入的字节数-零。但这不是一个好主意;它意味着代码应该再试一次,也许会发送更多的字节,但实际上不会发生这种情况。另一个选项是错误-write()返回-1并设置适当的errno。不清楚是否存在一个。EINVAL或EBADF都不准确:文件描述符在这一端是正确且打开的(并且应在失败的写入后关闭);只是没有任何东西可以读取它。EPIPE意味着“破损的PIPE”;因此,除了“这是套接字而不是管道”的警告之外,它将是适当的错误。如果忽略SIGPIPE,则可能返回的errno。在管道的上下文中,当管道断开连接时发送信号对操作系统有益;套接字非常接近于管道。有趣的一面:在检查SIGPIPE的消息时,我发现了套接字选项:
#define SO_NOSIGPIPE 0x1022 /* APPLE: No SIGPIPE on EPIPE */

所以基本上你的意思是SIGPIPE存在是因为很多程序员在写操作时忽略了错误码,这可能会导致进程占用系统资源而实际上并没有完成任何事情?或者换句话说,人们更加细心地检查输入而不是输出,这就是readwrite之间不对称的原因? - Robert S. Barnes
@Robert:是的,基本上是这样。人们往往在假定输出设备不会消失或空间不足的情况下编写代码。当输出是管道且接收程序在输出结束之前停止读取时,确保编写程序注意此事非常重要。而这是一个简单的机制,使得编写程序更加简单。 - Jonathan Leffler
那么在 SIGPIPE 之前有过这样的时期吗?由于您说这在某种程度上是用户/程序员不良行为的结果,是否曾经有一个 Unix 版本在向关闭的管道写入时返回错误,然后将其更改为返回信号,或者 SIGPIPE 从一开始就存在,以预防不良行为? - Robert S. Barnes
2
@Robert S. Barnes:管道在Unix中很早就被添加了。根据丹尼斯·里奇在《UNIX分时系统的演变》一书中的说法,管道在PDP-7版本的UNIX上不可用,并于1972年(比UNIX的第一个版本晚2-3年)添加到PDP-11版本中。(参考文献:“UNIX® SYSTEM: Readings and Applications, Volume II”,1987年。这是AT&T(贝尔)专门致力于Unix的期刊的第二版,包含一些有趣的内容。它由Prentice-Hall出版,ISBN为0-13-939845-7 - http://www.amazon.com/Unix-System-Readings-Applications-UNIX-R/dp/0139398457) - Jonathan Leffler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接