Linux之信号
信号集类型sigset_t
是描述信号的集合的结构体
定义
typedef struct
{
unsigned long sig[_NSIG_WORDS];
}sigset_t
相关函数
#include <signal.h>
sigemptyset(sigset_t *set) //初始化由set指定的信号集,信号集里面的所有信号被清空
sigfillset(sigset_t *set) //调用该函数后,set指向的信号集中将包含linux支持的64种信号
sigaddset(sigset_t *set, int signum) //在set指向的信号集中加入signum信号;
sigdelset(sigset_t *set, int signum) //在set指向的信号集中删除signum信号;
sigismember(const sigset_t *set, int signum) //判定信号signum是否在set指向的信号集中。
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);
}
-
sa_handler
:函数指针,指向信号处理函数。默认使用此函数处理信号 -
sa_sigaction
:同样是信号处理函数,有三个参数,可以获得关于信号更详细的信息。可以通过参数四使用此函数处理信号。即参数一和参数二是二选一的。 -
sa_mask
:信号处理函数执行期间需要被屏蔽的信号。 -
sa_flags
:指定信号处理的行为- SA_RESTART,使被信号打断的系统调用自动重新发起
- SA_NOCLDSTOP,使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号
- SA_NOCLDWAIT,使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程
- SA_NODEFER,使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
- SA_RESETHAND,信号处理之后重新设置为默认的处理方式
- SA_SIGINFO,使用
sa_sigaction
而不是sa_handler
作为信号处理函数
-
sa_restorer
:废弃不使用
sigaction函数
用于设置对某种信号的处理方式
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
-
signum
:操作的信号 -
act
:设置新的处理方式。 -
oldact
:原来的处理方式 -
返回值
:0 表示成功,-1 表示有错误发生
Linux下信号处理机制
Linux下的信号采用的异步处理机制,当进程收到信号时,操作系统会中断进程当前的正常流程,转而进入信号处理函数执行操作,完成后再返回中断的地方继续执行,且信号处理期间系统不会再次触发它,即处理期间如果有同类型的信号发来会被屏蔽。所以我们希望信号处理函数执行快、尽可能少错过信号。
一般的解决方案是,信号处理函数用于通知主循环发生信号,具体的处理逻辑放在主循环中。
统一事件源
信号处理函数使用管道将信号传递给主循环,信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出信号值。使用I/O复用(select、poll、epoll)系统调用来监听管道读端的可读事件,这样信号事件与其他文件描述符都可以通过epoll来监测,从而实现统一处理
使用管道通信
在linux下,使用socketpair函数能够创建一对套接字进行通信
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);
-
domain
:协议族 -
type
:协议。SOCK_STREAM(基于TCP)或者SOCK_DGRAM(基于UDP) -
protocol
:类型,只能为0 -
sv[2]
:即管道两端,均可进行读写操作 -
返回值:成功为0,失败为-1
以下为具体案例 (省略头文件)
//自定义信号处理函数,仅通知主循环发生信号
void sig_handler(int sig)
{
//为保证函数的可重入性,保留原来的errno
//可重入性表示中断后再次进入该函数,环境变量与之前相同,不会丢失数据
int save_errno = errno;
int msg = sig;
//将信号值从管道写端写入,传输字符类型,而非整型
send(pipefd[1], (char *)&msg, 1, 0);
//将原来的errno赋值为当前的errno
errno = save_errno;
}
//通过sigaction函数设置用自定义信号处理函数处理信号
void addsig(int sig, void (handler)(int), bool restart = true)
{
//创建sigaction结构体变量
struct sigaction sa;
memset(&sa, '\0', sizeof(sa));
//信号处理函数中仅仅发送信号值,不做对应逻辑处理
sa.sa_handler = handler;
//可以设置被信号打断的系统调用自动发起
if (restart)
sa.sa_flags |= SA_RESTART;
//将所有信号添加到信号集中,即处理信号期间屏蔽所有信号
sigfillset(&sa.sa_mask);
//执行sigaction函数给信号设置处理方式,assert只用于检测错误
assert(sigaction(sig, &sa, NULL) != -1);
}
//创建管道套接字
ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);
assert(ret != -1);
//管道通信时如果缓冲区满了,则会阻塞,进一步增加信号处理函数执行时间
//为了避免,设置为非阻塞
//未对非阻塞返回值处理是因为定时事件是非必须立即处理的事件,可以允许这样的情况发生
setnonblocking(pipefd[1]);
//设置管道读端为ET非阻塞,即检测到事件触发时通知,必须立即处理
addfd(epollfd, pipefd[0], false);
//传递给主循环的信号值,这里只关注 SIGALRM 和 SIGTERM
addsig(SIGALRM, sig_handler, false);
addsig(SIGTERM, sig_handler, false);
//循环条件
bool stop_server = false;
//超时标志
bool timeout = false;
//每隔TIMESLOT时间触发SIGALRM信号
alarm(TIMESLOT);
while (!stop_server)
{
//监测发生事件的文件描述符
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
break;
}
//轮询文件描述符
for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;
//管道读端对应文件描述符发生读事件
if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
{
int sig;
char signals[1024];
//从管道读端读出信号值,成功返回字节数,失败返回-1
//正常情况下,这里的ret返回值总是1,只有14和15两个ASCII码对应的字符
ret = recv(pipefd[0], signals, sizeof(signals), 0);
if (ret == -1)
{
//handle the error
continue;
}
else if (ret == 0)
{
continue;
}
else
{
//真正处理信号值对应的逻辑
for (int i = 0; i < ret; ++i)
{
switch (signals[i])
{
case SIGALRM:
{
timeout = true;
break;
}
case SIGTERM:
{
stop_server = true;
}
}
}
}
}
}