send或者write socket遭遇SIGPIPE信号

本文深入探讨了Linux下socket编程中遭遇SIGPIPE信号的原理及应对策略,包括如何安全屏蔽SIGPIPE信号,处理服务器并发问题,以及在不同场景下如何避免不必要的进程终止。详细解释了RST信号的含义及其引发的机制,并提供了解决方案,确保程序稳定运行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

send或者write socket遭遇SIGPIPE信号(转)

转自:http://linux.chinaunix.net/techdoc/net/2008/10/10/1037290.shtml

当服务器close一个连接时,若client端接着发数据。根据TCP协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。

又或者当一个进程向某个已经收到RST的socket执行写操作是,内核向该进程发送一个SIGPIPE信号。该信号的缺省学位是终止进程,因此进程必须捕获它以免不情愿的被终止。


根据信号的默认处理规则SIGPIPE信号的默认执行动作是terminate(终止、退出),所以client会退出。若不想客户端退出可以把 SIGPIPE设为SIG_IGN

如:signal(SIGPIPE, SIG_IGN);
这时SIGPIPE交给了系统处理。

服务器采用了fork的话,要收集垃圾进程,防止僵尸进程的产生,可以这样处理:
signal(SIGCHLD,SIG_IGN);
交给系统init去回收。
这里子进程就不会产生僵尸进程了。


在linux下写socket的程序的时候,如果尝试send到一个disconnected socket上,就会让底层抛出一个SIGPIPE信号。
这个信号的缺省处理方法是退出进程,大多数时候这都不是我们期望的。因此我们需要重载这个信号的处理方法。调用以下代码,即可安全的屏蔽SIGPIPE:
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigaction( SIGPIPE, &sa, 0 );

signal设置的信号句柄只能起一次作用,信号被捕获一次后,信号句柄就会被还原成默认值了。
sigaction设置的信号句柄,可以一直有效,值到你再次改变它的设置。

struct sigaction action;
action.sa_handler = handle_pipe;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGPIPE, &action, NULL);
void handle_pipe(int sig)
{
        //不做任何处理即可
}

RST的含义为“复位”,它是TCP在某些错误情况下所发出的一种TCP分节。有三个条件可以产生RST:

1), SYN到达某端口但此端口上没有正在监听的服务器。
2), TCP想取消一个已有连接
3), TCP接收了一个根本不存在的连接上的分节。

1. Connect 函数返回错误ECONNREFUSED:
如果对客户的SYN的响应是RST,则表明该服务器主机在我们指定的端口上没有进程在等待与之连接(例如服务器进程也许没有启动),这称为硬错(hard error),客户一接收到RST,马上就返回错误ECONNREFUSED.

TCP为监听套接口维护两个队列。两个队列之和不超过listen函数第二个参数backlog。

当一个客户SYN到达时,若两个队列都是满的,TCP就忽略此分节,且不发送RST.这个因为:这种情况是暂时的,客户TCP将重发SYN,期望不久就能在队列中找到空闲条目。要是TCP服务器发送了一个RST,客户connect函数将立即发送一个错误,强制应用进程处理这种情况,而不是让TCP正常的重传机制来处理。还有,客户区别不了这两种情况:作为SYN的响应,意为“此端口上没有服务器”的RST和意为“有服务器在此端口上但其队列满”的 RST.
Posix.1g允许以下两种处理方法:忽略新的SYN,或为此SYN响应一个RST.历史上,所有源自Berkeley的实现都是忽略新的SYN。

2.如果杀掉服务器端处理客户端的子进程,进程退出后,关闭它打开的所有文件描述符,此时,当服务器TCP接收到来自此客户端的数据时,由于先前打开的那个套接字接口的进程已终止,所以以RST响应。

经常遇到的问题:
如果不判断read , write函数的返回值,就不知道服务器是否响应了RST, 此时客户端如果向接收了RST的套接口进行写操作时,内核给该进程发一个SIGPIPE信号。此信号的缺省行为就是终止进程,所以,进程必须捕获它以免不情愿地被终止。

进程不论是捕获了该信号并从其信号处理程序返回,还是不理会该信号,写操作都返回EPIPE错误。


3. 服务器主机崩溃后重启
如果服务器主机与客户端建立连接后崩溃,如果此时,客户端向服务器发送数据,而服务器已经崩溃不能响应客户端ACK,客户TCP将持续重传数据分节,试图从服务器上接收一个ACK,如果服务器一直崩溃客户端会发现服务器已经崩溃或目的地不可达,但可能需要比较长的时间; 如果服务器在客户端发现崩溃前重启,服务器的TCP丢失了崩溃前的所有连接信息,所以服务器TCP对接收的客户数据分节以RST响应。

二、关于socket的recv:

对于TCP non-blocking socket, recv返回值== -1,但是errno == EAGAIN, 此时表示在执行recv时相应的socket buffer中没有数据,应该继续recv。

【If no messages are available at the socket and O_NONBLOCK is not set on the socket's file descriptor, recv() shall block until a message arrives. If no messages are available at the socket and O_NONBLOCK is set on the socket's file descriptor, recv() shall fail and set errno to [EAGAIN] or [EWOULDBLOCK].】

对于UDP recv 应该一直读取直到recv()==-1 && errno==EAGAIN,表示buffer中数据包被全部读取。

 

 


    while(res != 0)
    {
        //len = recv(sockfd, buff, MAXBUF, 0);


        len =recv(sockfd, buff, 5, 0);
        if(len < 0 ){
            if(errno== EAGAIN){
                printf("RE-Len:%d errno EAGAIN\n", len);
                return 12;
            }

            if (errno == EINTR)

               continue;          
            perror("recv error\n");
            break;
        }elseif(len > 0){
            printf("Recved:%s, and len is:%d \n", buff, len);
            len =send(sockfd, buff, len, 0);/* if the client socket was closed and the socketfd here in kernel has set the status as RST this process will recv a SIGPIPE, and the process will exit if we don't handle it to SIGING */
            if(len < 0){
                perror("send error");
                return-1;
            }
            memset(buff, 0, MAXBUF);
            continue;
        }else{//==0

            printf("Disconnected by peer!\n");
            res = 0;
        }
    }



外记:
accetp()是慢系统调用,在信号产生时会中断其调用并将errno变量设置为EINTR,此时应重新调用accept()。
所以使用时应这样:


 while(1){
  if((connfd =accept(....))==-1 ){
  if(errno== EINTR)
  continue;
  perror("accept()");
  exit(1);
  }


  /* do sth with "connfd" */
  }



signal 与 sigaction 区别:
    signal函数每次设置具体的信号处理函数(非SIG_IGN)只能生效一次,每次在进程响应处理信号时,随即将信号处理函数恢复为默认处理方式.所以如果想多次相同方式处理某个信号,通常的做法是,在响应函数开始,再次调用signal设置。

int sig_int();//My signal handler

    ...
    signal(SIGINT, sig_int);
    ...

int sig_int()
{

   signal(SIGINT, sig_int);
    ....
}

这种代码段的一个问题是:在信号发生之后到信号处理程序中调用s i g n a l函数之间有一个
时间窗口。在此段时间中,可能发生另一次中断信号。第二个信号会造成执行默认动作,而对
中断信号则是终止该进程。这种类型的程序段在大多数情况下会正常工作,使得我们认为它们
正确,而实际上却并不是如此。
另一个问题是:在进程不希望某种信号发生时,它不能关闭该信号

sigaction:
1.在信号处理程序被调用时,系统建立的新信号屏蔽字会自动包括正被递送的信号。因此保证了在处理一个
给定的信号时,如果这种信号再次发生,那么它会被阻塞到对前一个信号的处理结束为止
2.响应函数设置后就一直有效,不会重置
3.对除S I G A L R M以外的所有信号都企图设置S A _ R E S TA RT标志,于是被这些信号中断
的系统调用(read,write)都能自动再起动。不希望再起动由S I G A L R M信号中断的系统调用的原因是希望对I / O操作可以设置时间限制。  所以希望能用相同方式处理信号的多次出现,最好用sigaction.信号只出现并处理一次,可以用signal
<think>我们正在解决一个套接字编程中的错误:“Writeendclosed:Socketoperationonnon-socketerror”根据引用[1]:关闭套接字后,所有后续操作都会失败。远程端将不会再收到数据(在队列数据被刷新后)。套接字在垃圾回收时会自动关闭。引用[2]展示了客户端创建套接字并连接的代码示例。引用[3]解释了listen函数将主动套接字转换为被动套接字(监听状态)。引用[4]则提供了处理SIGPIPE信号(管道破裂)和EPIPE错误的方法。用户遇到的错误信息包含两部分:1."Writeendclosed":表示写端已经关闭,这通常发生在尝试写入一个已经被关闭的套接字(或管道)时。2."Socketoperationonnon-socket":表示操作的对象不是一个有效的套接字描述符。结合引用,我们可以推断:-程序可能试图在一个已经关闭的套接字上执行写入操作(导致“Writeendclosed”)。-或者,在写入之前,套接字描述符已经被关闭,甚至可能已经被释放(导致描述符无效,从而出现“non-socket”错误)。以下分析原因和解决方案:###错误原因分析1.**套接字被提前关闭**:-在写入之前,套接字已经被调用`close()`关闭,但程序仍尝试写入。-可能由于多线程环境下,一个线程关闭了套接字,而另一个线程仍在尝试写入。2.**套接字描述符无效**:-套接字描述符被关闭后,其文件描述符可能被重用(例如用于打开其他文件或套接字),此时再对该描述符进行套接字操作就会出错。-或者,程序错误地使用了未初始化的描述符(例如未正确创建套接字)。3.**未处理SIGPIPE信号**:-默认情况下,向一个已关闭写端的套接字写入数据会触发SIGPIPE信号,该信号默认行为是终止进程。如果程序捕获了该信号但未正确处理,可能继续尝试写入,导致后续错误。-引用[4]提到,可以捕获SIGPIPE信号或者检查EPIPE错误。4.**套接字创建失败**:-在创建套接字时,如果失败(例如资源不足),则后续的connect、write等操作都会因为描述符无效而失败。###解决方案####1.确保套接字有效-在每次使用套接字描述符前,检查其有效性。但注意:在关闭后,描述符会变为无效,因此更重要的是在逻辑上保证关闭后不再使用。-检查套接字创建是否成功:```cintsockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd<0){perror("socketcreationfailed");exit(EXIT_FAILURE);}```####2.处理SIGPIPE信号(避免进程终止)-根据引用[4],我们可以设置忽略SIGPIPE信号或者捕获它:```c#include<signal.h>signal(SIGPIPE,SIG_IGN);//忽略SIGPIPE信号```或者使用更健壮的sigaction(如引用[4]中的示例)来捕获信号。-另一种方法是检查每次写入的返回值,如果返回`EPIPE`错误(即errno被设为EPIPE),则表示对端已经关闭,此时应停止写入:```cssize_tn=write(sockfd,buf,len);if(n==-1){if(errno==EPIPE){//对端已经关闭,关闭本端套接字并清理资源close(sockfd);sockfd=-1;//将描述符置为无效,避免再次使用}else{//其他错误处理}}```####3.管理套接字生命周期(避免使用已关闭的套接字)-在关闭套接字后,立即将描述符变量设置为无效值(如-1),并在使用前检查。-在多线程环境中,使用互斥锁保护套接字,确保关闭和写入操作不会同时发生。####4.检查连接状态-在写入前,可以通过`getpeername`或`getsockopt`等函数检查套接字是否仍然连接,但更常见的做法是直接写入并处理错误(因为即使检查时连接存在,写入时也可能断开)。-对于TCP套接字,如果对端已经关闭连接,那么本端写入会触发SIGPIPE或返回EPIPE错误。####5.示例代码修正-参考引用[2]中的客户端代码,在连接失败时退出,但写入时没有错误处理。我们应添加写入错误处理:```c//假设已经成功连接char*message="Hello,server";ssize_tbytes_sent=send(clientSock,message,strlen(message),0);if(bytes_sent<0){if(errno==EPIPE){fprintf(stderr,"Connectionclosedbypeer\n");}else{DieWithError("send()failed");}close(clientSock);clientSock=-1;}```###总结出现“Writeendclosed:Socketoperationonnon-socket”错误,通常是因为:-套接字已经被关闭,但程序仍尝试写入(导致写端关闭错误)。-套接字描述符无效(可能被关闭后重用,或从未成功创建)。解决方法:1.确保套接字创建成功。2.在套接字关闭后不再使用,并设置描述符为无效值(如-1)。3.处理SIGPIPE信号或检查EPIPE错误。4.在多线程环境中使用锁保护套接字操作。通过以上步骤,可以避免此类错误。§§相关问题§§1.如何在多线程程序中安全地关闭套接字?2.忽略SIGPIPE信号和捕获SIGPIPE信号哪种方式更好?3.除了EPIPE,还有哪些错误表示套接字连接已断开?</think>### 套接字编程中的 "Write end closed: Socket operation on non-socket" 错误分析与解决 这个错误表明程序尝试在**已关闭的套接字**或**无效的文件描述符**上进行写入操作。结合引用内容,以下是核心原因和解决方案: --- #### 一、错误原因分析 1. **套接字已关闭但仍在操作** [^1] - 当套接字被`close()`后,所有后续操作都会失败 - 常见于多线程环境中:一个线程关闭套接字,另一个线程仍在尝试写入 2. **无效文件描述符** [^4] - 尝试操作未初始化的套接字描述符 - 套接字创建失败(如`socket()`返回负值)但未检查错误 - 文件描述符被意外重用(如关闭后未置为-1) 3. **未处理管道破裂(SIGPIPE)** [^4] - 向已关闭的套接字写入会触发`SIGPIPE`信号 - 默认行为是终止进程,导致"non-socket"错误 4. **连接状态管理不当** [^3] - 混淆主动套接字(connect)和被动套接字(listen) - 尝试在监听套接字上执行写入操作 --- #### 二、解决方案指南 ##### 1. 检查套接字状态(基础防御) ```c // 创建套接字后必须检查有效性 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket creation failed"); exit(EXIT_FAILURE); } // 执行操作前验证描述符 if (fcntl(sockfd, F_GETFD) == -1 && errno == EBADF) { perror("Invalid socket descriptor"); return; } ``` ##### 2. 正确处理SIGPIPE信号(关键)[^4] ```c #include <signal.h> // 方法1:忽略SIGPIPE信号 signal(SIGPIPE, SIG_IGN); // 方法2:捕获并处理SIGPIPE void sigpipe_handler(int signum) { printf("Caught SIGPIPE, closing socket\n"); // 清理资源 } int main() { struct sigaction sa; sa.sa_handler = sigpipe_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGPIPE, &sa, NULL); // ...套接字操作代码... } ``` ##### 3. 安全写入模式(带错误检测) ```c ssize_t write_data(int fd, const void* buf, size_t len) { ssize_t bytes_sent = send(fd, buf, len, MSG_NOSIGNAL); // 避免触发SIGPIPE if (bytes_sent == -1) { if (errno == EPIPE) { // 对端已关闭 printf("Write end closed\n"); close(fd); return -1; } else if (errno == EBADF) { // 无效描述符 perror("Operation on non-socket"); return -1; } } return bytes_sent; } ``` ##### 4. 连接生命周期管理 - **主动套接字** (客户端)[^2]: ```c connect(sockfd, ...); // 成功后才能读写 ``` - **被动套接字** (服务端)[^3]: ```c listen(sockfd, 5); // 只能接受连接,不能直接读写 int client_fd = accept(sockfd, ...); // 返回的新套接字用于通信 ``` ##### 5. 多线程安全实践 ```c // 全局套接字描述符 volatile int active_socket = -1; // 关闭套接字的线程安全函数 void safe_close(int* sock) { pthread_mutex_lock(&sock_mutex); if (*sock != -1) { shutdown(*sock, SHUT_RDWR); close(*sock); *sock = -1; // 标记为无效 } pthread_mutex_unlock(&sock_mutex); } ``` --- #### 三、预防措施 1. **描述符生命周期规则**: - 关闭后立即置为-1 - 使用前检查`fcntl(fd, F_GETFL) != -1` 2. **错误处理三原则**: ```c if (operation() == -1) { // 1.检查返回值 perror("reason"); // 2.打印错误 handle_error(); // 3.清理资源 } ``` 3. **协议设计建议**: - 添加心跳机制检测连接状态 - 使用应用层ACK确认机制 > **调试提示**:使用`strace -e trace=network`追踪套接字系统调用,观察`close()`和`write()`的执行顺序[^1][^4]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值