目录
1 信号的本质
软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。
收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:
- 类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。
- 忽略某个信号,对该信号不做任何处理,就象未发生过一样。
- 对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。
2 信号列表
自己的UNIX操作系统支持多少种信号,可以在命令行中通过kill -l指令查看。一般来说MAC支持31种信号,Linux支持64种信号。不同平台支持的信号都差不多,因为毕竟都是按照POSIX标准来的。
SIGHUP | 1 | A | 终端挂起或者控制进程终止 |
SIGINT 2 | 2 | A | 键盘中断(如break键被按下) |
SIGQUIT | 3 | C | 键盘的退出键被按下 |
SIGILL | 4 | C | 非法指令 |
SIGABRT | 6 | C | 由abort(3)发出的退出指令 |
SIGFPE | 8 | C | 浮点异常 |
SIGKILL | 9 | AEF | Kill信号 |
SIGSEGV | 11 | C | 无效的内存引用 |
SIGPIPE | 13 | A | 往一个写端关闭de socket中连续写入数据 |
SIGALRM | 14 | A | 由alarm(2)发出的信号 |
SIGTERM | 15 | A | 终止信号 |
SIGUSR1 | 30,10,16 | A | 用户自定义信号1 |
SIGUSR2 | 31,12,17 | A | 用户自定义信号2 |
SIGCHLD | 20,17,18 | B | 子进程结束信号 |
SIGCONT | 19,18,25 | 进程继续(曾被停止的进程) | |
SIGSTOP | 7,19,23 | DEF | 终止进程 |
SIGTSTP | 18,20,24 | D | 控制终端(tty)上按下停止键 |
SIGTTIN | 21,21,26 | D | 后台进程企图从控制终端读 |
SIGTTOU | 22,22,27 | D | 后台进程企图从控制终端写 |
处理动作一项中的字母含义如下:
- A 缺省的动作是终止进程
- B 缺省的动作是忽略此信号,将该信号丢弃,不做处理
- C 缺省的动作是终止进程并进行内核映像转储(dump core),内核映像转储是指将进程数据在内存的映像和进程在内核结构中的部分内容以一定格式转储到文件系统,并且进程退出执行,这样做的好处是为程序员提供了方便,使得他们可以得到进程当时执行时的数据值,允许他们确定转储的原因,并且可以调试他们的程序。
- D 缺省的动作是停止进程,进入停止状况以后还能重新进行下去,一般是在调试的过程中(例如ptrace系统调用)
- E 信号不能被捕获
- F 信号不能被忽略
3 信号发送时机
信号发送时机主要有两种,一种是内核自动给进程发送信号。另一种是进程主动给进程发送信号,此时可以是当前进程给当前进程发信号,也可以是进程A给进程B发信号。下面分别解释:
3.1 内核自动给进程发送信号
这里单独列出内核给进程发送信号其实有点牵强,因为内核本身就属于进程地址空间的一部分,只不过这部分地址空间是所有进程共享的。这里只讲一个信号,SIGALARM。
此信号和alarm系统调用有关,alarm()系统调用是给调用进程设置一个告警时间值,到达那个告警时间值内核自动给进程发一个SIGALARM信号,其实现过程是这样的:进程调用alarm()系统调用会传一个时间参数(以秒为单位),该系统调用会在调用进程的task_struct.alarm字段上加上指定的秒然后退出系统调用。内核调度程序schedule()每次执行的时候会遍历一遍进程数组列表里面的所有进程,只要发现有进程当前时间的值已经大于task_struct.alarm的值,就给该进程发一个SIGALARM信号,并重置该进程的alarm=0。我们姑且认为这种信号发送机制为内核自动给进程发送的。
3.2 进程给进程发送信号
进程给进程发送信号分为进程给自己发信号,进程给其他进程发信号两种。大部分应用场景都是进程给其他进程发信号。
不管如何进程给进程发信号都要通过系统调用kill(pid , sig)来实现,注意这里kill不仅仅代表杀死进程的意思,虽然大多数信号都是杀死进程。这里有一个限制,发送进程的euid必须和接受进程的euid相同,或者发送进程具有超级用户权限。该函数的参数说明如下(pid标志接收信号的进程,sig标志要发送的信号):
- pid > 0 , pid代表进程号,即给某单个进程发信号,该单个进程由pid来唯一标志。
- pid = 0 , 信号被发送给当前进程的进程组中的所有进程,这里的一个隐含条件是发送信号的进程必须是进程组的组长。
- pid = -1 , 信号被发送给除0号进程进程外的所有进程。
- pid < -1 , 信号被发送给进程组中的所有进程(进程组号=-pid)。
如果是进程自己给进程自己发信号,则一般是在进程执行程序中调用kill(pid,sig)。
如果是进程自己给其他进程发信号,可选择的方式就很多了,可以在进程代码执行过程中发送,也可以用命令行发送。
用命令行发送信号的格式一般是 kill -sig pid。其实用命令行发送信号本质也是进程给进程发信号。
4 信号处理时机
信号在被进程从内核态转到用户态的时候执行。常见的执行态切换有 系统调用返回, 时钟中断返回。这两种本质上都是中断处理返回。Linux当中系统调用的返回值放在eax寄存器中,接着内核代码判断进程状态,如果进程状态不是0,则去执行调度程序。如果进程时间片到期,则也去执行调度程序。
接着开始检查当前进程的task_struct.signal & ~task_struct.blocked , 如果有收到未被屏蔽的信号,则按照信号从低位到高位的方式依次调用do_signal信号处理函数。
从这里可以看出,信号的处理过程是异步的,并不是说给进程发了信号,进程就立刻马上执行完当前指令就去执行信号处理程序。而是选择在从内核态返回到用户态的时候检查处理。一个进程在执行过程中可能没有系统调用(第一次创建fork(),最后一次退出exit()除外),或者系统调用的频率非常低,所以我们发的信号有可能很长时间得不到处理。但是时钟中断发生的频率非常频繁,并且是匀速发生的,所以在这里从时钟中断的角度来说的话,可以认为信号的处理是准实时进行的。
5 统一事件源
当信号发生的时候进入信号处理函数,信号处理函数只是简单的发送到管道,IO复用程序会监听管道的另一头如果有数据到来就进入主循环内获取信号值,根据不同的信号值来处理对应的业务逻辑。
代码实现:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <signal.h>
#define MAX_EVENT_NUMBER 1024
static int pipefd[2];
int setnonblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int net_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
void addfd(int epollfd, int fd)
{
struct epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd, 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> <port>\n",basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int ret = 0;
struct sockaddr_in svr_addr;
memset(&svr_addr, 0, sizeof(svr_addr));
svr_addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &svr_addr.sin_addr);
svr_addr.sin_port = htons(port);
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
bind(listenfd, (struct sockaddr*)&svr_addr, sizeof(svr_addr));
listen(listenfd, 5);
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
addfd(epollfd, listenfd);
/*使用socketpair创建管道,也就是UNIX域套接字*/
socketpair(AF_UNIX, SOCK_STREAM, 0, pipefd);
setnonblocking(pipefd[1]);
addfd(epollfd,pipefd[0]);
addsig(SIGHUP);
addsig(SIGCHLD);
addsig(SIGTERM);
addsig(SIGINT);
bool stop_server = false;
while (!stop_server) {
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if ((number < 0) && (errno != EINTR)) {
printf("epoll fails!\n");
break;
}
int i = 0;
for (;i < number;i++) {
int sockfd = events[i].data.fd;
if (sockfd == listenfd) {
struct sockaddr_in cli_addr;
socklen_t cli_addr_len = sizeof(cli_addr);
int clifd = accept(listenfd, (struct sockaddr*)cli_addr, &cli_addr_len);
addfd(epollfd, clifd);
}
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 {
/*每个信号值占1字节,所以按字节逐个接收信号,以SIGTERM为例,来说明如何安全地终止服务器主循环*/
int i = 0;
for (; i < ret; ++i) {
switch(signals[i])
{
case SIGCHLD:
case SIGHUP:
{
continue;
}
case SIGTERM:
case SIGINT:
{
stop_server = true;
}
}
}
}
}
else {
//这部分处理其他socket获取的数据
}
}
}
printf("close fds\n");
close(listenfd);
close(pipefd[0]);
close(pipefd[1]);
return 0;
}
参考: