目录
sigset_t类型
信号集操作函数
sigprocmask 函数
sigpending 函数
代码实现
信号捕捉
sigaction
volatile关键字
上节课我们主要学习了进程的产生前与进程产生中的相关内容,学习了进程的产生方式有哪些,学习了进程收到信号之后,进程会怎样进行保存,今天我们在上期学习的基础上继续进程进程信号的学习。
sigset_t类型
在之前创建fork函数创建子进程时,我们的返回值是pid_t类型,在学习共享内存时,我们使用ftok函数获取共享内存的key值,返回值类型是key_t,这些类型都是操作系统内部封装之后的类型,方便我们去理解,同样的在信号中sigset_t也是一种操作系统封装的类型。在上期学习的阻塞表和pending表中,我们知道了这两张表都是位图结构,每个元素都只有0和1两种类型,0表示没有,1表示有,所以我们可以使用操作系统定义的sigset_t类型来设置block表和pending表位图中元素的有无,sigset_t类型也被称作信号集(表示block位图和pending位图),sigset集类型的变量需要配合信号集操作函数来使用。
信号集操作函数
- sigemptyset函数,用于清空block和pending信号集中的bit位(全部bit位清零)。
- sigfillset函数,用于填满block和pending信号集中的bit位(全部bit位置1)。
- sigaddset函数,用于填满对应信号集中的对应信号的bit位(对应信号的bit位置1)。
- sigdelset函数,用于清空对应信号集中对应信号的bit位(对应信号的bit位清零)。
- sigismember函数,用于判断一个信号集中的一个信号是否有效。(信号对应的bit位为1则表示有效,为0表示无效)。
- 注意:一般情况下,在使用sigset_t类型的变量(信号集)之前,一定要使用sigemptyset函数或者sigfillset函数使信号集整体处于确定的状态。
- 返回值:前四个函数成功返回0,失败返回-1。最后一个函数的返回值为bool类型,在信号集中返回1,不在返回0,出错返回-1。
sigprocmask 函数
sigprocmask函数可以用来读取或者更改进程的信号block集(我们称,block集为阻塞集也称信号屏蔽字)。
参数:第一个参数为如何更改,默认为SIG_SETMASK即可,第二个参数为输入型参数,为我们要想要更改称为的信号集,第二个参数为输出型参数,为进程原本的信号集。
返回值:成功返回0,失败返回-1。
sigpending 函数
用于获取当前进程的pending信号集,我们也称pending信号集为未决信号集。
返回值:成功返回0,失败返回-1。
代码实现
代码如下。
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void show_pending(sigset_t* pending )
{for(int i=1;i<32;i++){if(sigismember(pending,i)){printf("1");}else{printf("0");}}printf("\n");
}int main()
{sigset_t iset,oset;sigemptyset(&iset);sigemptyset(&oset);sigaddset(&iset,2);sigprocmask(SIG_SETMASK,&iset,&oset);//设置pending信号集sigset_t pending;int count=1;while(1){//清空pending信号集sigemptyset(&pending);//设置pending信号集为我们所创建的信号集sigpending(&pending);//展示设置的信号集show_pending(&pending);sleep(1);count++;if(count==10){sigprocmask(SIG_SETMASK,&oset,NULL);}}return 0;
}
我们分别通过sigset_t类型设置了信号集变量,最终将设置的信号集变量转为了block信号集和pending信号集。
运行结果如下。
因为刚开始我们将2号信号在阻塞信号集中对应的位置设置为了1,意味着阻塞了2号信号,所以最开始我们发送2号信号,2号信号没有被递达,所以不会进程不会处理2号信号的动作,但是在打印pending信息集的过程中,我们恢复了阻塞信号集为原本的信号集,原本的信号集为oset,因为我们清空了oset,所以最终恢复了block信号集时也是一个清空的信号集,没有阻塞任何信号,所以最终我们发送了2号信号,进程执行了2号信号的默认处理动作,终止了进程。
信号捕捉
在此之前,我们已经学习了信号发送前和信号发送中的所有内容,接下来,我们就要学习信号发送后的内容。
在上期我们讲过,进程收到信号的时候,可能不会立即去处理信号的动作,而是在合适的时候,那么问题来了,究竟什么时候才是合适的时候呢?我们直接给出结论,进程由内核态转为用户态的时候,进程去处理信号的动作。那么什么是内核态,什么是用户态呢?
内核态:进程处理内核的代码和数据时所处的状态我们称作内核态。
用户态:进程处理用户的代码和数据时所处的状态我们称作用户态。
通过示意图为大家讲解。
每个进程都有自己独有的进程地址空间,然后也有自己独有的用户页表,我们要访问用户的代码时,会通过用户页表加载到进程地址空间中,供进程直接访问,因为和进程直接打交道的是进程地址空间。但是需要注意的是内核页表只有一份,是所有进程共享的,因为内核代码也只有一份。把内核代码通过内核页表加载到进程地址地址空间中的内核空间,供进程去访问。同时,在CPU中有一个CR3控制寄存器,当寄存器中的值为0时为内核态,为1时为用户态。
信号捕捉的流程如下。
大体总共分为6步:在用户态调用系统调用接口->转为内核态执行系统调用接口->执行完系统效用接口时进行信号检测,在三张表中检测信号信息,如果没信号直接切回用户态执行下一行代码->如果有信号,切回用户态执行信号的处理动作->处理完信号后切回内核态执行sys_sigreturn()接口->切回用户态执行下一行代码。
所以大家只需要记住一点,之前所说的合适的时候执行信号的处理动作,这个合适的时候即为进程在内核态执行完系统调用函数返回时的时候,这个时候顺便做了信号检测。
sigaction
sigaction其实也是一个信号捕捉函数与我们之前的signal函数类似,但是signal函数只能用来捕捉非实时信号,sigaction可以既可以捕捉实时信号也可以捕捉非实时信号。这个函数具体怎么使用呢?
参数:第一个参数为要捕捉的信号,第二个参数为输入型参数,为我们设置的新的自定义捕捉对象,第三个参数为输出型参数,为之前的信号捕捉类型。
返回值:调用成功返回0,调用失败返回-1。
可以看到参数中的struct sigaction类型,这个类型是什么呢?
struct sigaciton是一个结构体,结构体的第一个成员为一个函数指针,保存了自定义捕捉函数的地址。第三个参数为sa_mask,为sigset_t类型,所以见名思义,sa_mask就是一个block阻塞信号集,这个信号集用来干啥的,下文会讲到。其它四个变量为捕捉实时信号使用的,我们不予考虑。
代码如下。
#include<stdio.h>
#include<signal.h>
#include<string.h>
#include<unistd.h>void handler(int signo)
{while(1){printf("捕捉到了%d号信号\n",signo);sleep(1);}
}int main()
{//创建了一个自定义捕捉对象struct sigaction sg;//初始化对象空间memset(&sg,0,sizeof(struct sigaction));//绑定自定义捕捉函数sg.sa_handler=handler;//初始化block信号集,全部置0sigemptyset(&sg.sa_mask);//将阻塞信号集中3号信号所对应的位置的置为1,即阻塞三号信号sigaddset(&sg.sa_mask,3);//捕捉二号信号sigaction(2,&sg,NULL);while(1){printf("hahahahahahaahahah\n");sleep(1);}return 0;
}
运行结果如下。
当我们在执行某个信号的捕捉函数时, 当相同的信号再次产生,此时操作系统并不会递达该信号,而是将该信号的阻塞信号集置1,阻塞该信号,阻止该信号,直到上个相同的信号的动作处理完成。所以这个sa_mask的作用就是在运行自定义捕捉函数的时候,如果还想阻止其他信号捕捉函数的执行,也可以将对应的信号的阻塞信号集置为1,阻塞对应的信号,阻止该信号被递达,这便是sa_mask成员的用法。
volatile关键字
代码如下。
#include<stdio.h>
#include<signal.h>int flag=0;
void handler(int signo)
{flag=1;
}int main()
{signal(2,handler);while(!flag);printf("进程正常退出\n");return 0;
}
makefile。我们使用了-大03,进行了编译器优化。
sig:sig.cgcc -o $@ $^ -O3
.PHONY:clean
clean:
设置一个全局变量flag为0,在main函数中设置了一个while循环,因为flag为0,所以为死循环,当发送二号信号时,将flag改为1,所以对于while循环就执行完毕,进程退出。
运行结果如下。
我们发现当我们发送2号信号时,即使将flag置为了1,最终进程还是没有退出。
但是如果我们给fiag这个变量前加入volatile关键字时呢?
volatile int flag=0;
运行结果如下。
此时进程正常退出,符合我们的预期,那么这个volatile关键字的作用到底是什么呢?通过图示为大家讲解。
while循环中是逻辑判断,所以需要CPU的参与,但是整个过程中,在main函数内部,我们发现flag的值并没有进行更改,所以CPU就把flag的值读取到了寄存器中,下个循环再来判断时,就不需要再去内存中读取flag的值,所以就直接读取了寄存器中的flag的值,但是在捕捉到了2号信号之后,在执行自定义方法时,我们发现,flag的值进行了修改,但是CPU始终是在寄存器中进行读取的flag置,所以,flag的值就算内存中改为了1,但是在寄存器中一直却是0,所以main函数中的while循环一直是死循环,所以进程始终无法退出。整个过程可以理解为是编译器的优化,但是volatile关键字阻止了CPU的优化,始终保证了CPU的每次判断都必须从内存中读取flag值,所以在捕捉信号的自定义函数中修改了flag值修改之后, CPU从内存中读取到了修改之后的flag值,flag值为1,在main函数中的while判断时,条件为假,终止循环,进程最终退出了。
综上,volatile关键字的作用为,阻止编译器优化,始终保持CPU对内存数据的可见性。
以上便是信号的所有内容。
本期内容到此结束^_^