【Linux庖丁解牛】— 保存信号!
1. 信号其他相关常见概念
• 实际执行信号的处理动作称为信号递达(Delivery)
• 信号从产生到递达之间的状态,称为信号未决(Pending)。
• 进程可以选择阻塞 (Block )某个信号。
• 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
• 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的⼀种处理动 作。
我们光知道这些概念还不够,我们接下来需要知道这些概念在系统内核中的具体体现,接下来我们来看看进程PCB中的三张表:
我们首先来看到pending表,这张表实际上就是unsigned int pending,就是一张位图【比特位的位置表示信号的编号,内容表示是否收到信号】,这张表的本质作用就是记录进程收到的信号。
我们再来看到第二张表block表,这张表实际上也是unsigned int block,也是一张位图【比特位的位置表示信号的编号,内容表示是信号是否阻塞】。pending表和block表就可以决定一个进程的某个信号是否收到,是否阻塞,是否未决,是否递达。
最后我们看到第三张表handler表,该表本质上是函数指针数组,数组的类型为sighandler_t*,还记得我们的自定义信号处理方法吗:
该表的下标记录信号的编号,内容就是信号所对应的处理方式。所以:自定义信号处理方式的本质就是修改handler表!!
> 我们再来认识两个宏:SIG_IGN【ignore】 & SIG_DEL【default】
很好理解,SIG_IGN就是忽略处理的意思,ING_DEL就是恢复默认处理的意思。
下面简单写个代码使用一下:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <functional>
#include <vector>void hander(int sig)
{std::cout<<"\nhello sig->"<<sig<<std::endl;signal(sig,SIG_DFL);std::cout<<"恢复默认处理动作\n";
}int main()
{signal(2,hander);//忽略处理// signal(2,SIG_IGN);while(true){printf("haha\n");sleep(1);}return 0;
}
2. 信号集及其操作函数
2.1 sigset_t
从上图来看,每个信号只有⼀个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型 可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号 是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽” 应该理解为阻塞而不是忽略。
2.2 信号集操作函数
sigset_t类型对于每种信号用⼀个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些 bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印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);
• 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不含任何有效信号。
• 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号 包括系统支持的所有信号。
• 注意,在使用sigset_t类型的变量之前,⼀定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。sigismember是⼀个布尔函数,用于判断⼀个信号集的有效信 号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
> sigprocmask & sigpending
调用函数sigprocmask可以读取或更改进程的信号屏蔽字【阻塞信号集】。
第一个参数可以传以下三个参数:
第二个参数传我们设置的信号集即可,第三个参数是输出型参数,获取旧的信号集。
下面看一下sigpending
该函数的作用就是读取当前进程的未决信号集,set即输出型参数。
> demon代码
接下来我们来写一个demon代码来将上面的函数使用一下,先说一下带代码的具体逻辑:先把当前进程的2号信号屏蔽,然后把pending位图循环打印输出,我们不断发送信号就可以看到pending的变化。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <functional>
#include <vector>void Print(sigset_t &pending)
{printf("我是一个进程(0x%0x),pending:", getpid());for (int i = 31; i >= 1; i--){if (sigismember(&pending, i)){std::cout << 1;}elsestd::cout << 0;}printf("\n");
}int main()
{// 屏蔽信号sigset_t block;sigset_t old_block;// 初始化sigemptyset(&block);sigemptyset(&old_block);// 将2号信号设置进block中sigaddset(&block, 2);// 完成屏蔽sigprocmask(SIG_SETMASK, &block, &old_block);while (true){sigset_t pending;sigpending(&pending);Print(pending);sleep(1);}return 0;
}
现在,如果我们将所有的信号屏蔽,那么所有信号不都是无法被递达了吗??那这个进程不就是无法被杀掉了吗??其实不然,9号信号既无法被捕捉,也无法被屏蔽!!
现在,我们基于以上代码10秒解除对2号信号的屏蔽,然后我们希望看到2号信号被递达:
这里还有一个问题:解除对2号信号的屏蔽之后,一旦2号信号被递达,那么pending是被立即修改【在处理动作完成之前】,还是在处理动作完成之后呢??做个实验便可得出结论了
预期结果:如果在捕捉动作中打印的pending 全为0,则pending是被立即修改【在处理动作完成之前】,否则,pending在处理动作完成之后修改。
3. core VS term
进程收到信号退出的方式有core和term两种,但这两种方式有什么区别呢??
core是核心意思,一旦进程以core方式退出,那么系统就会在当前路径下形成一个文件。进程异常退出的时候,会将进程在内存中存储的核心数据拷贝到磁盘中形成一个文件!!之后再进程退出,我们将这种机制叫做核心转储用来支持调试debug。而term就是直接进程退出。
我们之所以之前没有看到这个形成文件的行为,是因为在我们云服务器上默认是关闭这个机制的【原因:……】。
现在我们打开:
不过,为什么要有核心转储机制呢??
主要是为了支持debug调试,当我们的程序崩溃时,可以使用gdb命令中的core-file core快速定位到崩溃的代码!!!
说到这里,不知道各位是否还记得这张图:
0表示进程正常退出,那是因为我们的信号只有1~31,而没有0号信号!!低7位记录信号,第八位记录是否core dump。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <functional>
#include <vector>
#include <wait.h>int main()
{pid_t id = fork();if (id == 0){printf("hahaha\n");printf("hahaha\n");printf("hahaha\n");printf("hahaha\n");int a = 10;a /= 0;printf("hahaha\n");printf("hahaha\n");printf("hahaha\n");}int status=0;int n=waitpid(id,&status,0);printf("signal->%d exit_code->%d core_dump->%d\n",status&0x7F,(status>>8)&0xFF,(status>>7)&0x1);return 0;
}