如下是信号在内核中的表示示意图:
在这个阶段有以下几种情况:
💢 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
还有一个函数指针表示处理动作:
信号阻塞过程如下:
💦 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?
🎐 Linux的实现:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里
信号阻塞和未决的区别
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
这个类型可以表示每个信号的 “有效” 或 “无效” 状态
💞 阻塞信号集也叫做当前进程的 信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略
🔥 注意:该类型只在 Linux 系统上有效,是 Linux 给用户提供的一个用户级的数据类型
🌈 sigset_t 类型对于每种信号用一个 bit 表示 “有效” 或 “无效” 状态,至于这个类型内部如何存储这些 bit 则依赖于系统实现, 从使用者的角度是不必关心的,使用者只能调用以下函数来操作 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);
注意 : 在使用 sigset_t 类型的变量之前,一定要调用 sigemptyset 或 sigfillset 做初始化,使信号集处于确定的状态。初始化 sigset_t 变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。
调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
参数说明
1) how: 用于指定如何修改信号屏蔽字的操作方式。它可以取以下几个值之一:
2)set:指向一个 sigset_t 类型的信号集,表示需要操作的信号集合。可以使用 sigemptyset()、sigfillset()、sigaddset() 等函数来操作这个集合。
3)oldset: 指向一个 sigset_t 类型的变量,用于保存调用 sigprocmask 前的原始信号屏蔽字(如果 oldset 不为 NULL)。如果不关心原始的屏蔽字,可以将其设置为 NULL。
返回值:成功时,返回 0,失败时,返回 -1,并将 errno 设置为相应的错误代码。
🧃🥝🥥🫒 上面函数使用分析
假设当前的信号屏蔽字为 mask,下表说明了 how 参数的可选值
如果调用 sigprocmask 解除了对当前若干个未决信号的阻塞,则在 sigprocmask 返回前,至少将其中⼀个信号递达。
下面是一个使用 sigprocmask 的简单示例,演示如何阻塞和解除阻塞信号
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include <sys/wait.h>
void handle_signal(int sig) {
printf("Signal %d received\n", sig);
}
int main() {
sigset_t set, oldset;
// 初始化信号集,清空
sigemptyset(&set);
// 将 SIGINT 信号添加到信号集
sigaddset(&set, SIGINT);
// 阻塞 SIGINT 信号
if (sigprocmask(SIG_BLOCK, &set, &oldset) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT blocked, sleeping for 5 seconds...\n");
sleep(5);
// 解除阻塞 SIGINT 信号
if (sigprocmask(SIG_SETMASK, &oldset, NULL) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT unblocked, sleeping for another 5 seconds...\n");
sleep(5);
return 0;
}
(检查pending信号集,获取当前进程pending位图)
#include <signal.h>
int sigpending(sigset_t *set);
打印 pending 表并且实现对 2 号信号的屏蔽
void PrintPending(const sigset_t &pending)
{
std::cout << "curr pending list [" << getpid() << "]" ;
for(int signo = 31; signo > 0; signo--)
{
if(sigismember(&pending, signo))
{
std::cout << 1 ;
}
else
{
std::cout << 0 ;
}
}
std::cout << std::endl;
}
int main()
{
// 1. 对 2 号信号进行屏蔽
sigset_t block, oblock;
sigemptyset(&block);
sigemptyset(&oblock);
// 1.1. 添加 2 号信号
// 我们有没有把 2 号信号的屏蔽,设置进入内核中,只是再用户栈上设置了 block 的位图结构
// 没有设置到内核中
sigaddset(&block, 2);
// 1.2 设置进入内核
sigprocmask(SIG_SETMASK, &block, &oblock);
while(true)
{
// 2. 如何获取 pending 表
sigset_t pending;
sigpending(&pending);
// 2.1 打印
PrintPending(pending);
sleep(3);
}
return 0;
}
运行如下:
解除对 2 号信号的屏蔽,修改如下:
运行如下:
我们发现:后续输出没有了,原因:由于 2 号信号的默认动作是终止进程,一旦解除屏蔽,处理 2 号信号执行默认动作,就把自己干掉了,因此还需要做修改
void non_handler(int signo)
{
std::cout << "处理:" << signo << std::endl;
}
int main()
{
// 忽略 2 号信号的两种方式
// 方式一:系统调用
::signal(2, SIG_IGN);
// 方式二:动态捕捉
::signal(2, non_handler);
// 1. 对 2 号信号进行屏蔽
......
}
运行如下: