一、SIGPIPE
当往一个写端关闭的管道或socket连接中连续写入数据时会引发SIGPIPE信号,引发SIGPIPE信号的写操作将设置errno为EPIPE。在TCP通信中,当通信的双方中的一方close一个连接时,若另一方接着发数据,根据TCP协议的规定,会收到一个RST响应报文,若再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不能再写入数据。
实例程序
- server端
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#define port 8888
void handle(int sig)
{
printf("SIGPIPE : %d\n",sig);
}
void mysendmsg(int fd)
{
// 写入第一条消息
char* msg1 = "first msg";
int n = write(fd, msg1, strlen(msg1));
if(n > 0) //成功写入第一条消息,server 接收到 client 发送的 RST
{
printf("success write %d bytes\n", n);
}
sleep(1);
// 写入第二条消息,触发SIGPIPE
char* msg2 = "second msg";
n = write(fd, msg2, strlen(msg2));
if(n < 0)
{
printf("write error: %s\n", strerror(errno));
}
}
int main()
{
signal(SIGPIPE , handle); //注册信号捕捉函数
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);
int listenfd = socket(AF_INET , SOCK_STREAM , 0);
bind(listenfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
listen(listenfd, 128);
int fd = accept(listenfd, NULL, NULL);
if(fd < 0)
{
perror("accept");
exit(1);
}
mysendmsg(fd);
return 0;
}
- client端
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>
#include<arpa/inet.h>
#include<unistd.h>
#define PORT 8888
#define MAX 1024
int main()
{
char buf[MAX] = {'0'};
int sockfd;
int n;
socklen_t slen;
slen = sizeof(struct sockaddr);
struct sockaddr_in seraddr;
bzero(&seraddr,sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(PORT);
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket");
exit(-1);
}
if(connect(sockfd,(struct sockaddr *)&seraddr,slen) == -1)
{
perror("connect");
exit(-1);
}
int ret = shutdown(sockfd , SHUT_RDWR);
if(ret < 0)
{
perror("shutdown perror");
}
return 0;
}
- server端结果
可以看到再第二次write时由于连接已关闭,所以收到了SIGPIPE 信号。
success write 9 bytes
SIGPIPE : 13
write error: Broken pipe
如何处理
因为SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为写操作的错误而导致程序退出,尤其是作为服务器程序来说就更恶劣了。所以我们应该对这种信号加以处理,在这里,介绍两种处理SIGPIPE信号的方式:
1. 给SIGPIPE设置SIG_IGN信号处理函数,忽略该信号:
signal(SIGPIPE, SIG_IGN);
前文说过,引发SIGPIPE信号的写操作将设置errno为EPIPE,。所以,第二次往关闭的socket中写入数据时, 会返回-1, 同时errno置为EPIPE. 这样,便能知道对端已经关闭,然后进行相应处理,而不会导致整个进程退出.
- 使用send函数的MSG_NOSIGNAL 标志来禁止写操作触发SIGPIPE信号。
send(sockfd , buf , size , MSG_NOSIGNAL);
我们可以根据send函数反馈的errno来判断socket的读端是否已经; 此外,我们也可以通过IO复用函数来检测管道和socket连接的读端是否已经关闭。以POLL为例,当socket连接被对方关闭时,socket上的POLLRDHUP事件将被触发。
二、SIGHUP
UNIX中进程组织结构为 session(会话)包含一个前台进程组及一个或多个后台进程组,一个进程组包含多个进程。一个session可能会有一个session首进程,而一个session首进程可能会有一个控制终端。一个进程组可能会有一个进程组首进程。进程组首进程的进程ID与该进程组ID相等。这儿是可能会有,在一定情况之下是没有的。与终端交互的进程是前台进程,否则便是后台进程。
SIGHUP会在以下3种情况下被发送给相应的进程:
- 终端关闭时,该信号被发送到session首进程以及作为job提交的进程(即用 & 符号提交的进程)
- session首进程退出时,该信号被发送到该session中的前台进程组中的每一个进程
- 若父进程退出导致进程组成为孤儿进程组,且该进程组中有进程处于停止状态(收到SIGSTOP或SIGTSTP信号),该信号会被发送到该进程组中的每一个进程。
下面观察几种因终端关闭导致进程退出的情况,在这儿进程退出是因为收到了SIGHUP信号。login shell是session首进程。
首先写一个测试程序,代码如下:
#include <stdio.h>
#include<signal.h>
char **args;
void exithandle(int sig)
{
printf("%s : sighup received ",args[1]);
}
int main(int argc,char **argv)
{
args = argv;
signal(SIGHUP,exithandle);
pause();
return 0;
}
程序中捕捉SIGHUP信号后打印一条信息,pause()使程序暂停。
编译后的执行文件为sigtest
- 命 令:sigtest front > tt.txt
操 作:关闭终端
结 果:tt.txt文件的内容为front : sighup received
原 因: sigtest是前台进程,终端关闭后,根据上面提到的第1种情况,login shell作为session首进程,会收到SIGHUP信号然后退出。根据第2种情况,sigtest作为前台进程,会收到login shell发出的SIGHUP信号。 - 命 令:sigtest back > tt.txt &
操 作:关闭终端
结 果:tt.txt文件的内容为 back : sighup received
原 因: sigtest是提交的job,根据上面提到的第1种情况,sigtest会收到SIGHUP信号。 - 命 令:写一个shell,内容为[sigtest &],然后执行该shell
操 作:关闭终端
结 果:ps -ef | grep sigtest 会看到该进程还在,tt文件为空
原 因: 执行该shell时,sigtest作为job提交,然后该shell退出,致使sigtest变成了孤儿进程,不再是当前session的job了,因此sigtest即不是session首进程也不是job,不会收到SIGHUP。同时孤儿进程属于后台进程,因此login shell退出后不会发送SIGHUP给sigtest,因为它只将该信号发送给前台进程。第3条说过若进程组变成孤儿进程组的时候,若有进程处于停止状态,也会收到SIGHUP信号,但sigtest没有处于停止状态,所以不会收到SIGHUP信号。 - 命 令:nohup sigtest > tt
操 作:关闭终端
结 果:tt文件为空
原 因: nohup可以防止进程收到SIGHUP信号
至此,我们就清楚了何种情况下终端关闭后进程会退出,何种情况下不会退出。
要想终端关闭后进程不退出有以下几种方法,均为通过shell的方式:
- 编写shell,内容如下
trap "" SIGHUP #该句的作用是屏蔽SIGHUP信号,trap可以屏蔽很多信号
sigtest - nohup sigtest 可以直接在命令行执行,
若想做完该操作后继续别的操作,可以 nohup sigtest & - 编写shell,内容如下
sigtest &
其实任何将进程变为孤儿进程的方式都可以,包括fork后父进程马上退出。
三、SIGINT && SIGTERM && SIGKILL
信号 | 产生方式 | 对进程的影响 |
---|---|---|
SIGINT | ctrl+c | 信号被当前进程树接收到,也就是说,不仅当前进程会收到信号,它的子进程也会收到 |
SIGTERM | kill {pid} | 只有当前进程收到信号,子进程不会收到。如果当前进程被kill了,那么它的子进程的父进程将会是init,也就是pid为1的进程 |
SIGKILL | kill -9 {pid} | 与SIGTERM 不同的是,此信号不会被阻塞 |