赞
踩
上一篇文章讲到信号的是怎样产生的:
我们也介绍了core term
两种默认操作,core在执行信号后会形成一份core文件
(默认是关闭的,因为原本core文件的后缀是pid,运行出错后会创建core文件,导致磁盘空间不足),该文件里存储了出错原因,可以再gdb调试时进行使用。
进程退出会产生status
,里面储存退出信息。其中的core dump位记录是否产生core文件。
通过这样的代码可以获取子进程退出信息:
#include <signal.h> #include <iostream> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <string> #include <cstdio> int sum(int start, int end) { int sum = 0; for (int i = 0; i < end; i++) { sum /= i; sum += i; } return sum; } int main() { pid_t id = fork(); if(id == 0) { sleep(1); //子进程 sum(0 , 100); exit(0); } //父进程 int status = 0; pid_t rid = waitpid(id , &status , 0); if(rid == id) { printf("exit code : %d , exit sig : %d , core dump : %d" , (status >> 8) & 0xFF , status & 0x7F , (status >> 7) & 1); } return 0; }
这五种方式是信号产生的基本方式,上一篇文章我们初步尝试了使用signal
系统调用对信号进行捕捉。今天我们一起来看看信号时如何进行保存。
在认识信号的保存之前,我们先来熟悉几个概念
接下来我们就来介绍三张表,这三张表就能实现阻塞,未决和抵达:
在进程的内核数据结构中会维护三张表:
pending
表是通过位图来储存,一共31位 , 每个比特位代表信号编号,比特位的内容代表信号是否收到!接下来,可以来深入理解一下signal
系统调用了, sighandler_t signal(int signum, sighandler_t handler);
是对信号进行自定义捕捉,进程收到signum
信号后将会执行handler
方法。那么为什么它就可以做到自定义捕捉呢???这就和handler
表有关系了。
hanlder
表是函数指针数组。handler
表中的下标是信号编号,内容是收到对应信号会执行的方法。 sighandler_t signal(int signum, sighandler_t handler);
方法就是将signum
信号对应的方法修改为handler
,这样就完成了自定义捕捉。
block
表也是通过位图来储存(和pending一样)。每个比特位代表信号编号,比特位的内容代表信号是否阻塞!
对于这三张表,我们要横着看,对于 1 号信号:是否阻塞-是否收到-执行方法
。这样通过两张位图和一张指针数组就对于一个信号可以进行完美识别!
再次注意:
对于一个信号要不要进行处理由block
和pending
表来决定,如何执行由handler
表决定!
我们认识了内核数据结构中的三张表,那么如果对它们进行操作呢?Linux操作系统为我们提供了用户级别的位图!:
sigset_t
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集
这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
sigset_t的内部结构类似:
struct bits
{
uint32_t bits[400]; // 400 * 32个比特位
};
如何读取指定位置的比特位呢?进行运算即可
比如第40位:
i = 40 / (sizeof(uint32_t)*8) ;// -> bits[i]第几个数字中
j = 40 % (sizeof(uint32_t)*8) ;// -> bits[i]:j该数字中的第几位
#include <signal.h>
int sigemptyset(sigset_t *set);//清空位图 全部设置为0
int sigfillset(sigset_t *set);//填满位图 全部设置为1
int sigaddset (sigset_t *set, int signo);//把对应位置设置为1
int sigdelset (sigset_t *set, int signo);//将对应位置设置为0
int sigismember(const sigset_t *set, int signo);//查看对应位置是否为1
除了对位图的操作还有:
系统调用sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集block表)
#include <signal.h>
/* Prototype for the glibc wrapper function */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
其中参数:
block表可以修改了,那pending表怎么进行修改呢?其实pending表不需要我们进行修改,信号产生的5种方式都会对进程的pending表进行修改!我们只需要获取pending表就行
#include <signal.h>
int sigpending(sigset_t *set);//获取当前进程的pending位图
接下来我们来做一个实验:
#include <signal.h> #include <iostream> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <string> #include <cstdio> // int sigemptyset(sigset_t *set);//清空位图 全部设置为0 // int sigfillset(sigset_t *set);//填满位图 全部设置为1 // int sigaddset (sigset_t *set, int signo);//把对应位置设置为1 // int sigdelset (sigset_t *set, int signo);//将对应位置设置为0 // int sigismember(const sigset_t *set, int signo);//查看对应位置是否为1 //int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); void PrintPending(sigset_t pending) { std::cout << "cur process :" << getpid() << "pending ->"; for(int signo = 31 ; signo > 0 ; signo--) { if(sigismember(&pending , signo)) std::cout << 1 ; else std::cout << 0; } std::cout << std::endl; sleep(1); } int main() { sigset_t block_set , old_set; sigemptyset(&block_set); sigemptyset(&old_set); sigaddset(&block_set, 2);//将2号信号存入block_set中 //更改进程的block()表 sigprocmask(SIG_BLOCK , &block_set , &old_set);//真正完成修改内核block int cnt = 10; while (true) { //获取当前的pending信号集 sigset_t pending; sigpending(&pending); //打印pending信号集 PrintPending(pending); cnt--; if(cnt == 0) { sigprocmask(SIG_SETMASK , &old_set , &block_set); } } return 0; }
来看:
我们将 2 号信号进行了阻塞,这样向进程发送2号信号就不会被递达!!!会处在pending未决中!!!
并且我们发现当cnt变为0时解除了对2号信号的阻塞,这时候进程就退出了,因为2号信号解除阻塞后,就会执行2号信号的对应动作 — 终止!!!
这就是信号保存的方式!通过三张表来做到对信号的操作是十分的巧妙!!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。