在谈linux信号处理函数前,有必要先聊下linux信号的机制。包含信号的作用、信号的产生、信号的阻塞等。
信号机制
信号是linux进程通信的一种方式,很多情况下,信号是由一个错误产生的,通知进程修改行为,但是,也由很大一部分场景是由人为产生信号,通知进程执行某些动作。
信号的产生
信号的产生主要有以下几种情况:
- 用户在终端(比如:键盘)按下某些按键,终端(键盘)驱动程序会发送信号给前台进程。比如:ctrl+c产生SIGINT(2)信号;ctrl+\产生SIGQUIT(3)信号;ctrl+z产生SIGTSTP(20)信号。
- 硬件异常产生信号,硬件检测到异常就通知内核,然后内核进程向当前用户进程发送适当的信号。比如:用户进程进行了除以0的操作,CPU运算单元会产生异常,内核将这个异常解释为SIGFPE(8)信号发送给用户进程。比如:当前用户进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGEGV(11)信号发送个用户进程。
- 用户调用某些函数(例如kill函数),就可以发送信号到另一个进程。比如:终端下,输入kill命令发送信号给某个进程(kill命令内部也是调用kill函数实现的),如果kill命令没有明确指定信号,默认发送的是SIGTERM(15)信号,该信号的默认处理是终止进程。
- 当内核检测到某种软件条件发生时,也会通过信号通知用户进程。比如:闹钟超时信号SIGALRM(14),向读端已经关闭的管道写数据时会产生SIGPIPE(13)信号。
信号的阻塞
实际执行信号的处理动作称为信号抵达,信号从产生到抵达之间的状态,称为信号未决。进程可以选择阻塞某个信号。 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行抵达的动作。阻塞和忽略是不相同的,只要信号被阻塞就不会抵达,而忽略是在抵达之后可选的一种处理动作。
举个例子:
上图中,
- SIGHUP(1)信号未产生过(pending=0)也未阻塞(block=0),当它抵达时执行默认处理动作。
- SIGINT(2)信号产生过(pending=1),但正在被阻塞(block=1),所以暂时不能抵达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT(3)信号未产生过(pending=0),一旦产生SIGQUIT信号将被阻塞(block=1),它的处理动作是用户自定义函数sighandler。
linux信号相关数据结构
相关数据结构可以直接到linux上grep -R查找,一般是在<signal.h>头文件中。
struct sigaction
struct 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);
};
struct sigset_t
struct sigset_t就是信号集合,看代码就知道是一个信号数组。定义如下:
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
...
typedef __sigset_t sigset_t;
linux信号相关处理函数
sigemptyset
sigemptyset
初始化set所指向的信号集,使其中所有信号的对应的bit清零,表示该信号集不包含任何有效信号。
sigfillset
sigfillset
初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
sigaddset
sigaddset
在该信号集中添加某种有效信号。
sigdelset
sigdelset
在该信号集中删除某种有效信号。
sigismember
sigismember
是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含贼返回1,不包含则返回0,出错返回-1。
sigprocmask
sigprocmask
读取或更改进程的信号掩码,如果成功返回0,失败返回-1。
信号掩码是进程当前被阻塞的信号的集合,即阻塞信号集合。
SIG_BLOCK,就是进程当前已有的阻塞信号集和set参数指定的信号集的并集。
SIG_UNBLOCK,就是进程当前已有的阻塞信号集和set参数指定的信号集的差集。
SIG_SETMASK,就是直接用set参数指定的信号集来设置进程的阻塞信号集。
sigpending
sigpending
读取当前进程的未决信号集,通过set参数传出,调用成功则返回0,出错则返回-1。
举例
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void printsigset(sigset_t *set)
{
int i;
for(i=1;i<=31;i++){
if(sigismember(set,i))
putchar('Y');
else
putchar('N');
}
puts("");
}
int main()
{
sigset_t s,p; //定义两个信号集,s和p
sigemptyset(&s); //初始化清空信号集s
sigemptyset(&p); //初始化清空信号集p
sigaddset(&s,SIGINT); //往信号集s中添加信号SIGINT
sigprocmask(SIG_BLOCK,&s,NULL); //设置阻塞信号集,把SIGINT加入阻塞信号集
while(1)
{
sigpending(&p); //获取未决信号集,通过参数p返回
printsigset(&p);
sleep(1);
}
return 0;
}
运行结果:
这个程序的大概意思就是 我们阻塞一个信号集(在本程序里,这个信号集s只含有一个信号SIGINT),让它一直处于未决状态,然后在while循环过程中,我们手动ctrl+c(即发送SIGINT信号给进程),后面我们用sigpending获取进程的未决信号集(即参数p),它里面就会出现这个信号,然后他还是一直处于未决状态。
sigsuspend
临时替换掉进程阻塞信号集(信号掩码),挂起进程,等待,直到一个是调用handler 或者 终止进程的信号抵达。
信号抵达后,该函数就返回,此时进程阻塞信号集又还原成了sigsuspend执行之前的阻塞信号集。
举例
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void handler_int(int sig)
{
printf("recive sig INT..\n");
}
void handler_term(int sig)
{
printf("recive sig TERM..\n");
}
int main()
{
printf("pid=%d\n", getpid());
signal(SIGINT, handler_int);
signal(SIGTERM, handler_term);
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigaddset(&set, SIGALRM);
sigaddset(&set, SIGIO);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGTERM);
if (sigprocmask(SIG_BLOCK, &set, NULL) == -1)
{
printf("sigprocmask() failed\n");
}
printf("start sleep 3s..\n");
sleep(3);
printf("end sleep 3s..\n");
sigemptyset(&set);
while(1)
{
printf("while..\n");
sigsuspend(&set);
sleep(3);
}
return 0;
}
// gcc -g test_signal.c -o test_signal
信号抵达的处理函数
linux信号抵达处理函数有两个,一个是signal,一个是sigaction。
signal各个UNIX系统版本实现不同,不符合POSIX标准,即:移植性不好。sigaction符合POSIX标准,移植性好。并且sigaction功能比signal更全更强大。
signal
signal
signal函数就是给信号注册信号抵达时候的动作。
signal的不可移植性主要体现在:
当正在执行signal handler函数时,再来相同的信号,是否阻塞,已经如何处理的。
比如:
- 在最原始的UNIX系统上,当正在执行signal handler函数时,该信号的handler会被reset成SIG_DFL默认处理方式,并且不会阻塞后续的相同信号。相当于sigaction的sa.sa_flags = SA_RESETHAND | SA_NODEFER;
- 在System V标准中,当正在执行signal handler函数时,如果又来了相同信号,也是不阻塞,但是没有reset成SIG_DFL,就会递归调用signal handler。
- 在BSD标准中,当正在执行signal handler函数时,如果又来了相同信号,会阻塞,没有reset成SIG_DFL,当本次handler执行完毕后,被handler打断的一些系统调用会自动重启。相当于sigaction的sa.sa_flags = SA_RESTART;
注意:
- 正在执行的signal handler函数会堵塞相同的信号,但是没有办法阻塞其它其他的信号。比如:正在处理SIG_INT,再来一个SIG_INT则会堵塞,但是来SIG_QUIT则会被其中断,如果SIG_QUIT有处理,则需要等待SIG_QUIT处理完了,SIG_INT才会接着刚才处理。下面的代码例子可以验证这一点。
- 正在执行的singal handler函数,如果这时候又来了多个相同信号,那么这些相同信号会合并成一个,当handler函数执行完毕后抵达。下面的代码例子可以验证这一点。
举例
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void handler_int(int sig)
{
printf("handler_int start, handler = %d\n", sig);
sleep(5);
printf("handler_int end, handler = %d\n", sig);
}
void handler_quit(int sig)
{
printf("handler_quit start, handler = %d\n", sig);
sleep(10);
printf("handler_quit end, handler = %d\n", sig);
}
int main()
{
signal(SIGINT, handler_int); //注册信号处理函数
signal(SIGQUIT, handler_quit);
while(1)
{
sleep(1);
}
}
执行结果:
sigaction
sigaction
sigaction的内容很长,直接点上面链接查看吧,就不贴出来了。
先再贴下函数原型和struct sigaction数据结构,如下:
挑重点说:
- sa_handler就是指定信号抵达后的处理函数。
- sa_mask指定当正在执行sa_handler时,要被阻塞的信号集。
- sa_flags,当sa_flags是默认的(即0),那么在执行sa_handler时,相同信号再来时也会被阻塞,如果设置为SA_NODEFER,则不会。
举例
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void handler_int(int sig)
{
printf("handler_int start, handler = %d\n", sig);
sleep(5);
printf("handler_int end, handler = %d\n", sig);
}
int main()
{
struct sigaction act;
act.sa_handler = handler_int;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT);
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
while(1)
{
sleep(1);
}
}
运行结果:
signal和sigaction对比总结
sigaction比signal功能强大处就在于,可以明确地设置:当正在处理某个信号的handler时,可以阻塞其他信号。不至于循环嵌套引起语义不明确。