🏠关于专栏:Linux的浅学到熟知专栏用于记录Linux系统编程、网络编程等内容。
🎯每天努力一点点,技术变化看得见
文章目录
信号的内核表示与信号阻塞
信号的相关概念
●信号递达:实际执行信号的处理动作。
●信号未决:信号从产生到递达之间的状态。
★ps:进程可以选择阻塞某个信号,被阻塞的信号产生时将保持在未决状态,直到进行解除对信号的阻塞,才会执行递达的动作。
★ps:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达时可选的一种处理动作。
信号在内核中的表示
系统中的信号是发送给进程的,对于进程而言,信号是给进程的PCB发的,PCB使用位图结构记录收到的信号,这样进程执行时就知道自己收到什么信号了。
信号为什么要保存呢?进程收到信号之后,可能不会立即处理。这个信号从产生到递达之间的时间窗口内,进程需要记录该信号已经产生了,等到处理的时候才能知道哪些信号已经发生了。
- 比特位的内容为1或0,表示是否收到某个信号
- 比特位的位置(第几个),表示信号的编号c
- 所谓的“发信号”,本质就是操作系统去修改task_struct的信号位图的对应比特位,发送信号其实就是写信号。
操作系统是进程的管理者,只有它才有资格去修改task_struct内部的属性,即操作系统需要提供相应的系统调用以实现信号的发送、阻塞等功能。
task_struct中针对信号,包含了2张位图和1张函数指针表,分别是block位图、pending位图、handler处理函数指针表↓↓↓
block位图记录某个信号是否被阻塞,如果某个信号对应的比特为1表示该信号被阻塞,为0表示没有阻塞;对于被阻塞的信号,即使该信号产生了,进程也不会对该信号做任何处理。
pending位图记录是否收到某个信号,如果收到某个信号,则会将对应的比特位置1;处理完某个信号后,会将对应的比特位置0。由于每个比特位只能表示信号的有无,若在信号产生到信号递达的时间窗口内,重复收到多个同样的信号,最终也只会递达一次。
handler函数指针数组用于记录对各个信号的处理方式。如果设置为SIG_DFL表示执行系统默认处理函数,设置为SIG_IGN表示忽略该信号;设置为用户空间的某个函数时,待信号递达时,则会从内核态切换回用户态以执行该部分代码。
对于每个信号都有两种标志位分别表示是否阻塞和是否已经处理,还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的pending表对应比特位为未决状态,即比特位置1,直到信号递达才清除该标志,即将对应比特位置0。
对于上图,penging表记录了当前进程收到了2号到6号信号,而1号信号并没有收到。block表记录了,当前进程对3号和5号信号进行了阻塞。
对于2号信号,penging表记录了该信号已经产生,block表记录了该信号没有被阻塞,故进程会执行该信号对应的handler函数指针数组中的操作,即系统的默认操作。
对于4号信号,pending表记录了该信号已经发送,block表记录了该信号没有被阻塞,故该进程会执行对应的操作,即对该信号进行忽略。
对于6号信号,pending表记录了该信号已经发生,block表记录了该信号没有被阻塞,故该进程会执行对应的操作,即指定用户指定的SigHandler函数,此时会发生内核态到用户态的转换。
5号SIGSTAP信号产生过,但正在被阻塞,暂时不能递达。虽然它的默认处理动作是忽略,但是没有解除阻塞之前,不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
3号SIGQUIT信号产生过,一旦解除对该信号的屏蔽,则它就会被递达,因为它的pending表对应比特位尚未被置0。一旦解除对该信号的屏蔽,则会执行SIG_DFL默认动作。
★ps:如果在进程解除对某信号的阻塞之前,这个信号产生过多次,将如何处理呢?POSIX.1允许系统递送该信号一次或多次,Linux是这样实现的:常规信号在递达之前产生多次只记录一次,而实时信号在递达之前产生多次可以依次放在队列里,这里对实时信号不做讨论。
信号集操作函数
从上面的讨论可知,对于阻塞信号集和pending信号集来说,都可以使用同一种类型来实现,只要能够标识某个信号是否被阻塞(或者是否已经发生)即可。故系统提供了一种统一的类型sigset_t来处理这两个位图结构。但为了保证该类型的易用性、可移植性等问题,故用户不能直接操作,而需要使用系统提供的接口。且若是sigset_t指针,指向PCB结构的sigset_t字段时,则不能直接对该结构进行位操作,因为只有操作系统有权限修改PCB的属性信息。↓↓↓
★ps:阻塞信号集(block位图)也就做当前进程的信号屏蔽字,这里的“屏蔽”应该理解为阻塞,而不是忽略。
上图各个系统调用接口的用法如下表所示↓↓↓
系统调用接口 | 用途 |
---|---|
sigemptyset | 将传入的set位图的各个位清零 |
sigfillset | 将传入的set位图的各个位置1 |
sigaddset | 将signum信号在set中对应的比特位置1 |
sigdelset | 将signum信号在set中对应的比特位置0 |
sigismember | 查看signum对应的比特位在set中是否为1,为1则返回1,否则返回0 |
除了sigismember外,其他的调用接口成功返回0,失败返回-1。
注意,在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后,可以调用sigaddset和sigdelset在该信号集中添加或删除某种信号。
★ps:使用时不应该对sigset_t的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。
上述接口只是对sigset_t这个位图结构做操作,并未对block和pending位图直接做操作,下面介绍关于这两个位图的系统调用函数↓↓↓
sigprocmask
调用函数sigprocmask可以读取或修改进程的信号屏蔽字(阻塞信号集)。关于上述各个参数的设置,即how的可选数值如下标所示↓↓↓
how取值 | 用法及含义 |
---|---|
SIG_BLOCK | set包含我们希望添加到当前信号屏蔽字中的信号,相当于mask=mask|set |
SIG_UNBLOCK | set包含我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask=set |
第三个参数,在用户传入非空的sigset_t类型的变量时,则会返回执行当前sigprocmask之前的进程信号屏蔽字。
★ps:如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回之前,至少将其中一个信号递达(如果收到解除屏蔽的信号的前提下)。
下面代码演示了如何获取当前进程的信号屏蔽字↓↓↓
#include <stdio.h>
#include <signal.h>
int main()
{
sigset_t set;
sigprocmask(SIG_BLOCK, NULL, &set);
int i = 1;
for(; i < NSIG; i++)
{
if(sigismember(&set, i))
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
return 0;
}
★ps:NSIG的取值是最大的信号数+1。
下面代码演示使用sigprocmask设置1号到8号信号的信号屏蔽,后解除1到5号信号的信号屏蔽,最终将信号屏蔽字改为仅屏蔽11到15号信号↓↓↓
#include <stdio.h>
#include <signal.h>
void PrintSig(sigset_t set)
{
int i = 1;
for(;i < NSIG; i++)
{
if(sigismember(&set, i)) printf("1");
else printf("0");
}
printf("\n");
}
int main()
{
sigset_t set;
sigemptyset(&set);
int i = 1;
for(; i <= 8; i++)
{
sigaddset(&set, i);
}
sigset_t oldset;
sigprocmask(SIG_BLOCK, &set, &oldset);
printf("初始的信号屏蔽字:");
PrintSig(oldset);
sigemptyset(&set);
i = 1;
for(; i <= 5; i++)
{
sigaddset(&set, i);
}
sigprocmask(SIG_UNBLOCK, &set, &oldset);
printf("屏蔽1到8号信号屏蔽字:")