9.2信号(信号的保存、对信号集的处理)

信号的保存

一、信号其他相关概念

  • 实际执行信号的处理动作的过程称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)
  • 进程可以选择阻塞(Block)某个信号
  • 被阻塞的信号产生时,将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
  • 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

二、阻塞信号

在这里插入图片描述

在收到信号到信号处理这个时间窗口内,这个信号是存在但是没有被处理的,所以这个信号需要临时保存下来保存在哪里,怎么保存?保存在进程结构体对应的信号位图中,也就是pending表(成员变量),如果有第几号到达,就把位图的第几位修改为1。

三、block表、pending表、handler表

在Linux系统中,进程的信号处理涉及到几个关键的数据结构,包括block表(信号屏蔽字)、pending表(未决信号集)和handler表(信号处理函数指针数组)。这些表都是以位图的形式实现的,每个信号对应一个比特位。

  1. Block表(信号屏蔽字):用于记录哪些信号被当前进程阻塞。如果某个信号被阻塞,那么即使该信号已经发送给进程,它也不会被立即处理。阻塞的信号会保持未决状态,直到进程解除对该信号的阻塞。
  2. Pending表(未决信号集):用于记录哪些信号已经被发送给进程但尚未被处理。当一个信号产生时,如果该信号没有被阻塞,它会被添加到pending表中,并标记为“有效”(即设置为1)。一旦信号被处理,通常会从pending表中移除或重置其对应的比特位。
  3. Handler表(信号处理函数指针数组):用于存储每个信号对应的处理函数指针。当信号被递达时,操作系统会调用这个表中相应的函数指针来处理信号。

上面的3个数据结构中,block表和pending表都是位图,而handler表是个指针数组,里面保存的是一个一个的函数指针,当哪个信号可以处理,就通过这个表查询执行哪个函数

在这里插入图片描述
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前,这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。
Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号

hander表里的元素可以这样理解,os对一个函数类型重定义,把无返回值、传入一个整型参数的函数类型重定义为hander_t,再用这个类型定义一个函数指针数组,这个数组保存的全是函数的地址,数组的下标就是信号的编号

typedef void (*hander_t)(int);
hander_t hander[32];

3.1pending表(位图)里为1、为0表示的意思

当os收到信号,os就要修改pending(将对应的位图修改为1),这时是保存接受过的信号。未来要处理这个信号时,发现该位图为1,就表明收到过该类型的信号,os就拿着这个信号在位图上的编号,在handler上做索引,以O(1)的方式找到对应的信号处理函数

两个参数SIG_DFL(默认)和SIG_IGN(忽略),让进程忽略某种信号

handler表的前2个元素是SIG_DFL,SIG_IGN

三个相关的宏

typedef void (*__sighandler_t) (int);//定义了函数指针
#define SIG_ERR  ((__sighandler_t) -1)   //返回错误      (语法上是把-1强制转换为了__sighandler_t型,即函数指针型)
#define SIG_DFL  ((__sighandler_t) 0)    //执行信号默认操作  
#define SIG_IGN  ((__sighandler_t) -1)  //忽略信号

SIG_DFL 和 SIG_IGN 的值都是将整数转换为 __sighandler_t 类型的函数指针。

SIG_DFL 的值是将整数 0 转换为 __sighandler_t 类型的函数指针,表示执行信号的默认操作。

SIG_IGN 的值是将整数 -1 转换为 __sighandler_t 类型的函数指针,表示忽略信号。

它们的类型都是 __sighandler_t,即指向接受一个整数参数并返回 void 的函数的指针。

在这里插入图片描述

3.2block表为1、为0表示的意思

block也是一个位图,结构和pending一摸一样,block位图中的内容代表的是对应的信号是否被阻塞(是0就是没有被阻塞,是1代表被阻塞)。

3.3handler表的内容(当成指针数组)

os拿到一个信号编号,通过这个编号在handler上找到对应的函数地址(handler[signum]),然后判断该信号是否等于0或者等于1

在这里插入图片描述

四、block表、pending表、handler表在信号产生到处理信号的联动过程

一个信号被处理,是一个怎样的过程

4.1先记录哪个信号到达

Os向目标进程发送信号,就是修改该进程的pending位图(对应的比特位变为1),这样就记录了曾经有哪个信号到达过但是未被处理的。在合适的时候进入处理信号流程。

在这里插入图片描述

4.2处理信号

首先遍历pending位图,找到比特位为1的几号信号,再返回block位图查找对应比特位是否为1来判断改号信号是否被阻塞,如果为0代表没被阻塞,然后才进入handler寻找对应下标的函数地址,执行对应的信号处理函数

在这里插入图片描述

os自定义的类型,如pid_t,这些类型是为了匹配系统调用接口返回的数据类型。
sigset_t就是os自定义的数据类型

五、sigset_t类型

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

sigset_t就是表示信号集的类型,这是个内核数据结构。

既然有信号集,就得有对这个信号集的操作函数。

六、信号集

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

下列操作函数用于对pending信号集和block信号集的处理

#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);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。

  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系
    统支持的所有信号。

  • 注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的
    状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

七、sigpending系统接口

在这里插入图片描述

sigpending接口,用于检查当前挂起(未决)的信号集,并获取当前进程的pending信号集

int sigpending(sigset_t *set);//接收一个信号集,

参数 set 是一个指向 sigset_t 类型结构的指针,该结构在函数执行完毕后会被填充上当前挂起的信号集。如果函数成功执行,通常会返回0;如果出现错误,会返回-1并设置 errno 来指示具体的错误情况。

7.1未决的概念

在信号处理的术语中,“未决”(pending)指的是一个已经发送给进程但尚未被该进程处理的信号

当一个信号被发送给进程时,它可能会立即被进程捕获并处理,也可能因为进程正忙于其他事情而暂时没有被处理。在这种情况下,信号就会处于“未决”状态,即挂起状态,等待进程准备好处理它。

信号的未决状态持续到以下几种情况之一发生:

  1. 进程主动处理了该信号,通常是通过执行与该信号关联的信号处理函数(signal handler)。
  2. 信号被阻塞(blocked),这意味着进程选择暂时不处理该信号。处于阻塞状态的信号仍然被视为未决,直到它们被解除阻塞并且可以被处理。
  3. 进程接收到了信号的默认行为,例如终止或忽略,这通常会结束未决状态。
  4. 进程调用了 sigpending 或其他系统调用来检查或修改未决信号集。

八、sigprocmask系统调用(用于操作阻塞信号集)

在这里插入图片描述
用来检查并修改被阻塞的信号集(block表)

第一个参数how的可填参数

在这里插入图片描述

SIG_BLOCK参数意思是把集和set里的信号类型添加到block位图(信号屏蔽字)中,相当于mask=mask按位或上set

SIG_UNBLOCK参数意思是从当前信号屏蔽字中接触set集合中含有的信号类型

SIG_SETMASK参数的意思是将set集合的信号类型全部覆盖到block里,相当与产生了一个新的block

第三个参数*oldset的作用

上面第一个how参数所拥有的三个参数都是修改原始信号屏蔽字的,万一未来你想使用原来的没有被修改过的信号屏蔽字,那这个oldset参数就是你用来保存老的信号屏蔽字的变量,oldset是输出型参数

思考

1.如果我们对所有的信号都进行自定义捕捉,我们算不算写了不会被异常或者用户杀掉的进程?

答案是NO,尽管你对所有信号都进行了自定义的信号捕捉,但并不算是编写了一个不会被异常捕获或者用户杀死的进程,os设计者考虑到了这种情况

其中的9号信号是不可以被自定义捕捉的,9号信号属于管理员信号,为的就是处理这种情况。

2.如果我们将2号信号Block,并且不断的获取并打印当前进程的pending信号集,这时候发送2号信号,会不会肉眼看到pending信号集中有1个比特位从0变为1?

验证代码

void catchSig(int signum)
{
   cout << "捕捉到一个信号:该信号是:  " << signum << endl;
}
static void showPending(sigset_t &pending)
{
   for (int sig = 1; sig <= 31; sig++)
       if (sigismember(&pending, sig)) // 判断该号信号是否在pending信号集里
           cout << " 1";
       else
           cout << "0";
   cout << endl;
}
int main()
{
   // 0.方便测试,不要执行2号的默认执行进程
   signal(2, catchSig);
   // 1.定义信号集,由于在函数内定义的变量都在栈上,所以这两个信号集
   // 属于用户空间
   sigset_t bset, boldset;
   sigset_t pending;
   // 2.初始化信号集
   sigemptyset(&bset);
   sigemptyset(&boldset);
   // 3.向集合添加要屏蔽的信号
   sigaddset(&bset, 2);//2号信号设置为阻塞
   // 4.设置set到内核中对应的进程内部【默认情况下进程不会对任何信号屏蔽】
   int n = sigprocmask(SIG_BLOCK, &bset, &boldset);
   assert(n == 0);
   (void)n; // 消除警告
   // 5.重复打印当前进程的pending信号集
   while (true)
   {

       // 获取当前进程的pending 信号集
       sigpending(&pending);
       // 显示pending信号集中没有被递达的信号
       showPending(pending);
       sleep(1);
       count++;
       // 加到20就接触屏蔽
       if (count == 20)
       {
           // 通过使用记录的老的pending表来恢复原始信号集,也可以用SIG_UNBLOCK恢复
           int n = sigprocmask(SIG_SETMASK, &boldset, nullptr);
           assert(n == 0);
           (void)n; // 消除警告
           cout << "解除2号信号的block" << endl;
       }
   }
   return 0;
}

在这里插入图片描述
前20s内,我们把2号信号设置进pending信号集,但是由于我们把2号信号设置也进了block信号集,所以即便收到2号信号,也不会执行2号信号的自定义信号处理函数,只有20s后解除2号信号屏蔽,才开始执行2号的自定义处理函数。

2号信号屏蔽仅仅是把2号信号在block的对应的比特位设置为1,不代表收到了2好信号,没收到2号信号时,对应的pending位图比特位依然是0,收到2号信号后,pending对应的2号信号比特位才会变成1。

因为2号信号前20s是被设置成屏蔽的,向进程发送2号信号,该信号前20s内不会递达(被进程处理相应的动作),只能保存在pending信号集中

解除对2号信号的屏蔽后(代表着进程可以执行2号信号),如果此时pending上2号信号对应的比特位为1,进程就自动会执行2号信号的默认信号处理函数,pending中对应的比特位再从从1变为0

总结就是:当一个信号被发送给进程并且没有被阻塞时,它会出现在pending表中。一旦进程处理了该信号(例如,执行了对应的信号处理函数),通常该信号在pending表中的标记会被清除或重置,表示该信号已经被处理过了。

如果pending位图上对应的2号信号比特位为1,解除屏蔽后会自动执行2号信号的信号处理函数

当一个信号被解除屏蔽时,操作系统会检查pending位图。如果在pending位图上对应的信号比特位为1,这意味着之前有信号在等待处理,但由于被屏蔽而没有被立即递达。一旦解除屏蔽,操作系统会自动将该信号递达至进程,进程随后会调用相应的信号处理函数。

这一机制确保了即使在信号被屏蔽的期间内多次发送了相同的信号,只要pending位图上的对应比特位为1,那么只需解除一次屏蔽,就能触发信号的处理。这样可以避免信号的丢失,并保证信号能够按预期被处理。

3.假设我们对所有信号都block,我们是不是写了一个不会被异常或者用户杀掉的进程?

代码


void blockSig(int sig)
{
    sigset_t bset, boldset;
    sigset_t pending;

    sigemptyset(&bset);
    sigemptyset(&boldset);

    sigaddset(&bset, sig);

    sigprocmask(SIG_BLOCK, &bset, &boldset);
}
int main()
{
    for (int sig = 1; sig < 32; sig++) // 注册所有常用信号处理函数
    {
        blockSig(sig);
    }
    sigset_t pending;
    while (1)
    {
        sigpending(&pending);
        showPending(pending);
        sleep(1);
    }
}

结果
在这里插入图片描述
可以看到,9号信号依然不能被屏蔽,即便你在程序里把9号信号屏蔽了,在执行的时候9号信号不会被真的屏蔽,进程依然能收到9号信号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值