信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行线路。很显然,信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽太久。一种典型的解决方案是:把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时,它只是简单地通过通知主程序接受到的信号,并把信号传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。信号处理函数通常使用管道来将信号“传递”给主循环:信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。那么主循环怎么知道管道上何时有数据可读呢? 这很简单,我们只需要使用I/O复用系统调用来监听管道的读端文件描述符上的可读事件。如此一来,信号事件就能和其它I/O事件一样被处理,既统一事件源
很多优秀的I/O框架库和后台服务器程序都统一处理信号和I/O事件,比如Libevent I/O框架库和xinetd超级服务。
#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<assert.h> #include<signal.h> #include<unistd.h> #include<errno.h> #include<string.h> #include<fcntl.h> #include<stdlib.h> #include<sys/epoll.h> #include<pthread.h> #define MAX_EVENT_NUMBER 1024 static int pipefd[2]; int setnonblocking(int fd){ int old_option = fcntl(fd , F_GETFL); int new_option = old_option | O_NONBLOCK; fcntl(fd , F_SETFL , new_option); return old_option; } int addfd(int epfd , int fd){ struct epoll_event event; event.events = EPOLLIN | EPOLLET; event.data.fd = fd; epoll_ctl(epfd , EPOLL_CTL_ADD , fd , &event); setnonblocking(fd); } void sig_handler(int sig){ int save_errno = errno; int msg = sig; send(pipefd[1] , (char*)&msg , 1 , 0); errno = save_errno; } void addsig(int sig){ 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[]){ if(argc <= 2){ printf("usage: %s ip_address port \n" , argv[0]); return -1; } const char* ip = argv[1]; short port = atoi(argv[2]); int ret = 0; struct sockaddr_in address; memset(&address , 0 , sizeof(address)); address.sin_family = AF_INET; inet_pton(AF_INET , ip , &address.sin_addr); address.sin_port = htons(port); int listenfd = socket(PF_INET , SOCK_STREAM , 0); assert(listenfd >= 0); ret = bind(listenfd , (struct sockaddr*)&address , sizeof(address)); if(ret == -1){ perror("bind"); return -1; } ret = listen(listenfd , 5); assert(ret != -1); struct epoll_event events[MAX_EVENT_NUMBER]; int epfd = epoll_create(1); assert(epfd != -1); addfd(epfd , listenfd); ret = socketpair(PF_UNIX , SOCK_STREAM , 0 , pipefd); assert(ret != -1); setnonblocking(pipefd[1]); addfd(epfd , pipefd[0]); addsig(SIGHUP); addsig(SIGCHLD); addsig(SIGTERM); addsig(SIGINT); int stop_server = 0; while(!stop_server){ int number = epoll_wait(epfd , events , MAX_EVENT_NUMBER , -1); if( (number < 0) && ( errno != EINTR) ){ printf("epoll failed\n"); break; } for(int i = 0 ; i < number ; i++){ int sockfd = events[i].data.fd; if(sockfd == listenfd){ struct sockaddr_in client_address; socklen_t client_addrlength = sizeof(client_address); int connfd = accept(listenfd , (struct sockaddr*)&client_address , &client_addrlength); addfd(epfd , connfd); } else if ( (sockfd == pipefd[0]) && events[i].events & EPOLLIN){ int sig; char signals[1024]; ret = recv(pipefd[0] , signals , sizeof(signals) , 0); if( ret == -1){ continue; }else if(ret == 0){ continue; } else{ for(int i = 0 ; i < ret ; ++i){ switch(signals[i]){ case SIGCHLD: case SIGHUP: { continue; } case SIGTERM: case SIGINT: { stop_server = 1; } } } } }else{ } } } printf("close fds\n"); close(listenfd); close(pipefd[1]); close(pipefd[0]); return 0; }
为什么管道写端要非阻塞?
send是将信息发送给套接字缓冲区,如果缓冲区满了,则会阻塞,这时候会进一步增加信号处理函数的执行时间,为此,将其修改为非阻塞。
没有对非阻塞返回值处理,如果阻塞是不是意味着这一次定时事件失效了?
是的,但定时事件是非必须立即处理的事件,可以允许这样的情况发生。
总结:
Linux下的信号采用的异步处理机制,信号处理函数和当前进程是两条不同的执行路线。具体的,当进程收到信号时,操作系统会中断进程当前的正常流程,转而进入信号处理函数执行操作,完成后再返回中断的地方继续执行。
为避免信号竞态现象发生,信号处理期间系统不会再次触发它。所以,为确保该信号不被屏蔽太久,信号处理函数需要尽可能快地执行完毕。
一般的信号处理函数需要处理该信号对应的逻辑,当该逻辑比较复杂时,信号处理函数执行时间过长,会导致信号屏蔽太久。
这里的解决方案是,信号处理函数仅仅发送信号通知程序主循环,将信号对应的处理逻辑放在程序主循环中,由主循环执行信号对应的逻辑代码。