还记得我们上面讲过的 sigset_t 类型吗,sigset_t 类型对于每种信号用一个 bit 表示 “有效” 或 “无效” 状态,至于这个类型内部如何存储这些 bit 则依赖于系统实现,从使用者的角度是不必关心的,因为不同的操作系统可能对 sigset_t 变量的定义不一样,有的可能是变量,有的可能是数组,有的可能是封装在结构体内等等。
使用者只能调用以下函数来操作 sigset_t 变量,而不应该对它的内部数据做任何解释,比如用 printf 直接打印 sigset_t 变量是没有意义的!
#include <signal.h>
int sigemptyset(sigset_t *set); // 将set给定的信号集初始化为0
int sigfillset(sigset_t *set); // 将set初始化为full,也就是初始化所有信号为1
int sigaddset (sigset_t *set, int signo); // 在set中添加signo信号
int sigdelset(sigset_t *set, int signo); // 在set中删除signo信号
int sigismember(const sigset_t *set, int signo); // 检测signo信号是否存在set中
// 返回值:前四个函数都是成功返回0,失败返回-1。
// 而sigismember()是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。sigemptyset()初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。sigfifillset()初始化set所指向的信号集,使其中所有信号的对应bit置为1,表示该信号集的有效信号包括系统支持的所有信号。
💥💥注意:在使用 sigset_ t 类型的变量之前,一定要调用 sigemptyset() 或 sigfifillset() 做初始化,使信号集处于确定的状态。初始化 sigset_t 变量之后就可以再调用 sigaddset() 和 sigdelset() 在该信号集中添加或删除某种有效信号。
Ⅱ. sigprocmask(阻塞信号集)
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
// 作用:可以读取或更改进程的信号屏蔽字(阻塞信号集)
// 返回值:若成功则为0,若出错则为-1,并且设置错误码errno- 如果
oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出。 - 如果
set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。 - 如果
oldset和set都是非空指针,则先将原来的信号屏蔽字备份到oldset里,然后根据set和how参数更改信号屏蔽字。
假设当前的信号屏蔽字(阻塞信号集)为 mask,下表说明了 how 参数的可选值:
如果调用 sigprocmask() 解除了对当前若干个未决信号的阻塞,则在 sigprocmask() 返回前,至少将其中一个信号递达。
Ⅲ. sigpending(未决信号集)
#include <signal.h>
int sigpending(sigset_t *set);
// 作用:读取当前进程的未决信号集,通过set参数传出
// 返回值:若成功则为0,若出错则为-1,并且设置错误码errno
// 参数:set为要读取的信号集 这个相对 sigprocmask() 来说会更加简单一点,其仅仅只能读取未决信号集而已,而无法进行修改!
Ⅳ. 接口使用代码
下面我们写一段代码,我们来通过上面这些接口达到这个目的:定义我们自己的 sigset_t 类型变量,并且通过添加信号、删除信号,然后我们将每次这个变量的状态也就是比特位都打印出来,观察一下变化:
#include <iostream>
#include <signal.h>
#include <vector>
#include <unistd.h>
using namespace std;
static vector<int> catchSig{2, 3}; // 要阻塞的信号编号数组
// 自定义捕捉函数
void myhandler(int signo)
{
cout << "捕捉信号,编号为:" << signo << endl;
}
// 打印pending位图
static void show_pending(sigset_t& pending)
{
// 注意信号编号是从1开始的
for(int i = 31; i >= 1; --i)
{
// 判断该信号是否为未决信号
if(sigismember(&pending, i))
cout << "1";
else
cout << "0";
}
cout << endl;
}
int main()
{
// 自定义捕捉信号
// for(const auto e : catchSig)
// signal(e, myhandler);
// 1.先屏蔽指定的信号
// 1.1 定义阻塞信号集
sigset_t block, oldblock;
// 1.2 初始化阻塞信号集
sigemptyset(&block);
sigemptyset(&oldblock);
// 1.3 添加要屏蔽的信号
for(const auto e : catchSig)
sigaddset(&block, e);
// 1.4 更改当前进程的阻塞信号集(这一步才是真正的设置到内核,上面都只是用户级别操作)
sigprocmask(SIG_SETMASK, &block, &oldblock);
// 2.打印pending信号集观察
// 2.1 初始化pending信号集
sigset_t pending;
sigemptyset(&pending);
int cnt = 10;
while(true)
{
// 2.2 获取进程的pending到我们的pending变量
sigpending(&pending);
// 2.3 打印
show_pending(pending);
sleep(1);
// 十秒到后我们将其阻塞去掉
if(cnt-- == 0)
{
// 我们可以通过sigdelset,也可以像下面这样子来将阻塞去掉
sigprocmask(SIG_SETMASK, &oldblock, &block);
cout << "阻塞去掉,信号被捕捉" << endl;
}
}
return 0;
} 来解释一下上面这些代码:首先我们要用 sigprocmask() 函数来修改当前的进程阻塞信号集的话,那我们就得自己有 sigset_t 类型的变量用于传参,并且要有两个,方便我们下面的去除阻塞!
接着我们对两个阻塞信号集调用 sigemptyset() 进行初始化为全 0,然后将我们要阻塞的信号编号添加到 block 变量中,接着就是更改当前进程的阻塞信号集了(这一步才是真正的设置到内核,上面都只是用户级别操作),通过 sigprocmask() 函数,并且我们这里使用的第一个参数 how 是 SIG_SETMASK,也就是我们传的 block 有什么信号,就将进程的阻塞信号集改为和 block 一样!这样子一来我们就设置好了阻塞信号集。
然后我们循环打印一下 pending 位图,观察其变化。首先还是一样,需要定义一个我们自己的 sigset_t 类型的 pending 变量并初始化一下,然后在循环每次将当前进程的 pending 通过 sigpending() 读取到我们的自己的 pending 变量中,然后通过 show_pending() 函数,将 31 个信号编号对应如果存在的话就打印 1,不存在打印 0,这样子就能打印出位图的效果,这是通过 sigismember() 来实现判断是否某个编号存在 pending 中的!
首先我们先不自定义捕捉信号,也就是按照对应的信号的默认处理方式来测试一下:
可以看到一旦十秒到了,程序去调用 sigprocmask(SIG_SETMASK, &oldblock, &block) 去掉阻塞,那么进程也会立马去捕捉到该信号,也就是二号信号,对应的就会直接终止进程!
下面我们放开自定义捕捉信号的注释,看看结果有什么不一样(注意我们在自定义捕捉函数中不进行退出!):
我们可以看到捕捉完信号之后,我们的 pending 变量对应的信号编号也都变为了 0 !
那么还是那个问题,既然我们可以自定义捕捉信号并且让它们不退出,那么我们将所有的信号编号都自定义捕捉并且不退出,是不是就永远都杀不死这些进程了呢❓❓❓
答案肯定还是不行的,操作系统不会让我们这么做的,即使做了,操作系统也会保留一些指令比如九号信号等指令的默认动作的有效性!
⚜️sigaction(捕捉信号)
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
// 作用:读取或修改与指定信号相关联的处理动作。
// 返回值:成功返回0,失败返回-1,并且设置错误码errno参数:
signum:标识信号,可以是任意合法的信号除了SIGKILL和SIGSTOPact:若act指针非空,则根据act修改该信号的处理动作oldact:若oldact指针非空,则通过oldact传出该信号原来的处理动作
这个 act 和 oldact 就相当于我们上面讲的阻塞信号集中的 set 和 oldset,其中 set 用来传递修改阻塞集,而 oldset 用来保存原来的进程中的阻塞集;act 和 oldact 也是类似的道理!
其中 act 和 oldact 的结构体如下:
struct sigaction {
void (*sa_handler)(int); // ⚜️我们传递的处理动作:默认、忽略、自定义
void (*sa_sigaction)(int, siginfo_t *, void *); // 用来处理实时信号,这里我们不讲,直接设为nullptr
sigset_t sa_mask; // ⚜️标识阻塞信号集
int sa_flags; // sa_flags指定一组修改信号行为的标志。我们这里设为0即可
void (*sa_restorer)(void); // sa_restorer元素已过时,不应使用。POSIX未指定sa_restorer元素。直接设为nullptr
}; 我们这主要围绕 sa_handler 和 sa_mask 来展开讲解这个函数!
💥💥💥还要注意的是,这个结构体的名字和我们使用的这个函数名是一样的,但是却是完全不同的东西!
#include <iostream>
#include <signal.h>
using namespace std;
void myhandler(int signo)
{
cout << "catch a signal, the signo is : " << signo << endl;
}
int main()
{
// 初始化sigaction结构体
struct sigaction act, oldact;
act.sa_flags = 0;
act.sa_handler = myhandler; // 自定义捕捉
sigemptyset(&act.sa_mask); // 将阻塞信号集初始化
sigaction(SIGINT, &act, &oldact);
while(true);
return 0;
}
可以很明显发现,这个函数我们在使用的时候,和 signal 函数不就是一模一样的吗❓❓❓
是的,但是 signal 函数的功能比较简单,而 sigaction 函数一般还能处理实时信号等功能!
Ⅴ. 测试sigaction的一些场景
假设我们使用自定义捕捉信号函数,这个函数中处理耗时比较长,比如我们给它一句代码:sleep(20); 如果这个时候又来了许多相同信号,那么会发生什么呢,会不会发生不断的调用该自定义函数而造成递归呢❓❓❓
下面我们一起来看一下:
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <signal.h>
using namespace std;
void count(int n)
{
while(n >= 0)
{
printf("second : %2d\r", n--); // 倒计时一样,只在当前行刷新
fflush(stdout);
sleep(1);
}
cout << endl;
}
void myhandler(int signo)
{
cout << "catch a signal, the signo is : " << signo << endl;
count(20); // 倒计时20秒
}
int main()
{
// 利用sigaction自定义捕捉函数
struct sigaction act, oldact;
act.sa_flags = 0;
act.sa_handler = myhandler; // 自定义捕捉
sigemptyset(&act.sa_mask); // 将阻塞信号集初始化
sigaction(SIGINT, &act, &oldact);
while(true);
return 0;
}
是不是很奇怪,为什么我们发送那么多信号,只执行了两次,而且为何偏偏是两次❓❓❓下面我们分别来解释一下:
其实这和我们学过的 pending 和 block 信号集是有关系的!因为信号集其实本质是一个位图结构,也就是通过一个比特位来识别是否有信号或者是否阻塞信号,那为什么当我们进行正在递达某个信号期间,同类型的信号无法被递达呢 --- 其实是因为当当前信号正在被捕捉的时候,OS 会自动将当前信号加入到进程的信号屏蔽字也就是阻塞信号集中,所以此时如果还有同种信号发送过来的话,那么是会被阻塞的!
而当信号完成捕捉动作之后,OS 又会自动解除对该信号的阻塞,但是要注意,为什么同种信号只被执行了两次呢 --- 这是因为虽然当前信号正在被捕捉的时候会被加入到阻塞信号集,但是其 pending 位图中的标识位其实会被清零(因为已经被递达了),接下来又过来了一个同种信号,那么 pending 位图又将其置为 1,只不过此时当前信号还在捕捉过程,所以是阻塞的,所以当捕捉完毕之后,block 位图会将当前信号解除阻塞,OS 去检查 pending 位图的时候发现同种信号被置 1,所以会再执行一次捕捉处理,并重新将 pending 位图置 1!
由于 pending 位图对于非实时信号来说,只能标识一个,所以我们可以看到后面其它信号的话都没有被处理,只处理了两个,这就是整个流程!
由此我们可以得到结论:进程处理信号的原则是串行的处理同类型的信号,不允许发生递归处理!
如果我们正在处理一个信号的时候,也想顺便屏蔽其它信号,就可以添加到上述我们讲的 sa_mask 信号集中:
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <signal.h>
using namespace std;
void count(int n)
{
while(n >= 0)
{
printf("second : %2d\r", n--);
fflush(stdout);
sleep(1);
}
cout << endl;
}
void myhandler(int signo)
{
cout << "catch a signal, the signo is : " << signo << endl;
count(20);
}
int main()
{
struct sigaction act, oldact;
act.sa_flags = 0;
act.sa_handler = myhandler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT); // 将三号信号添加到屏蔽集中
sigaction(SIGINT, &act, &oldact);
while(true);
return 0;
}
可以发现,我们将二号信号调用之后,三号信号也会被顺带的加入到阻塞信号集中,所以此时就无法捕捉三号信号,直到二号信号捕捉完成,三号信号才能被递达!