Linux 进程信号:信号的概念、生命周期、产生流程、阻塞
在半年前我写过一篇博客介绍了Linux中信号的概念以及处理流程,这次再来深入的讲一讲信号的具体使用方法以及在网络编程中的具体应用。
信号函数
要想为一个信号设置处理函数,可以使用以下两个系统调用。
signal
signal函数的使用非常简单,只需要直接指定信号类型以及处理的方法即可
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数
sighandler_t:函数指针,返回值为void,参数为int的函数
signum:要捕获的信号类型
handler:指定的信号处理函数
返回值
调用成功:signal函数调用成功的时候会返回一个函数指针,函数指针的值为前一次调用signal函数时传入的函数指针,或者是信号sig对应的默认处理函数指针SIG_DEF(如果时第一次调用的话)调用失败:signal函数调用出错时返回SIG_ERR,并设置errno
sigaction
sigaction比起signal要更加的健壮。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
参数
signum:信号类型
act:指定新的信号处理方法
oact:输出该信号之前的处理方法
返回值
成功:返回0
失败:返回1并设置errno
从上面可以看到act和oldact都是sigaction结构体指针,下面来看看它里面有什么内容
struct sigaction {
void (*sa_handler)(int); //信号的处理方法
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask; //设置进程的信号掩码,是一个信号集
int sa_flags; //设置程序收到信号后的行为,参数如下图
void (*sa_restorer)(void); //已过时,最好不要使用
};
信号集
#include<bits/sigset.h>
#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t
上面是信号集的定义,从上面可以看出,其实信号集就是用于设置信号掩码的一个位图,他的每一个位都用来表示一个信号。
下面是信号集的基本操作
#include <signal.h>
//清空信号集
int sigemptyset(sigset_t *set);
//在信号集中设置所有信号
int sigfillset(sigset_t *set);
//将signum信号添加进信号集中
int sigaddset(sigset_t *set, int signum);
//将signum信号从信号集中删除
int sigdelset(sigset_t *set, int signum);
//查看signum信号在不在信号集中
int sigismember(const sigset_t *set, int signum);
我们还可以通过以下函数来查看进程的信号掩码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数
set:设置新的信号掩码
oldset:数据原来的信号掩码
how:设置信号掩码的方式
返回值
成功:返回0
失败:返回-1并设置errno
当我们设置完信号掩码之后,被屏蔽的信号就不能被进程接收,如果向进程发送一个被屏蔽的信号,则操作系统会将该信号设置为挂起信号,直到进程取消对该信号的屏蔽的时候再处理这个信号
同时,有两个信号比较特殊,9号信号SIGKILL和19号信号SIGSTOP,这两个信号不可被阻塞,不可被忽略,不可被自定义处理。
使用下面这个函数可以获得当前被挂起的信号集
因为挂起信号使用信号集来进行存储,而信号集又是一个位图,这就意味着即使一个信号多次被接收,在位图中也只能反应一次,并且我们结束屏蔽后对挂起信号进行处理时,也只会触发一次信号处理函数
#include <signal.h>
int sigpending(sigset_t *set);
参数
set:用于保存被挂起的信号集
返回值
成功:返回0
失败:返回-1并设置errno
统一事件源
信号是一种异步事件,所以信号处理函数和程序的主循环是两条不同的执行路线。很显然,信号处理函数需要尽可能快地执行完毕,确保该信号不会被屏蔽太久。(为了避免一些竞态条件,信号在处理期间,系统不会再次触发他)
一种典型的解决方案是把信号的主要处理逻辑放到程序地主循环中,当信号处理函数被触发,它只是简单的通知主循环程序接收到信号,并把信号值传递给主循环,主循环从读取出该信号,并根据具体接收到的信号值执行对应的逻辑代码。
信号处理函数通常都会使用管道来作为信号传递的媒介,那么主循环如何直到管道什么时候有数据呢?很简单,只需要用I/O复用来监听管道读端的可读事件,这样一来信号事件就可以和其他I/O事件一样被处理,这就是统一事件源。在Libevent库与xinetd超级服务中也采用了这样的机制
代码实现如下
#include<fcntl.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<errno.h>
#include<netinet/in.h>
#include<unistd.h>
const int MAX_LISTEN = 5;
const int MAX_EVENT = 1024;
const int MAX_BUFFER = 1024;
static int pipefd[2]; //管道描述符
//设置非阻塞
int setnonblocking(int fd)
{
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag |= O_NONBLOCK);
return flag;
}
//信号处理函数
void sig_handler(int sig)
{
//保留原本的errno, 再函数末尾恢复, 确保可重入性
int save_errno = errno;
send(pipefd[1], (char*)&sig, 1, 0); //将信号值通过管道发送给主循环
errno = save_errno;
}
//设置信号处理函数
void set_sig_handler(int sig)
{
struct sigaction sa;
sa.sa_handler = sig_handler;
sa.sa_flags |= SA_RESTART; //重新调用被信号中断的系统函数
sigfillset(&sa.sa_mask); //将所有信号加入信号掩码中
if(sigaction(sig, &sa, NULL) < 0)
{
exit(EXIT_FAILURE);
}
}
//将描述符加入epoll监听集合中
void epoll_add_fd(int epoll_fd, int fd)
{
struct epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event);
}
int main(int argc, char*argv[])
{
if(argc <= 2)
{
printf("输入参数:IP地址 端口号\n");
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
//创建监听套接字
int listen_fd = socket(PF_INET, SOCK_STREAM, 0);
if(listen_fd == -1)
{
printf("listen_fd socket.\n");
return -1;
}
//绑定地址信息
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
if(bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
{
printf("listen_fd bind.\n");
return -1;
}
//开始监听
if(listen(listen_fd, MAX_LISTEN) < 0)
{
printf("listen_fd listen.\n");
return -1;
}
//创建epoll,现版本已忽略大小,给多少都无所谓
int epoll_fd = epoll_create(MAX_LISTEN);
if(epoll_fd == -1)
{
printf("epoll create.\n");
return -1;
}
epoll_add_fd(epoll_fd, listen_fd); //将监听套接字加入epoll中
//使用sockpair创建全双工管道,对读端进行监控,统一事件源
if(socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd) < 0)
{
printf("socketpair.\n");
return -1;
}
setnonblocking(pipefd[1]); //将写端设为非阻塞
epoll_add_fd(epoll_fd, pipefd[0]); //将读端加入epoll监控集合
set_sig_handler(SIGHUP); //有连接脱离终端(断开)时发送的信号
set_sig_handler(SIGCHLD); //子进程退出时发送的信号
set_sig_handler(SIGINT); //接收到kill命令
set_sig_handler(SIGTERM); //用户按下中断键(DELETE或者Ctrl+C)
int stop_server = 0;
struct epoll_event events[MAX_LISTEN];
while(!stop_server)
{
int number = epoll_wait(epoll_fd, events, MAX_LISTEN, -1);
if(number < 0 && errno != EINTR)
{
printf("epoll_wait.\n");
break;
}
for(int i = 0; i < number; i++)
{
int sock_fd = events[i].data.fd;
//如果监听套接字就绪则处理连接
if(sock_fd == listen_fd)
{
struct sockaddr_in clinet_addr;
socklen_t len = sizeof(clinet_addr);
if(accept(listen_fd, (struct sockaddr*)&clinet_addr, &len) < 0)
{
printf("accept.\n");
continue;
}
epoll_add_fd(epoll_fd, sock_fd);
}
//如果就绪的是管道的读端,则说明有信号到来,要处理信号
else if(sock_fd == pipefd[0] && events[i].events & EPOLLIN)
{
int sig;
char signals[MAX_BUFFER];
int ret = recv(pipefd[0], signals, MAX_BUFFER, 0);
if(ret == -1)
{
continue;
}
else if(ret == 0)
{
continue;
}
else
{
//由于一个信号占一个字节,所以按字节逐个处理信号
for(int j = 0; j < ret; j++)
{
switch (signals[i])
{
//这两个信号主要是某个连接或者子进程退出,对主流程影响不大,直接忽略
case SIGCHLD:
case SIGHUP:
{
continue;
}
//这两个信号主要是强制中断主流程,所以结束服务(不能直接退出,因为还有描述符未关闭)
case SIGTERM:
case SIGINT:
{
stop_server = 1;
}
}
}
}
}
//处理读就绪与写就绪,因为这里主要演示统一事件源,所以不实现这块的逻辑
else
{
/* */
}
}
}
//关闭文件描述符
close(listen_fd);
close(pipefd[1]);
close(pipefd[0]);
return 0;
}
网络编程相关信号
SIGHUP
当某个进程脱离终端时(正常或者非正常),SIGHUP信号都将被触发。该信号的默认处理方式是终止收到该信号的进程,因此如果没有捕捉该信号,则进程就会退出。在网络编程中通常使用SIGHUP信号来强制服务器重读配置文件。
SIGPIPE
SIGPIPE是往一个读端关闭的描述符写数据时通知的信号,例如我们往一个读端关闭的管道或者socket连接中写数据就会引发SIGPIPE信号,并且将errno设置为EPIPE。
由于程序接收到SIGPIPE信号就会默认结束进程,所以我们需要在代码中捕获并且处理该信号,或至少忽略它,因为我们通常不会希望因为错误的写操作而导致程序退出
SIGURG
在Linux环境下,内核通知应用程序带外数据到达主要有两种方法,一种是I/O复用如select等系统调用在接收到带外数据的时候就会返回,并且会向应用程序报告socket上的异常事件。另一种方法就是使用SIGURG信号。