目录
一,信号是什么
1,信号产生前
在信号产生前,我们已经知道了信号产生的意义,并且知道信号产生后需要做什么,为什么能识别呢,因为已经知道了信号特征和怎么识别以及对应的处理过程(已经被提前培养好怎么处理信号)
2,信号产生中
当信号发生后,可能我们正在做优先级更高的事情,比如正在打游戏,快递来了,可能先打完游戏,再去楼下拿快递,所以信号产生后,我们会在合适的时间去处理信号,但是一定需要某种方式去记录(记录的方式通过对进程的位图做出改变)下来这个信号已经产生(),程序产生信号的方式:键盘输入,程序本身的异常。
3,信号处理
1,默认行为
提前安排好信号的处理方式(终止,暂停,继续运行等)。
2,自定义行为
程序员自己处理信号(自定义信号要做什么)
3,忽略信号
注意忽略信号并不是不处理,而是处理方式就是忽略。
二,信号如何发送和怎么记录
1,信号如何发送
进程收到信号,本质是存储信号的位图发生改变,只有操作系统有资格修改进程的位图,操作系统作为进程的管理者,有绝对的资格修改进程的task_struct中的位图,信号的发送方式有多种。
2,信号的记录
进程收到信号不一定直接处理,如果没有被处理,那这个信号就要被记录下来,等待合适的时间去处理,此时就需要数据结构去保存数据,位图就是用来记录信号数据的,通过对比特位的改变就能实现记录信号。多次相同信号输入,只记录一次。
三,信号在内核中如何表示的
信号在进程PCB块中会存在一个信号存储的位图,使用Block位图存储信号是否阻塞,pending位图用来存储信号是否被触发,handler处理方法,有默认,忽略,自定义。
产生信号->信号未决->信号递达
一般信号执行的方式:
发送信号->修改pending->时间合适->检查block->对应信号未被block->开始递达。
我们试着使用信号操作集函数实现对信号的捕捉
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void handler(int signo)
{
printf("get a %d signal\n",signo);
}
void printpending(sigset_t *pending)
{
int = 1;
for(; i <= 31; i++)
{
if(sigismember(pending, i))//判断指定信号在没在信号集里
{
printf("1 ");
}
else
{
printf("0 ");
}
}
printf("\n");
}
int main()
{
signal(2,handler);
sigset_t set oset;//用户定义空间变量
sigemptyset(&set);//初始化set和oset使得成为一个确定的数
sigemptyset(&oset);
sigaddset(&set,2);//将二号信号加入信号集//SIGINT
sigprocmask(SIG_SETMASK,&set,&oset);//阻塞二号信号
sigset_t pending;
int count=0;
while(1)
{
sigemptyset(&pending);
sigpending(&pending);//读取未决信号集,通过pending参数传出
printpending(&pending);
sleep(1);
count++;
if(count==10)
{
sigprocmask(SIG_SETMASK, &oset, NULL); //恢复曾经的信号屏蔽字
printf("恢复信号屏蔽字\n");
}
}
return 0;
}
就可以看到信号在接受到的时候,二号信号对应的比特位为1.
四,信号的捕捉
1,内核如何实现信号捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行
main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
如下图所示:
实际上我们在记录的时候可以如下记录状态的切换
2,为什么需要状态切换
在内核态的时候识别信号可以被捕捉,理论上可以直接访问用户态的代码,但是绝对不能这样设计。
因为如果信号处理函数中存在一些非法代码,而此时我们的状态是内核态,有非常大的权限,能够处理这些非法代码,容易造成错误,所以需要切换到用户态再去实现代码。
3,sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:
使用方法如下:
struct sigaction act, oact;
void handler(int signo)
{
printf("get a signal: %d\n", signo);
sigaction(SIGINT, &oact, NULL);//恢复成默认的信号处理
}
int main()
{
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGINT, &act, &oact);
while(1){
printf("I am a process!\n");
sleep(1);
}
return 0;
}
五,可重入函数
不可重入的函数:
1,调用了malloc和free,因为malloc也是使用全局链表来管理堆的
2,调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构
六,volatile关键字
int flag = 0;
void handler(int signo)
{
printf("get %d signo!\n", signo);
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("Proc Normal Quit!\n");
return 0;
}
在运行这个代码时,会发现当键入ctrl+c时,程序并不能正常退出,这是因为编译器的优化问题,将主函数的flag优化到寄存器里面,而handler函数的flag放在内存中,导致handler不能改变flag的值,为了解决这个问题,加入了volatile关键字避免将flag优化至寄存器里面。
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作