linux信号 | 学习信号四步走 | 一篇文章教你理解信号如何保存

CSDN 2024-10-06 13:37:02 阅读 62

        前言: 本节内容是信号的保存。 学习信号, 我们首先了解了信号的概念, 然后学习了信号的产生方式。 现在就开始讲解信号在时间窗口内是如何保存在进程内部的。 

        ps:本节内容需要了解信号的概念, 希望友友们了解一些信号相关概念再来观看哦。

目录

进程与信号的关系

信号的保存

handler与信号的递达

pending与信号的未决

block与信号的屏蔽

sigset_t

sigprocmask

sigpending

创建文件

makefile

mysignal.cpp

包含头文件

 打印pending表

handler自定义信号动作

主函数        

屏蔽2号信号

 重复打印pending表

20秒后解除屏蔽 

 运行结果


进程与信号的关系

        对于普通信号而言, 对于进程而言——自己有还是没有收到哪一个信号。 是给进程的PCB发, 进程的PCB里面有一个成员变量叫做signal, 初始的比特位为全0。

<code>task_struct

{

int signal; //00000000 00000000 00000000 00000000

}

        进程收到信号,然后保存。这个过程中, 进程就要对信号做管理, 进程对信号的组织方式是使用位图的方式, 其中的内容为01。

        1、比特位的内容是0还是1, 表示是否收到。        2、比特位的位置(第几个), 表示信号的编号。        3、所谓“发信号” , 本质就是OS去修改task_struct信号位图对应的比特位。——所以, 我们OS要给一个进程发信号, 只需要找到进程PCB, 找到里面的字段, 把对应的比特位由0置为1。 至此, 信号发送完毕。 

        为什么必须是操作系统去写信号呢? ——因为操作系统是进程的管理者只有它有资格修改进程内部的数据。 ——而这就要知道一个真相,进程的PCB包含进程的属性, 进程自己没有资格访问自己的PCB 只有OS有资格, 因为OS是进程管理者。 作为OS, OS为什么不直接把进程干掉? ——技术上绝对可以, 但是操作系统不想背锅, 如果进程内有很重要的数据, 操作系统直接干掉这个进程, 那么这些数据就丢失了, 那么用户就会怪罪操作系统!!!

信号的保存

        当一个进程收到信号的时候, 可能并不会立即处理这个信号——就要有一个时间窗口, 在这个时间窗口以内, 信号已经产生, 但是进程没有被处理。 而为什么使用位图仅仅是因为普通信号比较简单, 所以采用位图结构。 

        那么就意味着我们如果发了十几个相同的信号, 他也只会记录一次, 剩下的都会丢失, 因为位图只有0/1。而实时信号就是这种不能丢失的信号, 发了10次, 就必须要执行10次, 不允许丢失。 管理实时信号使用的是双链表, 队列。 

handler与信号的递达

        普通信号的范围[1, 31], 每一种信号都要有自己的一种默认处理方法, 首先重定义一个函数指针: typedef void(* handler_t) (int)

        然后就创建一个数组变量handler_t handler[31]——此时, 系统就会默认给这个数组内的元素添加对应的默认动作。 

        所以,handler数组里面默认就有大量的方法, 这些方法是默认方法。 当我们自定义了handler方法 那么我们就可以让其中一个元素指向这个方法。——这就叫做自定义方法。

        还有一种处理方法叫做忽略, 顾名思义就是忽略这个信号。

        注意:忽略也是一种处理方式。和上面两种方式一样都是被处理了。 而被处理就叫作信号的递达! 

pending与信号的未决

        信号在发出和递达之间的状态, 叫做信号的未决, pending表中保存的就是每一种信号是否处于未决状态。如果是为1, 如果否为0, 0和1即为未决的描述方法;pinding表是使用的位图, 位图即为未决的组织方法。         

         所以, 我们在进程中, 至少要存在两张表。 一张是pending表——表示已经发出的信号, 但是还没有被处理的信号, 处于未决状态。 另一张就是上边这张handler, 信号方法指针数组。 handler是一个数组结构,pending表示是否收到了信号, 以及收到的是哪种信号, pending是一个位图结构。

        其实, 我们信号的实现, 是模拟的我们的硬件中断。 这里的pinding信号的编号, 就类似于中断编号。 而函数指针数组, 就类似于我们的中断向量表。

block与信号的屏蔽

        另外, 进程也可以选择屏蔽某些信号, 一旦把某些信号屏蔽了,在该信号解决屏蔽之前, 即便收到了该信号, 对应的信号也不会被递达。 ——但是要注意的是, 这里的屏蔽是一种状态, 和信号产不产生没有任何关系。 就比如老师留作业, 老师的作业一定会布置下来, 但是我们不做。 那么这里的作业就是信号, 作业布置就是信号产生。 我们不去做作业, 就是信号屏蔽。——以上, 这里的屏蔽操作就是通过第三张表来进行——block表。 这张表也是一个位图结构, 这张表的结构和pending表一模一样, 就是二进制序列。 只是block表和pending表相同点都是位图, 比特位的位置是信号的编号。 但是比特位的内容是:0表示不屏蔽, 1表示屏蔽。 

        那么, 如何利用三张表看一个信号这个的处理过程呢?就如同下图:

        只需要横向看这个图, 先看三张图中的第一个位置。 block为0, 代表没有阻塞, pending为0, 代表没有收到这个信号。但是如果收到这个信号, 处理方法就是DFL。 2号新号虽然收到了, 但是对他是屏蔽的, 那么2号新号就会一直都是pending为1, 处理这个信号的方式, 就是signal。三号信号没有收到, 但是已经对三号信号做出屏蔽了, 即便收到, 那么也不能做出处理, 会一直pending为1, 处理方法就是在我们的用户空间进行处理。

        我们学习信号, 学习信号的函数, 总是绕不开这三张表的。 有的是修改block表的, 有的是进行获取pending表的, 有的是设置handler表的。 

        我们说信号的处理叫做递达, 信号的产生到递达的中间过程叫做信号的未决, 也就是pending。 如果一个信号阻塞了,那么这个信号即便产生, 也只能保持在未决状态, 无法递达。 

信号的忽略和阻塞:

        阻塞是信号的一种状态, 意味着信号不会被递达。        忽略的本质是处理信号, 他是信号递达的三种方式之一。 

        对于操作系统来讲, 未来一定会给我们提供接口来操作这些位图结构, 那么这些pending, block的设计结构可不可以是整数呢?——在技术角度讲, 这个当然可以, 但是未来操作系统如果将pending, block扩大呢?万一int类型不够怎么办?所以系统专门会设计一种位图结构, 这种位图结构是一种可以扩展的位图。 

        接下来我们看一下SIG_IGN, 这个SIG_IGN就是一个自定义方法,可以作为我们的signal的第二个参数。作用是忽略这个信号。 

        运行后, 结果如下图:

        我们就会发现, 我们无论如何ctrl + c也没有用了。

 

ps:我们转到定义, 其实可以看到SIG_IGN其实就是对1进行强转。 SIG_DFL是直接退出, 代表的是对0进行强转。 

sigset_t

        上面三张图都是属于操作系统的。 都是内核数据结构, 操作系统不允许用户直接修改这三张表, 那么就要提供系统调用。 但是, 我们如果想要拿到这三张表, 就意味着要在用户空间和内核空间中进行来回的拷贝。 数据拷贝时,我们就要在接口的参数设计上, 设置输入输出型参数。 那么就要求操作系统在用户层设计出一种数据类型, 用这个数据类型的对象作为输入输出型参数。 这个数据类型就是位图结构——也就是sigset_t。 但是未来我们不能随便的进行位操作, 所以操作系统就给我们提供了一种信号集类型, 这个类型对于每一种信号都用有效或者无效两个状态。 ——注意, 这个变量不能直接进行任何操作, 都是没有意义的, 只能通过相关的调用接口。  

sigprocmask

sigprocmask——调用函数可以读取或者更改进程的的信号屏蔽字(阻塞信号集)

这个how有三个选项, 三个选项只能选择一个:

相当于把覆盖式的设置, 让mask变成set。

这里面的第二个参数就是这个set, 也就是说, 这里面第一二个参数是配合起来设置block表的屏蔽字的。 然后第三个参数是保存原block表的, 用来数据恢复。 

这个函数的返回值就是:零代表成功, -1代表失败, 错误码被设置。 

sigpending

        这个函数的参数是一个输出型参数。 作用就是把pending位图以参数的形式带出来。 什么意思, 就是调用进程的pending表, 带出来我们就可以查看我们的操作系统向当前的进程发送过哪些类型的信号, 并且这些信号没有被处理。 

        这个调用的返回值就是成功零被返回, 失败-1被返回, 错误码被设置。 

        以上两种方法, 对应着我们的内核里面的两张表。 分别对应着block表, pending表。 那么第三张handler表呢? 是由我们的signal函数对应着。 也就是说三个方法——sigprocmask、sigpending、signal对应着我们的三张表——block、pending、handler

----------------------------------------------------

下面我们要做一些实验

这个实验的流程就是: 我们先把二号信号屏蔽掉, 屏蔽掉然后当我们再发送我们的二号信号的时候, 发送之前我们一直打印pending表, 发送之后我们也一直打印pending表。 所以2号信号不会被递达, 所以我们就能在pending表中看到打出的pending表中的第二个比特位出现了1。最后经过20秒后解除屏蔽, 2号信号递达。

创建文件

我们首先要创建两个文件, 一个makefile, 一个.cpp文件。 如下图:   

     

makefile

将makefile准备好。 这里将要生成的程序名称叫做mysignal.exe。

<code>mysignal.exe:mysignal.cpp

g++ -o $@ $^ -std=c++11

.PHONY:clean

clean:

rm -f mysignal.exe

mysignal.cpp

包含头文件

#include <iostream>

using namespace std;

#include <string>

#include <unistd.h>

#include <sys/types.h>

#include <signal.h>

 打印pending表

        我们知道, 其实pending表就是一个位图结构。 而位图的打印, 我们要用到位操作一个比特位一个比特位地打印。 那么就是使用一个for循环, 从第一个比特位, 到最后一个比特位, 挨个地打印下去。如下位代码:

void PrintPending(sigset_t& pending)

{

for (int i = 31; i >= 1; i--)

{

if (sigismember(&pending, i))

{

cout << "1";

}

else

{

cout << "0";

}

}

cout << " pid: " << getpid();

cout << endl << endl;

}

handler自定义信号动作

        这里设定的handler自定义动作是将捕捉到的信号编号打印出来。 这个是为了方便观察捕捉到了哪一个信号。 

void handler(int signo)

{

cout << "catch a signo: " << signo << endl;

}

主函数        

屏蔽2号信号

        主函数的工作分为几部分。 首先是先对2号信号进行屏蔽。

int main()

{

//捕捉信号

signal(2, handler);

// 先对二号信号进行屏蔽

sigset_t bset, oset; // 先定义两个信号位图比那辆

sigemptyset(&bset); //对信号位图做清空

sigemptyset(&oset); //对信号位图做清空

sigaddset(&bset, 2); //给信号位图添信号位, 但是这里只是将我们的自己创建的位图结构的第二个位置置为1

//但是这个位图需要被设置进入系统的block里面。 才能真正将我们的信号屏蔽。

//而如何设置, 就要用到sigprocmask

sigprocmask(SIG_SETMASK, &bset, &oset);

return 0;

}

 重复打印pending表

int main()

{

signal(2, handler);

// 先对二号信号进行屏蔽

sigset_t bset, oset; // 先定义两个信号位图比那辆

sigemptyset(&bset); //对信号位图做清空

sigemptyset(&oset); //对信号位图做清空

sigaddset(&bset, 2); //给信号位图添信号位, 但是这里只是将我们的自己创建的位图结构的第二个位置置为1

//但是这个位图需要被设置进入系统的block里面。 才能真正将我们的信号屏蔽。

//而如何设置, 就要用到sigprocmask

sigprocmask(SIG_SETMASK, &bset, &oset);

//重复打印当前进程的pending:

sigset_t pending;

int cnt = 0;

while (true)

{

int n = sigpending(&pending);

if (n < 0) continue;

//打印我们的pending

PrintPending(pending);

sleep(1);

}

return 0;

}

20秒后解除屏蔽 

int main()

{

signal(2, handler);

// 先对二号信号进行屏蔽

sigset_t bset, oset; // 先定义两个信号位图比那辆

sigemptyset(&bset); //对信号位图做清空

sigemptyset(&oset); //对信号位图做清空

sigaddset(&bset, 2); //给信号位图添信号位, 但是这里只是将我们的自己创建的位图结构的第二个位置置为1

//但是这个位图需要被设置进入系统的block里面。 才能真正将我们的信号屏蔽。

//而如何设置, 就要用到sigprocmask

sigprocmask(SIG_SETMASK, &bset, &oset);

//重复打印当前进程的pending:

sigset_t pending;

int cnt = 0;

while (true)

{

int n = sigpending(&pending);

if (n < 0) continue;

//打印我们的pending

PrintPending(pending);

cnt++;

sleep(1);

//解除屏蔽, 2号本来被屏蔽, 现在解除2号屏蔽。

if (cnt == 20) //问题是当cnt == 20的时候, 这个进程直接终止了。

//这是因为不管我们是阻塞还是不阻塞。执行的都是默认动作, 如果我们

//对2号信号进行捕捉, 那么这个进程的2号信号到底是不是阻塞, 都会

//将默认动作改为自定义动作。

{

cout << "unblock 2 signal" << endl;

sigprocmask(SIG_SETMASK, &oset, nullptr);//我们已经把2号信号解除屏蔽了

}

}

return 0;

}

 运行结果

        运行上面的程序, 我们就能看到在我们使用kill -2命令之前, pending表打印的是一串0。 但是当kill -2命令之后, pending表打印的第二个比特位就变成了1。

并且20秒后我们可以看到我们的信号被捕捉了, 说明信号已经递达。 也就是说2号信号解除了屏蔽!

——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。