目录
信号集和信号屏蔽字
信号集
信号屏蔽字
信号位操作函数
sigemptyset
sigaddset
sigismember
sigprocmask
sigpending
手动操作让2号信号屏蔽打印pending
信号处理函数sigaction
我们继续来学习信号的捕捉
信号集和信号屏蔽字
信号集
信号集是存储一组信号的集合,本质也是一个位图,信号集可以存储未决信号,屏蔽信号,所以信号屏蔽字是信号集的一种,我们可以自己创建一个信号集,其类型为sigset_t,sigset_t类型是系统内置类型。
信号屏蔽字
信号屏蔽字(Signal Mask)是 进程级 的一个 位掩码,用于指定 哪些信号应该被阻塞(屏蔽),即这些信号在进程解除屏蔽之前不会被递送到进程。每个进程都有一个 信号屏蔽字,它是一个 位图(bitmask),每一位对应一个 信号(如 SIGINT
、SIGTERM
等):
信号屏蔽字是进程(或线程)的一个状态变量,它决定了当前进程(或线程)正在屏蔽哪些信号。其本质是位图,注意信号屏蔽字就是 Block 表(阻塞信号集),block表的另一种说法是信号屏蔽字。所以说我们创建信号屏蔽字本质不就是创建一个信号集吗。
对进程信号捕捉机制进行操着无非就是操作那三张表,无非就是在改位图的比特位,但是我们不建议直接进行位操作的修改,所以Linux内置了不少的函数帮助我们完成这个事。
信号位操作函数
sigemptyset
sigemptyset
是一个 POSIX 信号处理相关的函数,它用于初始化信号集(sigset_t
),并将其清空,即不包含任何信号。
我们如果自己创建一个新的信号集,这时如果只是单纯的sigset_t set,这时信号集set往往是乱码的,不能直接用,我们就需要调用这个函数将其所以的bit位都制成0。
参数是一个指针。返回值如上:
sigaddset
sigaddset
是一个 POSIX 信号处理相关的函数,用于向信号集 (sigset_t
) 中添加一个指定的信号。
通过sigaddset添加的信号编号signum,它会修改内部的比特位表示,使得对应信号的比特位变为 1(表示该信号已被添加),是一个由0置1的过程
sigismember
sigismember
是一个 POSIX 信号处理函数,用于检查某个信号是否在指定的 sigset_t
信号集中。
我们值得关注一下这个返回值。
上面这几个是本次试验要用到的函数,我们操作信号主要还是修改进程中的pending表和block表,有没有直接可以对着这两个表进行操作的函数。
sigprocmask
sigprocmask
是 POSIX 提供的信号管理函数,用于修改或查询进程的信号屏蔽字(signal mask),即进程当前阻塞的信号集。被屏蔽的信号不会被进程接收,直到解除屏蔽。
就是操作block表的,具体这么操作取决于此函数的第一个参数how的传参。
how有三种传参方式如上,也就是说可以直接用我们自己创建的信号屏蔽字取代进程中的block。
第二个参数set是输入型参数,第三个参数oldset是输出型参数,用于存放调用前进程的信号屏蔽字。
sigpending
sigpending
用于获取当前进程被阻塞但未被递达的信号(即挂起的信号),就是获取进程的pending表。
这个set参数是输出型参数。
为什么没有修改和替换等的操作,我们每次发信号不就在修改pending表吗,OS接收到信号pending表对应位置不就置1了吗,在 Linux 及 POSIX 规范中,进程的 挂起信号集(pending
)是内核自动管理的,用户态程序不能直接修改它。这是出于信号处理的设计原则和系统安全性的考虑,记得9号信号是不能删除/替代/修改的,如果pending表可以让人随便修改,那不就违背了9号信号的原则了吗?
手动操作让2号信号屏蔽打印pending
我们接着的操作就是运用上面的函数,先屏蔽2号信号,然后获取pending表,只打印其中的32位,接着发送2号信号就可以看到2号信号pending效果,为什么要先屏蔽呢,我们不能让2号信号使得程序终止。
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void PrintPending(const sigset_t& pending)
{cout << "curr pending list [" << getpid() << "]: ";for (int i = 31; i > 0; i--){//判断pending表中是否存在此编号的信号if (sigismember(&pending, i)){cout << 1;}else if (sigismember(&pending, i) == 0){cout << 0;}//忽略对i无效的情况}cout << endl;
}
int main()
{//1.屏蔽2号信号sigset_t block;sigset_t oblock;//初始化sigemptyset(&block);sigemptyset(&oblock);//添加2号信号进block,只是添加还没有设置进内核sigaddset(&block, 2);//设置进内核sigprocmask(SIG_SETMASK, &block, &oblock);while (true){//循环获取并打印内核的pending表sigset_t pending;sigpending(&pending);//打印pending表的前32位(只关注32位信号)PrintPending(pending);sleep(1);}return 0;
}
这里sigprocmask的how可以使用上面的直接替换也可以使用SIG_BLOCK添加。至此我们就完成了先写入2号信号,然后内核屏蔽,打印pending的过程。
可以看到发送2号信号之后pending表果然有记录了(由0变1),为什么没有退出呢,因为block表里面已经事先屏蔽了该信号。
接着我们体验2号信号从屏蔽到接触的过程,接触2号信号只需要再次使用sigprocmask将保存原有block表的oblock替代之前的就可以了。
//设置进内核sigprocmask(SIG_SETMASK, &block, &oblock);int cnt = 0;while (true){//循环获取并打印内核的pending表sigset_t pending;sigpending(&pending);//打印pending表的前32位(只关注32位信号)PrintPending(pending);sleep(1);cnt++;if (cnt == 15){cout << "解除对2号信号的屏蔽" << endl;//我们这次不关心旧的block表的值sigprocmask(SIG_SETMASK, &oblock, nullptr);}}return 0;
}
我们一旦解除了,2号信号一递达进程就被退出了,所以就停止了!!!
信号处理函数sigaction
sigaction
是 Linux 中用来 处理信号(signal) 的系统调用,它比 signal()
更加强大和可靠。
也就是说sigaction具有和signal相同的功能,就是更改handler特定数组下标里面的内容。
和sigpromask的结构有点像,对一个编号为signum的信号进行act形式的捕捉,返回原有的捕捉方法oldact,这个act和oldact的类型是一个结构体,名字和函数名一样。
我们可以看到,sa_sigaction这个是用来捕捉实时信号的,我们不用管,上面这个就是signal同款的捕捉方式函数,同样的使用函数名传参(回调函数),sa_flag不用管,sa_mask是自定义屏蔽信号的位图,就是想屏蔽更多信号就sigaddset到这里。
sa_mask
里添加的信号会在 信号处理函数运行时 被 自动屏蔽,防止嵌套执行,所以sigaddset添加进入sa_mask的信号就不需要再sigprocmask进内核了,算是一种自动添加到block表的行为
不过:
sigaction
里的 sa_mask
作用是:
当 信号(
SIGINT
)被触发时(某种行为如Ctrl+C
),handler()
运行的这段时间,sa_mask
里的信号会自动被屏蔽,直到handler()
结束。所以这种屏蔽是暂时的,当信号没有被捕捉或者处理完了不调用handler了就不生效了。
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;void Printblock()
{sigset_t set;sigset_t olset;sigemptyset(&set);sigemptyset(&olset);sigprocmask(SIG_BLOCK, &set, &olset);for (int i = 31; i >= 0; i--){if (sigismember(&olset, i)){cout << 1;}else{cout << 0;}}cout << endl;
}
void handler(int signo)
{while (1){cout << "get a sig: " << signo << endl;Printblock();sleep(1);}
}int main()
{struct sigaction act, oact;act.sa_handler = handler;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask, 3);sigaddset(&act.sa_mask, 4);sigaddset(&act.sa_mask, 5);sigaddset(&act.sa_mask, 6);::sigaction(2, &act, &oact);while (true){Printblock();//等待信号到来pause();}
}
可以看到确实如此!!!
当handler方法正在被处理时,此时2号信号的pending表一定是1,但是如果假设handler处理时间很久并且有指令调用系统调用使CPU陷入内核,此时又来了很多个2号信号,OS不允许信号处理方法进行嵌套,当某个信号正在被处理时又来了这个信号,操作系统就不会将pending表其再置1了,转而将block表的对应位置1表示阻塞,等信息处理完自动解除阻塞状态。
pending表由1->0的过程表示信号被处理完了,这个过程是在调用handler表的处理函数之前就置0了。