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

news2024/11/27 0:35:21

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

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

目录

进程与信号的关系

信号的保存

handler与信号的递达

pending与信号的未决

block与信号的屏蔽

sigset_t

sigprocmask

sigpending

创建文件

makefile

mysignal.cpp

包含头文件

 打印pending表

handler自定义信号动作

主函数        

屏蔽2号信号

 重复打印pending表

20秒后解除屏蔽 

 运行结果


进程与信号的关系

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

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。

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号信号解除了屏蔽!

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2187286.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实用技能分享!推荐最适合论文写作的5款ai工具

在当今学术研究和教育领域&#xff0c;AI工具的应用已经变得越来越普遍。这些工具不仅能够提高写作效率&#xff0c;还能帮助生成高质量的文稿。对于教师而言&#xff0c;选择合适的AI工具可以显著提升论文写作的效率和质量。本文将重点推荐五款最适合教师论文写作的AI工具&…

Linux聊天集群开发之环境准备

一.windows下远程操作Linux 第一步&#xff1a;在Linux终端下配置openssh&#xff0c;输入netstate -tanp,查看ssh服务是否启动&#xff0c;默认端口22.。 注&#xff1a;如果openssh服务&#xff0c;则需下载。输入命令ps -e|grep ssh, 查看如否配有&#xff0c; ssh-agent …

【重学 MySQL】四十六、创建表的方式

【重学 MySQL】四十六、创建表的方式 使用CREATE TABLE语句创建表使用CREATE TABLE LIKE语句创建表使用CREATE TABLE AS SELECT语句创建表使用CREATE TABLE SELECT语句创建表并从另一个表中选取数据&#xff08;与CREATE TABLE AS SELECT类似&#xff09;使用CREATE TEMPORARY …

【重学 MySQL】五十四、整型数据类型

【重学 MySQL】五十四、整型数据类型 整型类型TINYINTSMALLINTMEDIUMINTINT&#xff08;或INTEGER&#xff09;BIGINT 可选属性UNSIGNEDZEROFILL显示宽度&#xff08;M&#xff09;AUTO_INCREMENT注意事项 适合场景TINYINTSMALLINTMEDIUMINTINT&#xff08;或INTEGER&#xff0…

Python 从入门到实战33(使用MySQL)

我们的目标是&#xff1a;通过这一套资料学习下来&#xff0c;通过熟练掌握python基础&#xff0c;然后结合经典实例、实践相结合&#xff0c;使我们完全掌握python&#xff0c;并做到独立完成项目开发的能力。 上篇文章我们讨论了数据库编程接口操作的相关知识。今天我们将学习…

SLF4J(W): Class path contains multiple SLF4J providers.

问题背景 最近在给某AI项目集成阿里的通义千问SDK&#xff0c;发现竟然有个奇怪的报错&#xff0c;仔细一看发现&#xff0c;我类上用的lombok的Slf4j注释&#xff0c;阿里用的是org.slf4j.simple.SimpleServiceProvider&#xff0c;但是lombok用的是LogbackServiceProvider&a…

关于Vben Admin多标签页面缓存不生效的问题

情况说明 笔者在接手一个基于Vben Admin框架改造的vue3后台管理项目&#xff0c;客户要求在切换头部Tab页面时&#xff0c;不要刷新清空已经填写的表单页面或者表格。 然而&#xff0c;笔者根据Vben Admin的官方文档来配置多标签页面缓存后&#xff0c;页面每次切换后&#x…

Linux 应用层协议HTTP

文章目录 一、初始HTTP协议二、URL格式网络中怎么通过URL进行定位资源呢&#xff1f;编码和解码 三、HTTP的请求格式和响应格式HTTP的请求格式HTTP的响应格式HTTP的请求方法GET方法POST方法GET Vs PostHTTP的封装和分用文件流操作浏览器获得一个完整的网页流程 HTTP的状态码对3…

一、Linux下MySQL的安装与使用

文章目录 1. 基于docker安装mysql2. 字符集的相关操作2.1 修改MySQL5.7字符集2.2 各级别的字符集2.3 字符集与比较规则(了解)2.4 请求到响应过程中字符集的变化 3. SQL大小写规范3.1 Windows和Linux平台区别3.2 Linux下大小写规则设置3.3 SQL编写建议 4. sql_mode的合理设置4.1…

知识图谱入门——5:Neo4j Desktop安装和使用手册(小白向:Cypher 查询语言:逐步教程!Neo4j 优缺点分析)

Neo4j简介 Neo4j 是一个基于图结构的 NoSQL 数据库&#xff0c;专门用于存储、查询和管理图形数据。它的核心思想是使用节点、关系和属性来描述数据。图数据库非常适合那些需要处理复杂关系的数据集&#xff0c;如社交网络、推荐系统、知识图谱等领域。 与传统的关系型数据库…

端侧大模型系列 | 端侧AI Agent任务拆解大师如何助力AI手机?(简短版)

引言 简介 模型 实验 意义&前景: 总结 引言 今人不见古时月&#xff0c;今月曾经照古人。 小伙伴们好&#xff0c;我是微信公众号《小窗幽记机器学习》的小编&#xff1a;卖沙茶面的小女孩。 设想一下&#xff0c;你的智能手机不再只是"聪明"&#xff0…

12.梯度下降法的具体解析——举足轻重的模型优化算法

引言 梯度下降法(Gradient Descent)是一种广泛应用于机器学习领域的基本优化算法&#xff0c;它通过迭代地调整模型参数&#xff0c;最小化损失函数以求得到模型最优解。 通过阅读本篇博客&#xff0c;你可以&#xff1a; 1.知晓梯度下降法的具体流程 2.掌握不同梯度下降法…

力扣 中等 129.求根节点到叶子结点数字之和

文章目录 题目介绍解法 题目介绍 解法 法一&#xff1a;有返回值、 class Solution {public int sumNumbers(TreeNode root) {return dfs(root, 0);}public int dfs(TreeNode root, int x) {if (root null) {return 0;}x x * 10 root.val;if (root.left root.right) { //…

LC刷题专题:dfs、哈希表合集

自己刷题缺少分类思想&#xff0c;总是这里刷一道那里刷一道&#xff0c;以后建立几个专辑&#xff0c;然后自己新刷的同类型的题目都会即使更新上。 文章目录 690. 员工的重要性 690. 员工的重要性 2024-10-03 题目描述&#xff1a; 我第一次写并没有考虑到dfs&#xff0c;…

基于Arduino的L298N电机驱动模块使用

一.简介&#xff1a; L298N作为电机驱动芯片&#xff0c;具有驱动能力强&#xff0c;发热量低&#xff0c;抗干扰能力强的特点,一个模块可同时驱动两个直流电机工作&#xff0c;能够控制电机进行正转、反转、PWM调速。 说明&#xff1a; 1&#xff09;12V输入端口接入供电电压…

esp32开发环境搭建和烧录测试

文章目录 前言一、硬件环境1、esp32开发板。2、两个micro usb 数据线&#xff0c;一路用于供电&#xff0c;另一路用于烧录和调试3、喇叭&#xff0c; 淘宝上买的 4 欧姆 3 W扬声器 二、软件环境配置1、开发软件2、ESP-IDF简介下载 3、vscode安装配置1、安装vscode2、安装IDF插…

论文提纲怎么写?分享5款AI论文写作软件

在学术研究和写作过程中&#xff0c;撰写高质量的论文是一项挑战性的任务。幸运的是&#xff0c;随着人工智能技术的发展&#xff0c;AI论文写作工具逐渐成为帮助学者和学生提高写作效率的重要工具。这些工具不仅能够提高写作效率&#xff0c;还能帮助简化复杂的写作流程&#…

C++(string类的实现)

1. 迭代器、返回capacity、返回size、判空、c_str、重载[]和clear的实现 string类的迭代器的功能就类似于一个指针&#xff0c;所以我们可以直接使用一个指针来实现迭代器&#xff0c;但如下图可见迭代器有两个&#xff0c;一个是指向的内容可以被修改&#xff0c;另一个则是指…

【JNI】hello world

JNI&#xff0c;作为java和C/C的中间层&#xff0c;为在Java中调用C/C代码提供了便利。作为初学者&#xff0c;这里简单记录学习的过程。 本文所有的操作都在kali linux上进行&#xff0c;jdk环境以及gcc&#xff0c;g编译器需自行提前安装好 操作系统&#xff1a; jdk&#…

行为型模式-命令-迭代-观察者-策略

命令模式 是什么 将一个请求封装成为一个对象, 从而可以使用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及可以撤销的操作 实例 请求封装成为对象 //用来声明执行操作的接口 public abstract class Command { protected Receiver receiver; public Comma…