在上一篇博客中,我们认识信号,在此篇博客中我们具体的来看看信号
上一篇博客链接:https://blog.csdn.net/dangzhangjing97/article/details/79969176
一、阻塞信号
1.信号的相关概念
信号递达:实际执行信号的处理动作就是信号递达
信号未决:信号从产生到递达之间的状态就是信号未决(未决就是没有解决)
进程可以选择阻塞某个信号,此信号如果产生的话,那就直接将保持在未决状态,直到进程解除对此信号的阻塞,才会执行递达操作
2.阻塞和忽略的区别
阻塞是在递达之前,只要信号被阻塞,就不会被递达(递达就是处理),
忽略是在递达之后,它是在处理信号时的一种可选动作
3.信号在内核中的表示示意图:
ps:每个信号都有两个标志位,分别表示阻塞和未决,还有一个函数指针表示处理动作
4.如果在进程解除对某信号的阻塞之前,这种信号产生过多次,该如何处理?
普通信号(编号为1-31的信号):产生多次只计一次
实时信号(编号为34-64的信号):产生多次时,会依次存放在一个队列里,一个一个来处理
5.收到一个信号的处理过程是:
收到某信号后,把未决信号集中的此信号置为1(1表示未解决的信号),然后去阻塞信号集中查看此信号有没有被阻塞,如果没有被阻塞的话,那就去信号捕捉函数数组中查看该如何处理该信号
6.sigset_t
(1)sigset_t是什么?
是一种数据类型,也是一个信号集
(2)为什么需要sigset_t?
在阻塞信号集中,阻塞和未阻塞用一个bit表示就可以,在未决信号集中也是一样,因此阻塞和未决可以用相同的数据类型sigset_t来存储
(3)sigset_t表示每个信号的“有效”和“无效”,“有效”和“无效”是什么?
1.在阻塞信号集中,有效表示的是该信号被阻塞,无效表示的是该信号没有被阻塞
2.在未决信号集中,有效表示的是该信号没有解决,无效表示的是该信号处于解决状态
6.信号集相关函数
头文件:#include<signal.h>
1.int sigemptyset(sigset_t* set);
功能:初始化信号集(把set所指向的信号集中的所有信号的对应bit清零,表示该信号集不包含任何有效信号)
返回值:成功返回0,失败-1
2.int sigfillset(sigset_t* set);
功能:初始化信号集(把set所指向的信号集中的所有信号的对应bit置为1,表示该信号集包含所有的有效信号)
返回值:成功返回0,失败-1
3.int sigaddset(sigset_t* set,int signo);
功能:添加有效信号signo(把编号为signo的信号在位图中的bit从0变为1)
返回值:成功返回0,失败-1
4.int sigdelset(sigset_t* set,int signo);
功能:删除有效信号signor(把编号为signo的信号在位图中的bit从1变为0)
返回值:成功返回0,失败-1
5.int sigismember(const sigset_t* set,int signo);
功能:用于判断信号集中的有效信号是否包含编号为signor的信号
返回值:包含则返回1,不包含则返回0,出错返回-1
ps:在使用sigset_t类型的变量之前,一定要调用sigemptyset()/sigfillset()做初始化,使信号集处于确定的状态
(1)sigprocmask()(操作阻塞信号集的函数)
功能:可以读取或更改进程的阻塞信号集(信号屏蔽字)
头文件:#include<signal.h>
返回值:成功返回0,失败返回-1
int sigprocmask(int how,const sigset_t* set,sigset_t* oset);
参数说明:
how
想要如何操作信号屏蔽字,此参数有三个可选值:
1.SIG_BLOCK:就是把对应信号的bit位改为1,即就是阻塞该信号
2.SIG_UNBLOCK:就是把对应信号的bit位改为0,即就是使该信号不阻塞
3.SIG_SETMASK:设置当前信号屏蔽字为set所指向的值
set/oset
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出
如果set是非空指针,则更改当前进程的信号屏蔽字,参数how指示给如何更改
如果set和oset都是非空指针,则oset中保存的是进程原来的信号屏蔽字,然后参数set和how来操作信号屏蔽字
ps:如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在此函数返回前,至少将其中一个信号递达
(2)sigpending()(操作未决信号集的函数)
功能:读取当前进程的未决信号集,通过参数set传出
头文件:#include<signal.h>
返回值:成功返回0,失败返回-1
int sigpending(sigset_t* set);
示例
1.阻塞2号信号
#include<stdio.h>
#include<signal.h>
void printpending(sigset_t* set)
{
int i=1;
//打印未决信号集
for(;i<32;i++)
{
//ismember()是判断某信号是否在目标集合中
if(sigismember(set,i))
{
putchar('1');
}
else
{
putchar('0');
}
printf(" ");
}
printf("\n");
}
int main()
{
sigset_t s,p;//定义信号集对象
sigemptyset(&s);//清空信号集,即对信号集做初始化
sigaddset(&s,SIGINT);//给信号集中添加2号信号SIGINT
sigprocmask(SIG_BLOCK,&s,NULL);//设置阻塞信号集
while(1)
{
sigpending(&p);//读取未决信号集
printpending(&p);//打印未决信号集
sleep(1);//每隔一秒把所有信号的状态都打印一次
}
return 0;
}
运行结果:
结果解析:2号信号的作用是中止进程,但是现在阻塞了2号信号,所以当程序在运行时,按下ctrl -c不起作用,中止不了进程,此时没有阻塞SIGQUIT,所以ctrl -\可以中止程序
2.阻塞2号信号和3号信号
#include<stdio.h>
#include<signal.h>
void printpending(sigset_t* set)
{
int i=1;
//打印未决信号集
for(;i<32;i++)
{
//ismember()是判断某信号是否在目标集合中
if(sigismember(set,i))
{
putchar('1');
}
else
{
putchar('0');
}
printf(" ");
}
printf("\n");
}
int main()
{
sigset_t s,p;//定义信号集对象
sigemptyset(&s);//清空信号集,即对信号集做初始化
sigaddset(&s,SIGINT);//给信号集中添加2号信号SIGINT
sigaddset(&s,SIGQUIT);//给信号集中添加3号信号SIGQUIT
sigprocmask(SIG_BLOCK,&s,NULL);//阻塞2号信号
while(1)
{
sigpending(&p);//读取未决信号集
printpending(&p);//打印未决信号集
sleep(1);//每隔一秒把所有信号的状态都打印一次
}
return 0;
}
结果解析:
结果解析:此时阻塞了SIGINT和SIGQUIT,这个信号的作用都是中止进程,此时分别按下这两个信号都不起作用,所以只能采用kill来中止进程了
3.阻塞2号信号、3号信号、9号信号
#include<stdio.h>
#include<signal.h>
void printpending(sigset_t* set)
{
int i=1;
//打印未决信号集
for(;i<32;i++)
{
//ismember()是判断某信号是否在目标集合中
if(sigismember(set,i))
{
putchar('1');
}
else
{
putchar('0');
}
printf(" ");
}
printf("\n");
}
int main()
{
sigset_t s,p;//定义信号集对象
sigemptyset(&s);//清空信号集,即对信号集做初始化
sigaddset(&s,SIGINT);//给信号集中添加2号信号SIGINT
sigaddset(&s,SIGQUIT);//给信号集中添加3号信号SIGINT
sigaddset(&s,SIGKILL);//给信号集中添加9号信号SIGINT
sigprocmask(SIG_BLOCK,&s,NULL);//设置阻塞信号集
while(1)
{
sigpending(&p);//读取未决信号集
printpending(&p);//打印未决信号集
sleep(1);//每隔一秒把所有信号的状态都打印一次
}
return 0;
}
运行结果:
结果解析:我们理论上是阻塞SIGINT、SIGQUIT、SIGKILL,但从执行结果来看SIGKILL没有被阻塞,因为信号SIGKILL不能被阻塞或者忽略,故可以用SIGKILL来结束进程
二、捕捉信号
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号
1.信号被捕捉的流程
ps:主控制流程(例如main函数)和信号自定义的处理函数是使用不同的堆栈空间的,它们之间不存在调用和被调用的关系,是两个独立的控制流程
2.相关函数
(1)sigaction()
头文件:#include<signal.h>
功能:读取和修改与指定信号相关联的处理动作
返回值:成功返回0,失败返回-1
int sigaction(int signo,
const struct sigaction*act,
struct sigaction* oact );
参数说明:
1.signo:是指定信号的编号
2.act:指向sigaction结构体,若此指针非空,则根据act修改信号的处理动作
3.oact:指向sigaction结构体,若此指针非空,则通过oact传出该信号原来
的处理动作
结构体sigaction
struct sigaction{
void (*sa_handler)(int);
void (*sa_sigaction)(int,siginfo_t*,void*);//是实时信号的处理函数
sigset_t sa_mask;//在信号被处理的过程中,指定那些信号被阻塞
//注意:默认当前信号本身被阻塞
int sa_flags;//一般设为0
sa_handler
sa_handler有三种形式:
1.给sa_handler赋值为常数SIG_ING传给sigaction函数表示忽略信号
2.给sa_handler赋值为常数SIG_DFL表示执行系统默认动作
3.给sa_handler赋值为一个函数指针的话,表示用户自定义捕捉信号后者说是向
内核注册了一个信号处理函数,此函数的返回值是void,可以一个int参数,通过
参数可以得知当前信号的编号,这样就可以使用一个函数处理多个信号
ps:此函数不是被main函数调用的,而是被系统调用
示例1
#include<stdio.h>
#include<signal.h>
void handler(int sig)
{
printf("hello\n");
}
int main()
{
struct sigaction act;
act.sa_handler=handler;//自定义信号处理方法
act.sa_flags=0;
alarm(1);//设置闹钟
sigaction(SIGALRM,&act,NULL);//捕捉闹钟信号
while(1)
{
printf("haha\n");
usleep(100000);
}
return 0;
}
运行结果:
解析:给程序设置了闹钟信号,对信号的处理方式是自定义的,当1秒钟过后,处理此信号,输出“hello”
示例2
#include<stdio.h>
#include<signal.h>
int main()
{
struct sigaction act;
act.sa_handler=SIG_DFL;//对此信号的处理采取默认行为
act.sa_flags=0;
alarm(1);//设置闹钟
sigaction(SIGALRM,&act,NULL);//捕捉闹钟信号
while(1)
{
printf("haha\n");
usleep(100000);
}
return 0;
}
运行结果:
解析:此信号的默认处理行为是当alarm函数设置的时间过后,直接终止进程
(2)signal()
功能:设置信号的处理方式
头文件:#include<signal.h>
void(* signal(int signum,void(*hander)(int))(int)
对于此函数,我们经常这样使用:
typedef void(*sighandler_t )(int);
sighandler_t signal(int signum,sighandler_t handler);
参数说明:
signum:是要进行处理的信号的编号
handler:有三种形式的取值
(1)SIG_IGN:忽略指定的信号
(2)SIG_DFL:对指定的信号采取默认行为
(3)对指定的信号采取自定义的处理办法
返回值
signal返回一个函数指针void(*)(int),即signal函数对指定的信号自定义
处理函数handler,如果定义成功,则会返回处理函数的指针
示例1:
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
void Hander(int sig)
{
printf("hahahha\n");
exit(1);
}
int main()
{
signal(SIGINT,Hander);
while(1)
{
printf("hello\n");
usleep(100000);
}
return 0;
}
运行结果:
解析:signal函数对SIGINT的处理方式自定义方法,输出”hahahha“,当我们按键产生2号信号—>SIGINT时,此信号就会被捕捉,从而输出我们想要的结果,并且终止进程
示例2:
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
int main()
{
signal(SIGINT,SIG_IGN);
while(1)
{
printf("hello\n");
usleep(100000);
}
return 0;
}
运行结果:
解析:我们对信号SIGINT的处理方法是忽略,故当按键产生此信号时,操作系统并不会处理此信号,当然,我们可以使用SIGQUIT信号来终止进程,因为此信号并没有被忽略哦
sigaction()与signal()的区别:
1.sigaction是UNIX早期的函数,sigaction的设计意图是取代signal;
2.saignal只能修改信号处理的动作,sigaction既能修改也能读取信号处理的动作;
3.sigaction接口更丰富,可以设置更多的选项,功能更强大
(3)pause()
头文件:#include<unistd.h>
功能:使调用进程挂起直到有信号递达
(使当前进程暂停(进入睡眠状态),直到被信号中断)
int pause(void)
返回值:
1.如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;
2.如果信号的处理动作是忽略,则进程继续处于挂起状态
pause函数不返回;
3.如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,
errno设置为EINTR
故pause函数只有出错的返回值
模拟实现sleep()
#include<stdio.h>
#include<signal.h>
void sig_alrm(int sig)
{
printf("haha\n");
}
int mysleep(int second)
{
struct sigaction new,old;
new.sa_handler=sig_alrm;
new.sa_flags=0;
sigaction(SIGALRM,&new,&old);//注册信号处理函数
alarm(second);
pause();
sigaction(SIGALRM,&old,NULL);//恢复信号的默认处理动作
return;
}
int main()
{
printf("before sleep\n");
mysleep(2);
printf("after sleep\n");
return 0;
}
如果不注册信号处理函数,会出现以下的情况:
#include<stdio.h>
#include<signal.h>
int mysleep(int second)
{
struct sigaction new,old;
new.sa_handler=SIG_DFL;
new.sa_flags=0;
sigaction(SIGALRM,&new,&old);//注册信号处理函数
alarm(second);
pause();
sigaction(SIGALRM,&old,NULL);//恢复信号处理函数
return;
}
int main()
{
printf("before sleep\n");
mysleep(2);
printf("after sleep\n");
return 0;
}
运行结果:
解析:如果不注册信号处理函数,而是执行信号的默认的处理方式,就会打印
Alarm sleep,不会执行mysleep()后面的代码,而是直接结束程序,因为
SIGALRM信号的默认处理方式是直接终止进程