对于信号的介绍,我再前面的一篇博客中做过专门的总结,感兴趣的可以看看。本文主要介绍在网络编程中几个密切相关的函数:SIGUP,SIGPIPE,SIGURG。
SIGHUP信号
在介绍SIGHUP信号之前,先来了解两个概念:进程组和会话。
进程组
进程组就是一系列相互关联的进程集合,系统中的每一个进程也必须从属于某一个进程组;每个进程组中都会有一个唯一的 ID(process group id),简称 PGID;PGID 一般等同于进程组的创建进程的 Process ID,而这个进进程一般也会被称为进程组先导(process group leader),同一进程组中除了进程组先导外的其他进程都是其子进程;
进程组的存在,方便了系统对多个相关进程执行某些统一的操作,例如,我们可以一次性发送一个信号量给同一进程组中的所有进程。
会话
会话(session)是一个若干进程组的集合,同样的,系统中每一个进程组也都必须从属于某一个会话;一个会话只拥有最多一个控制终端(也可以没有),该终端为会话中所有进程组中的进程所共用。一个会话中前台进程组只会有一个,只有其中的进程才可以和控制终端进行交互;除了前台进程组外的进程组,都是后台进程组;和进程组先导类似,会话中也有会话先导(session leader)的概念,用来表示建立起到控制终端连接的进程。在拥有控制终端的会话中,session leader 也被称为控制进程(controlling process),一般来说控制进程也就是登入系统的 shell 进程(login shell);
执行睡眠后台进程sleep 50 & 之后,通过 ps命令查看该进程及shell信息如上图:
PPID 指父进程 id;PID 指进程 id;PGID 指进程组 id
SID 指会话 id;TTY 指会话的控制终端设备;COMMAND 指进程所执行的命令
TPGID 指前台进程组的 PGID。
SIGHUP信号的触发及默认处理
在对会话的概念有所了解之后,我们现在开始正式介绍一下SIGHUP信号,SIGHUP 信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联. 系统对SIGHUP信号的默认处理是终止收到该信号的进程。所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出。
SIGHUP会在以下3种情况下被发送给相应的进程:
1、终端关闭时,该信号被发送到session首进程以及作为job提交的进程(即用 & 符号提交的进程);
2、session首进程退出时,该信号被发送到该session中的前台进程组中的每一个进程;
3、若父进程退出导致进程组成为孤儿进程组,且该进程组中有进程处于停止状态(收到SIGSTOP或SIGTSTP信号),该信号会被发送到该进程组中的每一个进程。
例如:在我们登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。
此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。 比如xinetd超级服务程序。
当xinetd程序在接收到SIGHUP信号之后调用hard_reconfig函数,它将循环读取/etc/xinetd.d/目录下的每个子配置文件,并检测其变化。如果某个正在运行的子服务的配置文件被修改以停止服务,则xinetd主进程讲给该子服务进程发送SIGTERM信号来结束它。如果某个子服务的配置文件被修改以开启服务,则xinetd将创建新的socket并将其绑定到该服务对应的端口上。
SIGPIPE信号
在网络编程中,SIGPIPE这个信号是很常见的。当往一个写端关闭的管道或socket连接中连续写入数据时会引发SIGPIPE信号,引发SIGPIPE信号的写操作将设置errno为EPIPE。在TCP通信中,当通信的双方中的一方close一个连接时,若另一方接着发数据,根据TCP协议的规定,会收到一个RST响应报文,若再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不能再写入数据。
测试程序如下:简单的测试程序,函数未加错误判断
server.c:
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <signal.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);
}
// 写入第二条消息,触发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.c
#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);
//socket()
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket");
exit(-1);
}
//connect()
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;
}
运行结果
此外,因为SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为写操作的错误而导致程序退出,尤其是作为服务器程序来说就更恶劣了。所以我们应该对这种信号加以处理,在这里,介绍两种处理SIGPIPE信号的方式:
1 、给SIGPIPE设置SIG_IGN信号处理函数,忽略该信号:
signal(SIGPIPE, SIG_IGN);
前文说过,引发SIGPIPE信号的写操作将设置errno为EPIPE,。所以,第二次往关闭的socket中写入数据时, 会返回-1, 同时errno置为EPIPE. 这样,便能知道对端已经关闭,然后进行相应处理,而不会导致整个进程退出.
2、使用send函数的MSG_NOSIGNAL 标志来禁止写操作触发SIGPIPE信号。
send(sockfd , buf , size , MSG_NOSIGNAL);
同样,我们可以根据send函数反馈的errno来判断socket的读端是否已经关闭。
此外,我们也可以通过IO复用函数来检测管道和socket连接的读端是否已经关闭。以POLL为例,当socket连接被对方关闭时,socket上的POLLRDHUP事件将被触发。
SIGURG信号
在介绍SIGURG信号之前,先来说说什么是带外数据。
带外数据
不论发送缓冲区中是否有排队等待发送的数据,它总是被立即发送。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。实际应用中,带外数据是使用很少见,有,telnet和ftp等远程非活跃程序。
UDP没有没有实现带外数据传输,TCP也没有真正的带外数据。不过TCP利用头部的紧急指针标志和紧急指针,为应用程序提供了一种紧急方式,含义和带外数据类似。TCP的紧急方式利用传输普通数据的连接来传输紧急数据。
SIGURG信号的作用
内核通知应用程序带外数据到达的方式有两种:
一种就是利用IO复用技术的系统调用(如select)在接受到带外数据时将返回,并向应用程序报告socket上的异常事件。
另一种方法就是使用SIGURG信号。
若对服务器同时处理普通数据和带外数据感兴趣的话可以参考示例程序。
/*************************************************************************
> File Name: client.c
> Author:
> Mail:
> Created Time: 2018年06月30日 星期六 15时12分52秒
************************************************************************/
//发送带外数据
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
struct sockaddr_in server_address;
bzero( &server_address, sizeof( server_address ) );
server_address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &server_address.sin_addr );
server_address.sin_port = htons( port );
int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( sockfd >= 0 );
if ( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( server_address ) ) < 0 )
{
printf( "connection failed\n" );
}
else
{
printf( "send oob data out\n" );
const char* oob_data = "abc";
const char* normal_data = "123";
send( sockfd, normal_data, strlen( normal_data ), 0 );
send( sockfd, oob_data, strlen( oob_data ), MSG_OOB );
send( sockfd, normal_data, strlen( normal_data ), 0 );
}
close( sockfd );
return 0;
}
/*************************************************************************
> File Name: server.c
> Author:
> Mail:
> Created Time: 2018年06月30日 星期六 13时11分14秒
************************************************************************/
//使用SIGURG检测带外数据是否到达
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#define BUF_SIZE 1024
#define port 8888
static int connfd;
void sig_urg( int sig )
{
int save_errno = errno;
char buffer[ BUF_SIZE ];
memset( buffer, '\0', BUF_SIZE );
int ret = recv( connfd, buffer, BUF_SIZE-1, MSG_OOB );
printf( "got %d bytes of oob data '%s'\n", ret, buffer );
errno = save_errno;
}
void addsig( int sig, void ( *sig_handler )( int ) )
{
struct sigaction sa;
memset( &sa, '\0', sizeof( sa ) );
sa.sa_handler = sig_handler;
sa.sa_flags |= SA_RESTART;
sigfillset( &sa.sa_mask );
assert( sigaction( sig, &sa, NULL ) != -1 );
}
int main( int argc, char* argv[] )
{
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons( port );
int sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( sock, 5 );
assert( ret != -1 );
struct sockaddr_in client;
socklen_t client_addrlength = sizeof( client );
connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
}
else
{
addsig( SIGURG, sig_urg );
//使用SIGURG信号之前,必须设置socket的宿主进程或进程组
fcntl( connfd, F_SETOWN, getpid() );
char buffer[ BUF_SIZE ];
while( 1 ) //循环读取普通数据
{
memset( buffer, '\0', BUF_SIZE );
ret = recv( connfd, buffer, BUF_SIZE-1, 0 );
if( ret <= 0 )
{
break;
}
printf( "got %d bytes of normal data '%s'\n", ret, buffer );
}
close( connfd );
}
close( sock );
return 0;
}