为何关闭TCP连接后首次send()不会触发SIGPIPE信号?
这是个非常典型的TCP连接状态机行为问题,咱们一步步拆解背后的逻辑:
1. 首次send成功的原因
当服务端主动调用close()关闭socket时,会向客户端发送一个FIN包,表示服务端不再发送数据,但此时客户端的TCP连接处于CLOSE_WAIT状态——客户端的发送通道其实还没被内核标记为失效。
当你调用send()时,这个函数的核心逻辑是把数据从用户态拷贝到内核的发送缓冲区,只要缓冲区有足够空间,send()就会立刻返回成功的字节数,不会立刻去检查对端是否还能接收。
此时服务端虽然已经关闭,但客户端还没收到服务端后续返回的RST包(因为服务端收到客户端首次send的数据后,发现连接已经关闭,才会发送RST包响应),所以内核认为发送操作是合法的,自然返回成功。
2. 第二次send触发SIGPIPE的原因
首次send的数据到达服务端后,服务端的TCP栈发现连接已经完全关闭,会向客户端返回一个RST包。客户端收到RST包后,内核会把该socket标记为已失效状态。
当你第二次调用send()时,内核检测到socket已经处于失效状态,就会触发SIGPIPE信号(默认行为是终止进程),同时send()返回-1,errno被设置为EPIPE。
复现代码示例
服务端简化代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serv_addr = {AF_INET, htons(8080), INADDR_ANY}; bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); listen(sockfd, 1); int connfd = accept(sockfd, NULL, NULL); // 等待客户端连接后,直接关闭连接 close(connfd); close(sockfd); return 0; }
客户端简化代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serv_addr = {AF_INET, htons(8080), {inet_addr("127.0.0.1")}}; connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); char buf[] = "hello"; // 第一次send ssize_t ret1 = send(sockfd, buf, strlen(buf), 0); printf("First send returned: %zd\n", ret1); // 第二次send ssize_t ret2 = send(sockfd, buf, strlen(buf), 0); printf("Second send returned: %zd\n", ret2); close(sockfd); return 0; }
操作步骤:
- 编译并启动服务端,服务端接受连接后立刻关闭
- 启动客户端,观察输出:第一次send返回5,第二次send触发SIGPIPE导致进程终止(如果没处理信号的话)
解决办法
如果你不想让进程被SIGPIPE终止,可以用以下几种方式处理:
忽略SIGPIPE信号:在程序开头添加信号处理,这样send会返回EPIPE错误,你可以在代码中捕获并处理:
#include <signal.h> signal(SIGPIPE, SIG_IGN);使用MSG_NOSIGNAL标志调用send:每次send时加上这个标志,内核就不会触发SIGPIPE,直接返回错误:
ssize_t ret = send(sockfd, buf, len, MSG_NOSIGNAL); if (ret == -1 && errno == EPIPE) { // 处理连接关闭的情况 printf("Connection closed by peer\n"); }结合IO多路复用检测:用
select()/poll()/epoll()先检测socket是否可写,但这种方式不能完全避免EPIPE(因为TCP状态可能在检测和send之间变化),所以还是建议结合前两种方式。
内容的提问来源于stack exchange,提问作者TsReaper




