阻塞信号
信号其他相关常见概念
实际执行信号的处理动作称为信号递达
信号从产生到递达之间的状态称为信号未决
进程可以选择阻塞某个信号
被阻塞的信号产生之后将保存在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
阻塞和忽略是不同的,信号只要被阻塞就不会递达,忽略本质上就是递达之后选择的一种处理动作
在内核中的表示
每个进程都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作;信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志;如果一个信号没有被产生,并不妨碍它可以先被阻塞
sigset_t
每个信号只有一个比特位,非0即1,不记录该信号产生了多少次,阻塞标志亦是如此;未决和阻塞都可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,此类型可以表示每个信号的有效或无效状态;阻塞信号也称当前进程的信号屏蔽字,屏蔽是阻塞而不是忽略
信号集操作函数
sigset_t类型对于每种信号用一个比特位表示有效或无效,至于类型内部如何存储这些比特位则依赖于系统实现
int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset(sigset_t *set,int signo); int sigdelset(sigset_t *set,int signo); int sigismember(const sigset_t *set,int signo);
函数 sigempty初始化 set所指向的信号集,使其中所有信号对应的比特位清零,表示该信号集不包括任何有效信号
函数 sigfillset初始化 set所指向的信号集,使其中所有信号对应的比特位置为1,表示该信号集的有效信号包括系统所支持的所有信号
函数 sigaddset将信号添加到信号集中;函数 sigdelset将信号从信号集中删除;函数 sigismember判断某种信号是否在信号集中
sigprocmask
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
如果oldset是非空指针,则读取进程当前的信号屏蔽字通过oldset参数传出,如果set是非空指针,则更改进程当前的信号屏蔽字,参数how指示如何更改;如果oldset和set都是非空指针,则将原来的信号屏蔽字备份到oldset中,然后根据set和how参数更改当前的信号屏蔽字
SIG_BLOCK | set包含了所有待添加到信号集中的信号 |
SIG_UNBLOCK | set包含了所有待从信号集中删除的信号 |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值 |
sigpending
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出
默认情况下所有信号都不是阻塞的,如果信号被屏蔽,则该信号不会被递达
代码实现:屏蔽信号2
#include<iostream> #include<signal.h> #include<unistd.h> #define BLOCK_SIGNAL 2 #define MAX_SIGNUM 31 using namespace std; void show_pending(const sigset_t& pending) { for(int signo=MAX_SIGNUM;signo>=1;signo--) { if(sigismember(&pending,signo)) { cout<<"1"; } cout<<"0"; } cout<<"\n"; } int main() { //1.屏蔽指定信号 sigset_t block,oldblock,pending; //1.1初始化 sigemptyset(&block); sigemptyset(&oldblock); sigemptyset(&pending); //1.2添加要屏蔽的信息 sigaddset(&block,BLOCK_SIGNAL); //1.3开始屏蔽 sigprocmask(SIG_SETMASK,&block,&oldblock); //2.遍历打印pending信号集 while(true) { //2.1初始化 sigemptyset(&pending); //2.2获取 sigpending(&pending); //2.3打印 show_pending(pending); //间隔一秒 sleep(1); } return 0; }
捕捉信号
内核如何实现信号的捕捉
操作系统中进程存在两种状态:用户态,内核态;用户态一般会访问操作系统的资源和硬件资源,为达这一目的,必须通过系统提供的系统调用接口,而且执行系统调用的身份必须是内核,为什么用户可以访问系统调用接口呢???
在CPU中存在着许多的寄存器分为:可见寄存器,不可见寄存器,只要是和进程强相关的都是保存着进程的上下文的数据;名为CR3的寄存器保存着当前进程的运行级别:0表示内核态,3表示用户态,在系统调用接口的起始位置,存在着int 80汇编,会将用户态修改为内核态,从而可以以内核态的身份访问系统调用接口
进程以内核身份访问系统调用接口的具体过程又是怎么样的呢???
在之前进程空间中学习过,进程空间包括用户空间和内核空间,系统调用接口就与这内核空间有关:每个进程都有自己的进程空间,用户空间独享,内核空间只有一份,也就是说所有进程共享同一份内核空间;当进程访问接口时,只需要在进程空间上进行跳转即可,就类似与动态库加载到进程空间
图解:
当开机时,操作系统会从磁盘加载到内存中的内核区中,当进程以内核态身份访问系统调用时,会在进程空间中跳转到内核空间通过内核级页表映射到内存中操作系统完成相应的调用,完毕之后再跳转回用户空间
信号在产生时,并不会被立刻处理,而是在合适的时间被操作系统处理,这个合适的时间就是从内核态返回用户态时;所以,进程在进程切换或者系统调用时先进入内核态,在内核态中进行信号检测,也就是进程中的两个标志位(pending/block)和函数指针(handler*):如果信号未决且未被阻塞,查找函数指针是否有对应的自定义处理方法,若有,将进程内核态身份修改为用户身份完成对应的处理方法,再还原为内核身份,完成剩余的系统调用,待系统调用结束后,最后将身份修改为用户态继续执行后续的代码
图解:
需要注意的是:不能以内核态的身份执行用户态的代码,因为操作系统不相信任何人,以免有人进行恶意破坏
sigaction
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
signum:待捕获的信号编号
act:结构体指针
其中包含处理方法sa_handler和信号集sa_mask
举个栗子,通过此函数捕获2号信号,捕获信号后休息20秒,多次向进程发送2号信号,观察进程运行结果
void Count(int cnt) { while(cnt) { printf("cnt:%2d\r",cnt); fflush(stdout); cnt--; sleep(1); } printf("\n"); } void handler(int signo) { cout<<"get a signo"<<signo<<"正在处理..."<<endl; Count(20); } int main() { struct sigaction act,oldact; act.sa_handler=handler; act.sa_flags=0; sigemptyset(&act.sa_mask); sigaction(SIGINT,&act,&oldact); while(true) sleep(1); return 0; }
虽然向进程发送多次同一种信号,但是进程只捕获了两次,因为当进程正在递达某一个信号时,将同种类型的信号是无法被抵达的,系统会自动将当前信号添加到屏蔽字中,将pending位图中该信号所在位置修改为0,再次发送同一信号,会将pending位图中该信号所在位置修改为1;当进程完成信号的递达时,系统会自动解除对该信号的屏蔽,所以系统会立即递达pending位图中的信号,也就是捕获第二次信号
当我们正在处理2号信号时,还想屏蔽3号信号,此时只需要将3号信号加入sa_mask信号集即可
void Count(int cnt) { while(cnt) { printf("cnt:%2d\r",cnt); fflush(stdout); cnt--; sleep(1); } printf("\n"); } void handler(int signo) { cout<<"get a signo"<<signo<<"正在处理..."<<endl; Count(20); } int main() { struct sigaction act,oldact; act.sa_handler=handler; act.sa_flags=0; sigemptyset(&act.sa_mask); //添加3号信号 sigaddset(&act.sa_mask,3); sigaction(SIGINT,&act,&oldact); while(true) sleep(1); return 0; }
和上面有所不同的是,这里的进程在最后直接退出了,其实是因为,在2号信号被屏蔽后,再次执行2号信号,最后执行3号信号结束进程
进程处理信号的原则是串行处理同类型的信号,不允许递归