一、信号的保存和处理
阻塞信号
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
注意:之前的捕捉信号操作也被称为信号递达。
下面我们讲解一下信号在内核中的表示:
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号 产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
下面我们解释一下上面所表达的意思:
pending表:位图结构。比特位的位置表示哪一个信号,比特位的内容代表是否收到该信号。
比如:000010 第二个二进制位为1代表这是某个信号,整体的大小代表了是否收到该信号
block表:位图结构。比特位的位置表示哪一个信号,比特位的内容代表是否对应的信号被阻塞。
handler表:函数指针数组,如下图:
这个指针数组里存放的函数指针就是上图这样的,该数组的下标表示信号编号,数组的特定下标的内容表示该信号的递达动作。
下面我们看看信号递达的动作,比如执行和忽略:
int main() { signal(2,SIG_DFL); while (true) { sleep(1); } return 0; }
首先DFL的意思是默认,默认就是执行了意思就是说遇到2号信号就执行,下面我们运行一下:
键盘输入ctrl+c后就执行了2号信号,下面我们查看SIG_DFL的宏:
通过函数定义我们看到这个宏就是用函数指针实现的,下面我们试试忽略信号:
int main() { signal(2,SIG_IGN); while (true) { sleep(1); } return 0; }
IGN是ignore的缩写,就是忽略的意思,就是说我们遇到2号信号就忽略:
代码运行后确实将2号信号忽略了,然后我们用ctrl + \退出程序。
下面我们认识一下sigset_t:
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
认识了信号集后我们学习一下如何用信号集操作函数:
sigset_t 类型对于每种信号用一个 bit 表示 “ 有效 ” 或 “ 无效 ” 状态 , 至于这个类型内部如何存储这些 bit 则依赖于系统实现, 从使用者的角度是不必关心的 , 使用者只能调用以下函数来操作 sigset_ t 变量 , 而不应该对它的内部数据做任何解释, 比如用 printf 直接打印 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 清零 , 表示该信号集不包含任何有效信号。
函数 sigfifillset 初始化 set 所指向的信号集 , 使其中所有信号的对应 bit 置位 , 表示该信号集的有效信号包括系统支持的所有信号。
注意 , 在使用 sigset_ t 类型的变量之前 , 一定要调用 sigemptyset 或 sigfifillset 做初始化 , 使信号集处于确定的状态。初始化sigset_t 变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号
以上这四个函数都是成功返回 0, 出错返回 -1 。 sigismember 是一个布尔函数 , 用于判断一个信号集的有效信号中是否包含某种信号, 若包含则返回 1, 不包含则返回 0, 出错返回 -1 。
下面我们认识一下sigprocmask这个系统调用函数:
调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字 ( 阻塞信号集)。
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值:
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
下面我们演示一下对2号信号进行屏蔽:
void showBlock(sigset_t *oset) { int signo = 1; for (;signo<=31;signo++) { if (sigismember(oset,signo)) { cout<<"1"; } else { cout<<"0"; } } cout<<endl; } int main() { //只是在用户层面上进行设置 sigset_t set,oset; sigemptyset(&set); sigemptyset(&oset); sigaddset(&set,2); //设置进入进程,谁调用,设置谁 sigprocmask(SIG_SETMASK,&set,&oset); //1.2号信号没有反应2.我们看到老的信号屏蔽字block位图是全0 while (true) { showBlock(&oset); sleep(1); } return 0; }
下面我们先讲解一下代码,首先进入main函数我们创建两个对象,因为创建的对象或变量是在栈中存放,所以我们只是在用户层面上进行设置。然后我们先将新的信号集和旧的信号集初始化一下,初始化完成后将2号信号添加到新的信号集上去,这样就相当于屏蔽了2号信号。然后我们设置信号屏蔽字为set所指向的值就是谁调用这个进程谁就将二号信号屏蔽了,然后这个函数返回值返回老的信号屏蔽字,但是由于我们已经将信号屏蔽字初始化了所以老的信号屏蔽字block位图全是0.然后我们写一个死循环去显示老的信号屏蔽字的位图有哪些信号被设置了有哪些信号没有被设置,在这个函数中我们知道一共有31个信号,并且需要判断当前这个信号是否在老的信号集里,如果是就打印1如果不是就打印0,sigismember能判断signo这个信号是否在老的信号集里。
下面我们运行起来:
通过结果我们可以知道与我们所想的是一样的,下面我们修改一下代码将信号屏蔽字取消屏蔽:
int main() { //只是在用户层面上进行设置 sigset_t set,oset; sigemptyset(&set); sigemptyset(&oset); sigaddset(&set,2); //设置进入进程,谁调用,设置谁 sigprocmask(SIG_SETMASK,&set,&oset); //1.2号信号没有反应2.我们看到老的信号屏蔽字block位图是全0 int cnt = 1; while (true) { showBlock(&oset); sleep(1); cnt++; if (cnt==10) { sigprocmask(SIG_SETMASK,&oset,&set); } } return 0; }
我们设置一个计数器让计数器等于10的时候将进程的信号集恢复为oset也就是取消屏蔽2号信号,现象就是一开始我们按ctrl+c是没有反应的,但是到cnt==10的时候2号屏蔽字恢复直接就终止程序了,下面我们看看现象:
我们可以看到现象与我们想的完全一样。
下面我们在认识一个新的接口:
sigpending函数是获取set的pending表,也就是说可以知道哪些信号是未决的,下面我们看看返回值:
如果成功则返回0如果失败返回-1,下面我们用函数重新写一下上面的代码并且引入新现象:
#include <iostream> #include <signal.h> #include <assert.h> #include <unistd.h> using namespace std; static void PrintPending(const sigset_t &pending) { for (int signo=1;signo<=31;signo++) { //sigsimember可以判断signo信号是否在pending中存在 if (sigismember(&pending,signo)) { cout<<"1"; } else { cout<<"0"; } } cout<<endl; } int main() { //1.屏蔽2号信号 sigset_t set,oset; // 1.1初始化 sigemptyset(&set); sigemptyset(&oset); // 1.2将2号信号添加到set中 sigaddset(&set,2); // 1.3将新的信号屏蔽字设置至进程 sigprocmask(SIG_BLOCK,&set,&oset); //2. while获取进程的pending信号集合,并以01打印 while (true) { // 2.1 先获取pending信号集 sigset_t pending; //初始化pending sigemptyset(&pending); int n = sigpending(&pending); assert(n==0); (void)n; //保证在release模式下不会出现编译时的warning // 2.2 打印,方便我们查看 PrintPending(pending); //2.3休眠一下 sleep(1); } return 0; }
这段代码的现象是:因为有block和pending位图,当我们将某个信号block后这个信号就不会被递答了,然后我们给进程发送这个信号,一旦发送那么这个信号在pending表中的比特位就会被修改为1,然后我们就可以看到pending表中2号信号位由0变1的过程。
上面的代码与前面那个演示代码非常相似,并且该注释的我们都注释了,下面我们直接运行起来看看现象:
现象和我们说的一样,当然我们输入ctrl+c也是一样的。因为我们将2号信号进行了屏蔽,即使我们发送了2号信号但是2号信号不会被递达,只能留在pending位图里,下面我们让代码在10秒后恢复被屏蔽的信号并且必须看到pending位图从1变成0:
//2. while获取进程的pending信号集合,并以01打印 int cnt = 0; while (true) { // 2.1 先获取pending信号集 sigset_t pending; //初始化pending sigemptyset(&pending); int n = sigpending(&pending); assert(n==0); (void)n; //保证在release模式下不会出现编译时的warning // 2.2 打印,方便我们查看 PrintPending(pending); //2.3休眠一下 sleep(1); //2.4 10s之后,恢复对所有信号的block动作 if (++cnt==10) { cout<<"解除对2号信号的屏蔽"<<endl; //先打印 sigprocmask(SIG_SETMASK,&oset,nullptr); } }
由于我们在倒计时结束后已经将之前老的信号屏蔽字设置为进程,所以这次我们不需要老的信号屏蔽字了直接设为nullptr即可。下面我们看一下现象:
为什么与我们预期的不一样呢,我们想要看到的pending表呢?这是因为2号信号由阻塞状态修改为解除屏蔽后2号信号直接终止进程了,所以我们是看不到现象的,要看到现象我们需要对2号信号进行捕捉:
static void handler(int signo) { cout<<"拦截到"<<signo<<"信号"<<endl; } signal(2,handler); while (true) { // 2.1 先获取pending信号集 sigset_t pending; //初始化pending sigemptyset(&pending); int n = sigpending(&pending); assert(n==0); (void)n; //保证在release模式下不会出现编译时的warning // 2.2 打印,方便我们查看 PrintPending(pending); //2.3休眠一下 sleep(1); //2.4 10s之后,恢复对所有信号的block动作 if (++cnt==10) { cout<<"解除对2号信号的屏蔽"<<endl; //先打印 sigprocmask(SIG_SETMASK,&oset,nullptr); } }
下面我们将程序运行起来:
可以看到这次的现象就与我们预期的现象一样了,一开始将2号信号进行了阻塞,然后当我们发送2号信号的时候信号保存在pending表中,等10s后解除2号信号的屏蔽了然后我们立即捕捉这个信号,然后循环继续打印pending表,此时2号信号已经递达所以2号信号的位置由1变成0.
总结
以上就是信号的保存的内容,现在我们已经学会了信号的产生,信号的保存,下一篇文章我们将详细介绍信号的处理。