阻塞信号
1. 信号其他相关常见概念
实际执行信号的处理动作称为信号递达(Delivery)
我们知道信号有三种处理方式,默认,忽略,自定义捕捉。但是呢这个动作被称为 递达。
信号从产生到递达之间的状态,称为信号未决(Pending)。
我们从进程接收到信号,然后再到 递达这个过程称为未决。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
2. 在内核中的表示
Pending其实是一个位图,称为:Pending位图
biock和Pending位图一样,代表的含义是对应的信号是否被阻塞。
handler
typedef (*handler_t)(int); handler_t handler[32]; -----函数指针数组---数组的下表就是信号的编号!
信号被处理的大致过程
信号编号signal handler[signal]; (int)handler[signal] == 0;//执行默认动作,done (int)handler[signal] == 1;//执行忽略动作,done handler[signal]();
中间的两个代码
#define SIG_DFL ((__sighandler_t) 0) /*. Default action. */ #define srG_IGN ((__sighandler_t) 1) /* Ignore signal.*/
0号代表的默认动作,1号是忽略。
handler有三个默认信号处理动作,默认,忽略,自定义捕捉。
基本上,语言会给我们提供.h, .hpp &&语言的自定义类型,同时,OS也会给我们捷供.h,和OS自定义的类型。
为什么要OS提供呢???因为要和OS提供的 .h 内部的接口相对应。
当我们通过语言的接口访问硬件的时候,我们是一定要通过操作系统的,在这个过程中,我们所包含的头文件中一定包含着OS提供的类型和.h。只不过我们不知道而已。就以struct FILE为例,里面就包含了sys、fcntl类的头文件等等!!
语言层面会给我们提供很多的数据类型:int、double........
操作系统提供的数据类型 pid_t,key_t、sigset_t........
sigset_t ---(由于是系统提供的类型)不允许用户自己进行位操作--- OS给我们提供了对应的操作位图的方法---
sigset_t --- user是可以直接使用该类型---和用内置类型&&自定义类型没有任何差别
sigset t--—定需要对应的系统接口,}来完成对应的功能,其中系统接口需要的参数,可能就包含了sigset_t定义的变量或者对象
sigset_t
这是一个位图结构
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态
操作函数
#include <signal.h> 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);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有 效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系 统支持的所有信号。
注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的 状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。
sigismember是一个布尔函数,用于判断一个信号集的有效信号中 是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
sigpending
NAME sigpending - examine pending signals SYNOPSIS #include <signal.h> int sigpending(sigset_t *set); RETURN VALUE sigpending() returns 0 on success and -1 on error. In the event of an error, errno is set to indicate the cause.
获取当前调用进程的pending信号集
sigprocmask
NAME sigprocmask - examine and change blocked signals SYNOPSIS #include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); RETURN VALUE sigprocmask() returns 0 on success and -1 on error. In the event of an error, errno is set to indicate the cause.
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
如果oldset是非空指针,则读取进程的当前信号屏蔽字通过old set参数传出。如果set是非空指针,则 更改进程的信 号屏蔽字,参数how指示如何更改。如果oldset和set都是非空指针,则先将原来的信号 屏蔽字备份到oldset里,然后 根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值
三个问题
1.如果我们对所有的信号都进行了自定义捕捉---我们是不是就写了一个不会被异常或者用户杀掉的进程?﹖可以吗?并不是,OS的设计者也考虑了!
这是不对的,如果是这样的话,恶意程序会利用这一点从产生大量的进程致使系统奔溃。
kill -9 这个命令是管理员级别的,无法被阻塞或者自定义捕捉。
通过代码验证
#include <iostream> #include <unistd.h> #include <signal.h> #include <cassert> using namespace std; void catchSig(int signum) { cout<<"我捕捉到了一个信号:"<<signum<<endl; } int main() { for(int sigi = 1;sigi <= 31;++sigi) { signal(sigi,catchSig); } while(true) { sleep(1); } return 0; }
2.如果我们将2号信号block。并且不断的获取并打印当前进程的pending信号集,如果我们突然发送一个2号信号,我们就应该内眼看到pending信号集中,有一个比特位0->1
#include <iostream> #include <unistd.h> #include <signal.h> #include <cassert> #include <sys/types.h> using namespace std; void catchSig(int signum) { cout<<"我捕捉到了一个信号:"<<signum<<endl; } int main() { cout<<"pid "<<getpid()<<endl; // 1. 定义信号集对象 sigset_t bset,boset; sigset_t pending; // 2. 初始化 sigemptyset(&bset); sigemptyset(&boset); sigemptyset(&pending); // 3. 添加要进行屏蔽的信号 for(int sigi = 1;sigi <= 31;++sigi) { sigaddset(&bset, sigi); } // 4. 设置set到内核中对应的进程内部[默认情况进程不会对任何信号进行block] sigprocmask(SIG_BLOCK,&bset,&boset); // 5. 重复打印当前进程的pending信号集 while(true) { // 5.1 获取当前进程的pending信号集 sigpending(&pending); // 5.2 显示pending信号集中的没有被递达的信号 for(int sigi = 1;sigi <= 31; ++sigi) { cout<<sigismember(&pending,sigi); // if(sigismember(&pending,sigi)) // { // cout<< // } } cout<<endl; sleep(1); } while(true) { sleep(1); } return 0; }
3.如果我们对所有的都进行block---我们是不是就写了一个不会被异常或者用户杀掉的进程? ﹖可以吗?
不会的,有些命令是无法被阻塞的。
#include <iostream> #include <unistd.h> #include <signal.h> #include <cassert> #include <sys/types.h> using namespace std; static void showPending(sigset_t& pending) { for(int sigi = 1;sigi <= 31; ++sigi) { if(sigismember(&pending,sigi)) { cout<<"1"; } else { cout<<"0"; } } cout<<endl; } static void blockSig(int signum) { sigset_t bset; sigemptyset(&bset); sigaddset(&bset, signum); sigprocmask(SIG_BLOCK, &bset, nullptr); } int main() { cout<<"pid "<<getpid()<<endl; for(int sigi = 1;sigi <= 31; ++sigi) { blockSig(sigi); } sigset_t pending; while(true) { sigpending(&pending); showPending(pending); sleep(1); } return 0; }
貌似没有一个接口用来设置pending位图(所有的信号发送方式,都是修改pending位图的过程),我们是可以获取的sigpending
信号产生之后,信号可能无法被立即处理,在合适的时候(是什么? )
1.在合适的时候(是什么? )
先来一个合理的推断
信号相关的数据字段都是在进程PCB内部-------->>这是属于内核范畴-------->>操作系统处理信号,肯定是执行操作系统的代码------------->>此时处于内核态
我们执行自己的代码------>>处于用户态
答:在内核态中,从内核态返回用户态的时候,进行信号检测和处理!
我为什么会进入内核态???进行系统调用,缺陷,陷入,异常等等.....
在汇编中,有这么一条指令 int 80,它的作用是让用户态陷入到内核态。我们在上面也说了,系统调用会从用户态进入到内核态。其实呢,可以这么理解 指令 int 80 内置在系统调用函数中。
用户态是一个受管控的状态
内核态是一个操作系统执行自己代码的一个状态具备非常高的优先级
我们学习进程地址空间的时候,知道[3,4]GB的空间是内核地址空间。还知道每个进程单独拥有一个页表,我们以前学习的是用户级页表。其实还有一个内核级的页表。可以这么理解:内核级页表可以被所有的进程都看到。
CPU寄存器2套,一套可见,一套CPU不可见,自用。
CR3寄存器----->>>它里面的某些bite位存储的运行态(其实它是一块空间嘛)-------->>表示当前CPU的执行状态,1内核3用户态(便于理解,1和3)
当我们的代码执行到系统调用时,我们是要进入操作系统进行函数的调用,但是我们是处于用户态,没有权限进行访问地址空间。但是我们的系统调用函数里面是有int 80 这个汇编指令,让我们从用户态进入到内核态。然后CPU在进行状态检测的时候,是内核态,此时我们可以进入[3,4]GB执行操作系统的代码。
46
pidof