之前我们按下Ctrl+C会直接终止一个进程,其实是个当前的前台进程发送了一个SIGINT信号
一、信号的基本概念
信号的几种产生方式:
1.键盘键入,用户在终端按下某些键时,终端程序会自动发送信号给前台进程。例如Ctrl+C产生SIGINT信号,Ctrl+\产生SIGQUIT信号,Ctrl+Z产生SIGTSTP 信号(将该进程放在后台执行)
2.硬件异常,程序执行过程中触发某些异常,由硬件检测到并通知给内核,然后内核向进程发送相应的信号。例如,当前正在执行一个除以0的指令,cpu的运算单元会产生异常,内核将这个异常解释为SIGFPE发送给进程,再比如对空指针进行解引用时,(访问非法地址),MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
3.调用kill函数,可以发送信号给另外一个进程。常用的函数有,kill()给某个进程发送信号;raise()给自己发送某个信号;abort()给自己发送SIGABRT信号(assert()断言)。
4.软件条件产生,例如闹钟超时产生SIGALARM信号,向所有读端关闭的管道中写数据时产生SIGPIP信号。
常见的几种信号处理方式:
1.忽略此信号
2.执行该信号的默认动作(大多数信号的默认动作为终止该进程)
3.自定义,用户提供一个信号处理函数,即在收到信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号
二、产生信号
1.键盘键入
SIGINT的默认处理动作为终止进程,而SIGQUIT的默认处理动作是终止进程并Core Dump(核心转储)生成一个Core.xxxx,这里的文件后缀为进程的pid,即把进程的用户空间内存的数据全部保存到磁盘上,也可以用在gdb中调试中查看错误原因。
因为默认的Core dump文件大小为0,这里需要先改一些参数
这里的信息可以看出,默认的core文件的大小为 0, 一个进程中所能申请的文件描述符的个数默认为1024个,栈的大小为8M,这些数值都是可以更改的。
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("pid is :%d\n",getpid());
while(1);
return 0;
}
2.调用系统函数向进程发信号
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
abort();
return 0;
}
3.由硬件异常产生信号
#include <stdio.h>
#include <signal.h>
int main()
{
int a=2;
a=a/0;
while(1);
return 0;
}
4.由软件条件产生
这里介绍alarm()函数SIGALRM信号
unisigned int alarm(unisigned int seconds);
调用该函数,即设定一个闹钟,也就是告诉内核在seconds秒后向进程发送SIGALRM信号(终止进程)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int i=0;
alarm(1);
for(i=0;1;i++)
{
printf("%d\n",i);
}
abort();
return 0;
}//这个程序就是在1秒之内数数,闹钟到了就被SIGALRM信号终止
小小一面:解引用空指针会有什么后果?
每个执行中的进程都会有自己的虚拟地址空间,在访问变量时,需要将逻辑地址经页表映射转化为为物理地址,在对空指针进行解引用时,页表映射过程中,MMU在进行虚拟地址到物理地址的转化中会识别出这个一个非法地址则产生硬件异常,内核将这个异常解释为SIGSEGV信号发送给进程,也就是我们看到的程序崩溃。
三、信号的阻塞
关于信号的其他相关概念:
1.信号在内核中的表示
根据上面的图来看:
每个信号都有两个标志位分别表⽰示阻塞(block)和未决(pending),还有一个函数指针表⽰示处理动作。
信号产⽣生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
(1)SIGHUP信号未阻塞也未产⽣生过,当它递达时执⾏行默认处理动作。
(2)SIGINT信号产⽣生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没 有解除阻塞之前不能忽略这个信号,因为进程仍有机 会改变处理动作之后再解除阻塞。
(3)SIGQUIT信号未产⽣生过,一旦产⽣生SIGQUIT信号将被阻塞,它的处理动 作是⽤用户⾃自定义函数sighandler。
如果在进程解除对某信号的阻塞之前这种信号产⽣生过多次,将如何处 理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产⽣生多次只计一 次,⽽而实时信号在递达之前产⽣生多次可以依次放在一个队列⾥里。本章不讨论实时信号
2.一些基本概念
实际执行信号的处理动作(已经处理了)称为 信号递答
信号从产生到递答的状态 称为 信号未决
当进程收到一个信号就会将未决信号集中那一位设置为1,若没有来的及处理该信号,就会将阻塞信号集中该信号对应的那一位设置为1,当该信号不再阻塞时,就会将阻塞信号集中该信号对应的那一位置0,当信号处理完成时,就会将未决信号集中那一位置0。还有一种情况是,当该信号正在阻塞时,又收到一个该信号,常规信号是在信号处理之前只计一次,位图的每一位只表示是否产生该信号,不记录信号的个数(实时信号不同)。
3.信号操作函数
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void MyHandler(int sig)
{
printf("sig :%d\n",sig);
}
void PrintSigSet(sigset_t *set)
{
int i=0;
for(i=1;i<32;i++)
{
int ret=sigismember(set,i);
printf("%d",ret);
}
printf("\n");
}
int main()
{
//int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
//这里的how由三个选项:
//SIG_BLOCK,添加一个信号到信号屏蔽字中
//SIG_UNBLOCK,解除一个信号从信号屏蔽字中
//SIG_SETMASK,将信号屏蔽字设置为set所指的指
//oset不为空时,即将之前的信号屏蔽字备份,之后还原要用到
//int sigpending(sigset_t *set)
//获取信号未决集,通过参数set返回
//1.捕捉SIGINT信号
signal(SIGINT,MyHandler);
//2.将SIGINT信号屏蔽掉
sigset_t set;
sigset_t oset;
sigemptyset(&set);//初始化
sigaddset(&set,2);//将信号SIGINT进行设置
sigprocmask(SIG_BLOCK,&set,&oset);//将信号屏蔽字更改
//3.循环读取未决信号集
int n=0;
while(1)
{
n++;
if(n%4==0)
{
printf("解除信号屏蔽字\n");
sigprocmask(SIG_SETMASK,&oset,NULL);
sleep(1);
printf("再次设置信号屏蔽字\n");
sigprocmask(SIG_BLOCK,&set,&oset);//将信号屏蔽字更改
}
sigset_t pending;
sigpending(&pending);
sleep(1);
PrintSigSet(&pending);
}
return 0;
}
信号的捕捉过程
1.当用户注册了SIGINT信号的处理函数,由于中断或者系统触发SIGINT信号时,程序从用户态进入内核态,
2.处理异常中断,处理完成之后,检测到SIGINT信号已经递答
3.此时就要就要处理信号行为,若该信号的处理动作是用户自定义的
4.则要回到用户态执行该信号的信号处理函数(此处不能理解为main()函数中调用了信号处理函数,信号处理函数和mian()函数不是同一个执行流,之间没有关系)
5.执行完信号的处理函数时,此时需要返回到内核,若没有新的信号递答,此时就该返回到main()函数中中断的地方继续执行了。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
void my_alarm(int sig)
{
(void)sig;
}
unsigned int My_sleep(unsigned int num)
{
struct sigaction new_sig;
struct sigaction old_sig;
unsigned int unslept = 0;
new_sig.sa_handler = my_alarm;
new_sig.sa_flags = 0;
//注册信号处理函数
sigaction(SIGALRM,&new_sig,&old_sig);
//调用闹钟处理信号
alarm(num);
//pause()
pause();
//清空闹钟
unslept = alarm(0);
sigaction(SIGALRM,&old_sig,NULL);
return unslept;
}
int main()
{
printf("hello\n");
My_sleep(3);
printf("world\n");
return 0;
}
main函数调⽤用my_sleep函数
后者调⽤用sigaction注册了SIGALRM信号的处理函数sig_alr
调⽤用alarm(nsecs)设定闹钟
调⽤用pause等待,内核切换到别的进程运⾏行
nsecs秒之后,闹钟超时,内核发SIGALRM给这个进程
从内核态返回这个进程的⽤用户态之前处理未决信号,发现有SIGALRM信号,其处理函数 是 sig_alrm
进⼊入sig_ alrm函数时SIGALRM信号被自动屏蔽
从sig_alrm函数 返回时SIGALRM信号⾃自动解除屏蔽
然后自动执⾏行系统调用sigreturn再次进⼊入 内核,再返回⽤用户态 继续执⾏行进程的主控制流程(main函数调⽤用的mysleep函数)