信号(三)【信号保存】-2. 信号的保存

  • 为什么要保存信号

    因为当操作系统向进程发送信号时,可能此时进程正在执行着更重要的任务,无法立即处理信号。对于操作系统而言,它并不知道、也无法知道进程何时能够立即处理信号;对于进程而言,信号与进程的执行是异步的,所以进程也无法预测何时会收到信号。所以因为种种原因,进程可能无法立即处理信号,因此在收到信号、到信号被处理这段时间窗口,就需要将信号保存起来。至于为什么采用位图来保存信号,仅仅是因为收到的是普通信号。

  • 普通信号与实时信号的区别

    我们又说过,进程收到信号后可能不会立即处理,所以可能进程还没处理已经收到的信号,操作系统又发来了几次信号,但是即便操作系统发了 100 次信号,而因为普通信号采用位图这样的数据结构来存储信号,每个信号对应一个比特位,比特位的结果就两种,要么有,要么没有,没办法记录有多少个的问题。所以无论收到多少次信号,进程都只会当作一次信号来处理,这是使用位图结构所必然的。

    而对实时信号,收到那一刻就必须立刻马上处理,不能等待。如果某个实时信号发了 10 次,那么进程就必须处理 10 次。实时信号在我们日常的均衡式操作系统基本用不到,更多的是类似与车载系统,一踩刹车就立刻需要给对应的硬件发送信号,然后制动车辆,还有类似智驾躲避障碍物,当识别到障碍物,然后向系统发送躲避信号,智驾系统立刻马上就必须去执行处理这个信号。

2.1 内核中的表示

  • 实际执行信号的处理动作称为信号递达(Delivery)。
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态(即保存在 pending 表中),直到进程解除对此信号的阻塞,才执行递达的动作(即处理信号)。换言之,进程阻塞了某个信号,不代表不能向该进程发送该信号。
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

在这里插入图片描述
其中的 handler 表,即函数指针数组,指向对应信号的处理方法,可以指向 SIG_DFL(表默认处理动作) 或者 SIG_IGN(表忽略),也可以指向用户自定义的处理动作,即根据信号编号索引到对应下标,修改其内部的函数指针即可。 因此,当用户调用 signal 对信号做捕捉,并自定义处理信号,本质就是修改 handler 表对应下标的函数指针。

信号未决,即信号产生了,但由于进程无法及时处理或者信号被屏蔽等原因,导致的信号无法被递达(无法被处理),信号一直保存在位图中,而常说的这个位图就是内核中的 pending。与 [1] 所说,将一个整型以比特位划分,以位图结构来存储信号是一样的。因此,操作系统向进程发送信号,本质就是修改 pending 位图的特定位。

进程在运行时收到了信号,只要信号的处理动作不是忽略,那么进程迟早会处理这个信号(对于还没被处理的信号则保证在 pending 中)。但是,有了 block 表,就允许进程屏蔽掉某些信号,而一旦进程把某些信号屏蔽了,在该信号没有被解除屏蔽之前,即便进程收到了该信号,那么该信号也不会被操作系统进行递达(即不会被处理)。信号被屏蔽就是上述所说的 进程可以选择阻塞 (Block )某个信号。但是,阻塞某个信号只是进程可以不去递达它,并不代表收到信号后可以不用做保存,即便该信号被进程阻塞了,收到信号后依旧要保存在 pending 表中。而如果该信号一直没有被解除屏蔽,那么收到的该信号就一直被保存在 pending 表中(这就好比身为大学生的你,平时的高数课你不想听,作业也不想做,所以你屏蔽掉关于这门课的一切,但是你会把作业和最后的复习 ppt 都保存到 pending 中,等到期末周,想到什么都不会的你就要考试了,你赶紧解除对这门课的屏蔽,把 pending 保存的作业什么的全部拿出来,开始猛学,最后期末 60分 压线过了)。

  • 如果信号还没有产生,进程可以阻塞该信号吗??

    可以。屏蔽是一种状态,和信号当前是否产生了没有任何关系! 就好比,你某一门课的作业还没有布置下来,你这个三好学生在心里就已经决定了不写这门课的作业了!还有类似于你不喜欢吃香菜,虽然你现在并没有遇到这种情况,但是你在心里已经说好不吃了,将来遇到香菜,你也不会吃。

而在内核中,系统通过 block 表这样的位图结构来实现对信号的阻塞,结构与 pending 表一模一样,比特位的位置对应着信号的编号。而不同的是,pending 表中比特位的内容表示是否收到信号,block 表中的比特位的内容表示是否屏蔽了该信号(1 表示屏蔽,0 表示没有屏蔽),当一个进程屏蔽某个信号,本质就是修改 block 位图中的特定位。

所以在内核中,每个信号都有两个标志位分别表示阻塞(block) 和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到该信号递达才清除该标志(由 1 置 0)。而当某个信号未阻塞也未产生过,信号递达时执行默认的信号处理动作。

而当一个信号产生过,但正在被阻塞,因此暂时不能递达。即便对于该信号的处理动作是忽略,但是在没有解除该信号的阻塞之前,依旧不能忽略这个信号(依旧需要做保存),因为进程仍有机会改变处理动作之后再解除阻塞。

对于一个信号未产生过,但是已经被标记了阻塞(即信号一产生就屏蔽),信号的处理方式是用户自定义,如果在进程解除对某信号的阻塞之前,这种信号产生过多次,只要是普通信号,那么在递达之前产生多次只计一次,后续解除阻塞了,也是只处理一次。

诸如上述所介绍的三张表,都属于操作系统内核数据结构,但是用户无法直接访问内核数据,所以用户在访问修改表中的数据时,一定是通过系统调用访问的。而为了让用户能够访问到内核中的位图,操作系统就需要设计一种位图数据类型 sigset_t ,并且作为系统调用的输出型参数。

sigset_t: 信号集类型,对于每种信号用一个比特位表示 “有效” 或 “无效” 的状态

#include <signal.h>

int sigemptyset(sigset_t *set);				// 清空信号集,将每个比特位都置0

int sigfillset(sigset_t *set);				// 设置位图,将每个比特位都置1

int sigaddset (sigset_t *set, int signo);	// 向指定信号集添加一个信号

int sigdelset(sigset_t *set, int signo);	// 向指定信号集删除一个信号

int sigismember(const sigset_t *set, int signo);	// 判断一个信号是否在指定信号集中

对于 pending 位图来说就会有一个 pending 信号集,用于对 pending 表的操作;对于 block 位图,会有 block 信号集,用于操作 block 表。但是对于 block 信号集,很多教材称为信号屏蔽字,即可以通过调用函数 sigprocmask 读取或更改进程的信号屏蔽字(也称阻塞信号集)。

NAME
      sigprocmask - examine and change blocked signals

SYNOPSIS
      #include <signal.h>

      int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
      
RETURN VALUE
       sigprocmask() returns 0 on success and -1 on error.  In the event of an error, errno is set to indicate the cause.

参数分析:
how:{
	SIG_BLOCK:set 包含了我们希望添加到当前信号屏蔽字的信号,相当 mask = mask | set
	SIG_UNBLOCK:set 包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当 mask = mask & ~set
	SIG SETMASK:设置当前信号屏蔽字为 set 所指向的值,相当于 mask = set,直接覆盖内核中的 block 
}
set:存储修改进程信号集的特定位
oldset:一个输出型参数,在修改进程的 block 表之前,先把原来的 block 拷贝到 oldset 返回给用户。

用法概括:how 表如何修改,set 表修改的内容,oldset 备份上一次的 block 表
NAME
      sigpending - examine pending signals

SYNOPSIS
      #include <signal.h>

      int sigpending(sigset_t *set);

RETURN VALUE
       sigpending()  returns 0 on success and -1 on error.  In the event of an error, errno is set to indicate the cause.

参数分析:
set:一个输出型参数,将调用该函数的进程的 pending 以位图数据类型的形式拷贝给用户。

省流:修改 block 表调用 sigprocmask,修改 pending 表即向进程发送信号,修改 handler 表掉 signal。

2.2 编码

接下来我们代码实践,在信号未产生时,我们是可以屏蔽某个信号的,而后我们向进程发送被屏蔽的信号,并打印 pending 表,我们就可以看到 pending 表中的二进制序列特定位由 0 变 1(因为该信号不会被递达,所以一直保存在 pending 中,而信号没有被递达,即可证明该信号被阻塞)

// 阻塞信号与解除阻塞
void PrintPending(sigset_t& pending)
{
	for(int signo = 31; signo > 0; --signo)
	{
		if(sigismember(&pending, signo)) cout << "1";
		else cout << "0";
	}
	cout << "\n\n";
}

int main()
{
	// 1. 先屏蔽2号信号
	sigset_t bset, oset;
	sigemptyset(&bset);
	sigemptyset(&oset);
	sigaddset(&bset, 2);	// 将2号信号添加到用户区的信号集
	sigprocmask(SIG_SETMASK, &bset, &oset);		//将用户定义的信号集设置到进程内核

	// 2. 打印 pending 
	sigset_t pending;
	int cnt = 0;
	while(true)
	{
		int n = sigpending(&pending);	// 获取 pending
		if(n == -1) continue;
		PrintPending(pending);
		sleep(1);
		if(++cnt == 10)
		{
			cout << "unblock 2 signo\n";
			sigprocmask(SIG_SETMASK, &oset, nullptr);	// 解除屏蔽
		}
	}
	return 0;
}

在这里插入图片描述

现象:我们不仅要能够看到,在收到 2 号信号后,打印出来的 pending 二进制序列的特定位由 0 变 1,而且在 2 号被解除屏蔽之后,该信号立马就被递达了,并不会因为这个信号是处于屏蔽状态下收到的,而舍去信号的处理!

如果想要在信号解除屏蔽之后,还能看到 pending 二进制序列,那么可以使用 signal(2, SIG_IGN); 对信号做捕捉,并自定义处理。

9 、19 这两个信号不可被捕捉,同样的,这两个信号也不可被屏蔽!如果操作系统给了用户捕捉或屏蔽全部信号(只讨论普通信号) 的权利,那么将来如果有人在操作系统上运行恶意进程,届时就没有用户能够制止这个进程。


如果感觉该篇文章给你带来了收获,可以 点赞???? + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!

上一篇:Linux命令笔记


下一篇:大学《程序设计基础》课后作业的思路主要包括以下几个步骤‌: