Linux编程入门五信号

服务器程序通常需要处理三类事件:I/O事件、信号及定时事件。信号是由用户、系统或进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。服务器程序必须处理(至少忽略)一些常见的信号,以免异常终止。Linux信号可由如下条件产生:

  • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C通常会给进程发送一个中断信号。
  • 系统异常。比如浮点异常和非法内存段访问。
  • 系统状态变化。比如alarm定时器到期将引起SIGALRAM信号。
  • 运行kill命令或调用kill函数。

Linux的可用信号都定义在bits/signum.h头文件中,其中包括标准信号和POSIX实时信号。
Linux编程入门五信号
Linux编程入门五信号

发送信号

在Linux下,一个进程给其他进程发送信号的API是kill函数。其定义如下:

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

该函数把信号sig发送目标进程;目标进程由pid参数指定,其可能的取值及含义:
Linux编程入门五信号
Linux定义的信号值都大于0,如果sig取值为0,则kill函数不发送任何信号。但将sig设置为0可以用来检测目标进程或进程组是否存在,因为检查工作总是在信号发送之前就执行。不过,这种检测方式是不可靠的。一方面由于进程PID的回绕,可能导致被检测的PID不是我们期望的进程的PID;另一方面,这种检测方法不是原子操作。
该函数成功时返回0,失败则返回-1并设置errno。
Linux编程入门五信号

信号处理方式

目标进程在收到信号时,需要定义一个接收函数处理之。信号处理函数的原型如下:

#include <signal.h>
typedef void (*__sighandler_t)(int); 

信号处理函数只带有一个整型参数,该参数用来指示信号类型。信号处理函数应该是可重入的,否则很容易引起一些竞态条件。所以在信号处理函数中严禁调用一些不安全的函数。
除了用户自定义信号处理函数外,bits/signum.h头文件中还定义了信号的两种其他处理方式:

#include <bits/signum.h>
#define SIG_DFL ((__sighandler_t)0)
#define SIG_IGN ((__sighandler_t)1)

SIG_IGN表示忽略目标信号,SIG_DFL表示使用信号的默认处理方式。
信号的默认处理方式有如下几种:结束进程(Term)、忽略信号(Ign)、结束进程并生成核心转储文件(Core)、暂停进程(Stop),以及继续进程(Cont)。

如果程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且errno被设置为EINTR。可以使用sigaction函数为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用。对于默认行为是暂停进程的信号(比如SIGSTOP、SIGTTIN),如果我们没有为它们设置信号处理函数,则它们也可以中断某些系统调用(比如connect、epoll_wait)。POSIX没有规定这种行为,这是Linux独有的。

要为一个信号设置处理函数,可以使用下面的signal系统调用:

#include <signal.h>
_sighandler_t signal(int sig, _sighandler_t _handler);

sig参数指出要捕获的信号类型。_handler参数是_sighandler_t类型的函数指针,用于指定信号sig的处理函数。signal函数成功时返回一个函数指针,该函数指针的类型也是_sighandler_t。这个返回值是前一次调用signal函数时传入的函数指针,或者是信号sig对应的默认处理函数指针SIG_DEF(如果第一次调用signal的话)。signal系统调用出错时返回SIG_ERR,并设置errno。

设置信号处理函数的更健壮的接口:

#include <signal.h>
int sigaction(int sig, const struct sigaction* act, struct sigaction* oact);

sig参数指出要捕获的信号类型。act参数指定新的信号处理方式,oact参数则输出信号先前的处理方式(如果不为NULL的话)。act和oact都是sigaction结构体类型的指针,sigaction结构体描述了信号处理的细节。

struct sigaction
{
#ifdef __USE_POSIX199309
	union
	{
		_sighandler_t sa_handler;
		void (*sa_sigaction)(int, siginfo_t*, void*);
	}_sigaction_handler;
#define sa_handler __sigaction_handler.sa_handler
#define da_sigaction __sigaction_handler.sa_sigaction
#else
	_sighandler_t sa_handler;
#endif	

	_sigset_t sa_mask;
	int sa_flags;
	void (*sa_restorer)(void);
}

该结构体中的sa_handler成员指定信号处理函数。sa_mask成员设置进程的信号掩码(确切地说是在进程原有信号掩码的基础上增加信号掩码),以指定哪些信号不能发送给本进程。sa_mask是信号集sigset_t(_sigset_t的同义词)类型,该类型指定一组信号。sa_flags成员用于设置程序收到信号时的行为。
Linux编程入门五信号

信号集

数据结构sigset_t来表示一组信号

#include <bits/sigset.h>
#define _SIGSET_NWORDS (1024/(8*sizeof(unsigned long int)))
typedef struct
{
	unsigned long int __val[_SIGSET_NWORDS];
}__sigset_t;

由定义可见,sigset_t实际上是一个长整型数组,数组的每个元素的每个位表示一个信号。这种定义方式和文件描述符fd_set类似。
Linux编程入门五信号

可以利用sigaction结构体的sa_mask成员来设置进程的信号掩码。此外,下面函数也可用于设置或查看进程的信号掩码:

#include <signal.h>
int sigprocmask(int _how, _const sigset_t* _set, sigset_t* _oset);

_set参数指定新的信号掩码,_oset参数则输出原来的信号掩码(如果不为NULL的话)。如果_set参数不为NULL,则_how参数指定设置进程信号掩码的方式
Linux编程入门五信号
如果_set为NULL,则进程信号掩码不变,此时我们仍然可以利用_oset参数来获得进程当前的信号掩码。

获得被挂起的信号
设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。如下函数可以获得进程当前被挂起的信号集:

#include <signal.h>
int sigpending(sigset_t* set);

set参数用于保存被挂起的信号集。显然,进程即使多次接收到同一被挂起的信号,sigpending函数也只能反映一次。并且,当我们再次使用sigprocmask使能该挂起的信号时,该程信号的处理函数也只被触发一次。

网络编程相关信号

SIGHUP
当挂起进程的控制终端时,SIGHUP信号将被触发。对于没有控制终端的网络后台程序,它们利用SIGHUP信号来强制服务器重读配置文件。
一个典型的例子是xinetd超级服务程序。xinetd程序在接收到SIGHUP信号之后将调用hard_reconfig函数,它循环读取/etc/xinetd.d/目录下的每个子配置文件,并检测其变化。如果某个正在运行的子服务的配置文件被修改以停止服务,则xinetd主进程将给该子服务进程发送SIGTERM信号以结束它。如果某个子服务的配置文件被修改以开启服务,则xinetd将创建新的socket并将其绑定到该服务对应的端口上。
SIGPIPE
默认情况下,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号。我们需要在代码中捕获并处理该信号,或者至少忽略它,因为程序接收到SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为错误的写操作而导致程序退出。引起SIGPIPE信号的写操作将设置errno为EPIPE。我们可以使用send函数的MSG_NOSIGNAL标志来禁止写操作触发SIGPIPE信号。在这种情况下,我们应该使用send函数反馈的errno值来判断管道或者socket连接的读端是否已经关闭。此外,我们也可以利用I/O复用系统调用来检测管道和socket连接的读端是否已经关闭。以poll为例,当管道的读端关闭时,写端文件描述符上的POLLHUP事件将被触发;当socket连接被对方关闭时,socket上的POLLRDHUP事件将被触发。
SIGURG
https://blog.csdn.net/liushengxi_root/article/details/82563181
https://www.cnblogs.com/c-slmax/p/5553857.html
在Linux环境下,内核通知应用程序带外数据到达主要有两种方法:一种是I/O复用产生的异常事件,select等系统调用在接收到带外数据时将返回并向应用程序报告socket上的异常事件。另一种方法就是使用SIGURG信号。

  1 #include <sys/socket.h>
  2 #include <netinet/in.h>
  3 #include <arpa/inet.h>
  4 #include <assert.h>
  5 #include <stdio.h>
  6 #include <unistd.h>
  7 #include <stdlib.h>
  8 #include <errno.h>
  9 #include <string.h>
 10 #include <signal.h>
 11 #include <fcntl.h>
 12 #define BUF_SIZE 1024
 13 static int connfd;
 14 //SIGURG信号的处理函数
 15 void sig_urg(int sig)
 16 {
 17         int save_errno = errno;
 18         char buffer[BUF_SIZE];
 19         memset(buffer,'\0',BUF_SIZE);
 20         int ret = recv(connfd,buffer,BUF_SIZE-1,MSG_OOB);//接收带外数据
 21         printf("get %d bytes of oob data '%s'\n",ret,buffer);
 22         errno = save_errno;
 23 }
 24 void addsig(int sig, void (*sig_handler)(int))
 25 {
 26         struct sigaction sa;
 27         memset(&sa,'\0',sizeof(sa));
 28         sa.sa_handler = sig_handler;
 29         sa.sa_flags |= SA_RESTART;
 30         sigfillset(&sa.sa_mask);
 31         assert(sigaction(sig,&sa,NULL)!=-1);
 32 }
 33 int main(int argc, char* argv[])
 34 {
 35         if( argc <= 2 )
 36         {
 37                 printf("usage: %s ip_address port_number\n", basename(argv[0]));
 38                 return 1;
 39         }
 40         const char* ip = argv[1];
 41         int port = atoi(argv[2]);
 42         struct sockaddr_in address;
 43         bzero(&address,sizeof(address));
 44         address.sin_family = AF_INET;
 45         inet_pton(AF_INET,ip,&address.sin_addr);
 46         address.sin_port = htons(port);
 47         int sock = socket(PF_INET,SOCK_STREAM,0);
 48         assert(sock>=0);
 49         int ret = bind(sock,(struct sockaddr*)&address,sizeof(address));
 50         assert(ret!=-1);
 51         ret = listen(sock,5);
 52         assert(ret!=-1);
 53 
 54         struct sockaddr_in client;
 55         socklen_t client_addrlength = sizeof(client);
 56         printf("accept function\n");
 57         connfd = accept(sock,(struct sockaddr*)&client,&client_addrlength);
 58         printf("accept function\n");
 59         if( connfd < 0 )
 60         {
 61                 printf("errno is: %d\n", errno);
 62         }else{
 63                 addsig(SIGURG,sig_urg);
 64                 //在使用SIGURG信号之前,我们必须设置socket的宿主进程或进程组
 65                 fcntl(connfd,F_SETOWN,getpid());
 66 
 67                 char buffer[BUF_SIZE];
 68                 while(1) //循环接收普通数据
 69                 {
 70                         memset(buffer,'\0',BUF_SIZE);
 71                         ret = recv(connfd,buffer,BUF_SIZE-1,0);
 72                         if( ret <= 0 )
 73                         {
 74                                 break;
 75                         }
 76                         printf("got %d bytes of normal data '%s'\n",ret,buffer);
 77                 }
 78                 close(connfd);
 79         }
 80         close(sock);
 81         return 0;
 82 }

统一事件源

信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。很显然,信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽(前面提到过,为了避免一些竞态条件,信号在处理期间,系统不会再次触发它)太久。

把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时,它只是简单地通知主循环程序接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。信号处理函数通常使用管道来将信号“传递”给主循环:信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。那么主循环怎么知道管道上何时有数据可读呢?只需要使用I/O复用系统调用来监听管道的读端文件描述符上的可读事件。如此一来,信号事件就能和其他I/O事件一样被处理,即统一事件源。

  1 #include <sys/types.h>
  2 #include <sys/socket.h>
  3 #include <netinet/in.h>
  4 #include <arpa/inet.h>
  5 #include <assert.h>
  6 #include <stdio.h>
  7 #include <signal.h>
  8 #include <unistd.h>
  9 #include <errno.h>
 10 #include <string.h>
 11 #include <fcntl.h>
 12 #include <stdlib.h>
 13 #include <sys/epoll.h>
 14 #include <pthread.h>
 15 #define MAX_EVENT_NUMBER 1024
 16 static int pipefd[2];
 17 int setnonblocking(int fd)
 18 {
 19         int old_option = fcntl(fd,F_GETFL);
 20         int new_option = old_option|O_NONBLOCK;
 21         fcntl(fd,F_SETFL,new_option);
 22         return old_option;
 23 }
 24 void addfd(int epollfd,int fd)
 25 {
 26         epoll_event event;
 27         event.data.fd = fd;
 28         event.events = EPOLLIN|EPOLLET;
 29         epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
 30         setnonblocking(fd);
 31 }
 32 //信号处理函数
 33 void sig_handler(int sig)
 34 {
 35         //保留原来的errno,在函数最后恢复,以保证函数的可重入性
 36         int save_errno = errno;
 37         int msg =sig;
 38         send(pipefd[1],(char*)&msg,1,0);//将信号值写入管道
 39         errno = save_errno;
 40 }
 41 //设置信号的处理函数
 42 void addsig(int sig)
 43 {
 44         struct sigaction sa;
 45         memset(&sa,'\0',sizeof(sa));
 46         sa.sa_handler = sig_handler;
 47         sa.sa_flags |= SA_RESTART;
 48         sigfillset(&sa.sa_mask);
 49         assert(sigaction(sig,&sa,NULL)!=-1);
 50 }
 51 int main(int argc, char* argv[])
 52 {
 53         if(argc<=2)
 54         {
 55                 printf("usage: %s ip_address port_number\n", basename(argv[0]));
 56                 return 1;
 57         }
 58         const char* ip = argv[1];
 59         int port = atoi(argv[2]);
 60         int ret = 0;
 61         struct sockaddr_in address;
 62         bzero(&address,sizeof(address));
 63         address.sin_family = AF_INET;
 64         inet_pton(AF_INET,ip,&address.sin_addr);
 65         address.sin_port = htons(port);
 66         int listenfd = socket(PF_INET,SOCK_STREAM,0);
 67         assert(listenfd>=0);
 68         ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address));
 69         if(ret==-1)
 70         {
 71                 printf("errno is %d\n",errno);
 72                 return 1;
 73         }
 74         ret = listen(listenfd,5);
 75         assert(ret!=-1);
 76 
 77         epoll_event events[MAX_EVENT_NUMBER];
 78         int epollfd = epoll_create(5);
 79         assert(epollfd!=-1);
 80         addfd(epollfd,listenfd);
 81 
 82         //使用socketpair创建管道,注册pipefd[0]上的可读事件
 83         ret = socketpair(PF_UNIX,SOCK_STREAM,0,pipefd);
 84         assert(ret!=-1);
 85         setnonblocking(pipefd[1]);
 86         addfd(epollfd,pipefd[0]);
 87         //设置一些信号的处理函数
 88         addsig(SIGHUP);
 89         addsig(SIGCHLD);
 90         addsig(SIGTERM);
 91         addsig(SIGINT);
 92         bool stop_server = false;
 93 
 94         while(!stop_server)
 95         {
 96                 int number = epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);
 97                 if((number<0)&&(errno!=EINTR))
 98                 {
 99                         printf("epoll failure\n");
100                         break;
101                 }
102                 for(int i = 0; i < number; i++)
103                 {
104                         int sockfd = events[i].data.fd;
105                         if(sockfd == listenfd)
106                         {
107                                 struct sockaddr_in client_address;
108                                 socklen_t client_addrlength = sizeof(client_address);
109                                 int connfd = accept(listenfd,(struct sockaddr*)&client_address    ,&client_addrlength);
110                                 addfd(epollfd,connfd);
111                         }else if((sockfd == pipefd[0])&&(events[i].events & EPOLLIN)){
112                                 //如果就绪的文件描述符是pipefd[0],则处理信号
113                                 int sig;
114                                 char signals[1024];
115                                 ret = recv(pipefd[0],signals,sizeof(signals),0);
116                                 if(ret == -1)
117                                 {
118                                         continue;
119                                 }else if(ret == 0)
120                                 {
121                                         continue;
122                                 }else{
123                                         //因为每个信号值占1字节,所以按字节来逐个接收信号
124                                         //以SIGTERM为例,来说明如何安全地终止服务器主循环
125                                         for(int i = 0; i < ret; i++)
126                                         {
127                                                 switch(signals[i])
128                                                 {
129                                                         case SIGCHLD:
130                                                         case SIGHUP:
131                                                         { continue; }
132                                                         case SIGTERM:
133                                                         case SIGINT:
134                                                         { stop_server = true; }
135                                                 }
136                                         }
137                                 }
138                         }else{
139 
140                         }
141                 }
142         }
143         printf("close fds\n");
144         close(listenfd);
145         close(pipefd[1]);
146         close(pipefd[0]);
147         return 0;
148 }
149 

Linux编程入门五信号

参考:
Linux高性能服务器编程

上一篇:Linux Kernel信号处理机制源码分析


下一篇:linux 多线程之间信号传递