【Linux】信号之信号的保存和处理详解

🤖个人主页:晚风相伴-CSDN博客

💖如果觉得内容对你有帮助的话,还请给博主一键三连(点赞💜、收藏🧡、关注💚)吧

🙏如果内容有误或者有写的不好的地方的话,还望指出,谢谢!!!

让我们共同进步

目录

前言

✨信号的阻塞 

🔥信号的常见概念

🔥在内核中的表示

❔sigset_t类型

🔥信号集操作函数

sigprocmask

sigpending

示例代码

✨信号的捕捉

🔥理解信号捕捉的流程

🔥sigaction

❔可重入函数​编辑

❔volatile


前言 

信号的时间线 

在上一篇《信号之信号的产生》中已将信号产生讲明白了,本篇就来讲讲信号的保存和处理吧。

✨信号的阻塞 

🔥信号的常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)
  • 进程可以选择阻塞(Block)某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

注意阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

🔥在内核中的表示

信号在内核中的表示示意图

每个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针数组,里面保存的是函数的地址表示处理动作。信号产生时,操作系统在进程控制块(PCB)中设置该信号的未决标志(由0->1),直到信号递达后才清理该标志位(由1->0)。

例如在上面的图中

  • SIGHUP信号没有阻塞也没有产生,当它递达时执行默认的处理动作。
  • SIGINT产生了,但是Block位图中是1,表示它正在被阻塞着,所以暂时不能递达,虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作在解除阻塞之前。
  • SIGQUIT信号没有产生,但是Block位图中是1,所以一旦产生了SIGQUIT信号将会被阻塞,它的处理动作是用户自定义函数sighandler。

如果在进程解除对某信号的阻塞之前这个信号产生了多次,Linux操作系统该如何处理呢?

对于常规信号而言[1-31],操作系统会将它们在递达之前产生多次只计一次。也就是说,即使这个信号在阻塞期间产生了多次,当阻塞被解除并且信号能够传递给进程时,进程只会收到一个这样的信号。

对于实时信号而言[34-64],它们在递达之前产生多次是可以排队的,即多个相同的实时信号在阻塞期间产生会依次放在一个队列里,当阻塞被解除时,它们会按照产生的顺序依次递达给进程。

❔sigset_t类型

每个信号就是由一个比特位来表示其状态,非0即1。从上图可以看出,在阻塞位图和未决位图中都是这样表示的,所以它们可以用相同的数据类型sigset_t来进行存储,sigset_t称为信号集。对于这个类型内部如何存储这些比特位的则依赖于系统实现,用户并不需要关心,这个类型可以表示每个信号“有效”或“无效”的状态,在阻塞位图(阻塞信号集)中“有效”或“无效”就表示该信号是否被阻塞,而在未决位图(未决信号集)中“有效”或“无效”就表示该信号是否产生。

注:阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)。

🔥信号集操作函数

可以调用以下函数对sigset_t类型进行操作

#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(sigset_t *set, int signo);
  • sigemptyset函数:初始化set所指向的信号集,使其中所有的信号对应的比特位清0,表示该信号集不包含任何有效信号。
  • sigfillset函数:初始化set所指向的信号集,使其中所有的信号对应的比特位置1,表示该信号集包含所有有效信号
  • sigaddset函数:在该信号集中添加某个有效信号
  • sigdelset函数:在该信号集中删除某个有效信号
  • sigismember函数:判断一个信号集的有效信号中是否包括了某个信号

前4个函数的返回值都是成功返回0,失败返回-1。最后一个是若包含则返回1,若不包含则返回0,失败返回-1。  

sigprocmask

该函数用于读取或者更改进程的信号屏蔽字(阻塞信号集) 

如果oldset是非空指针,则读取进程通过oldset参数传出的当前信号屏蔽字。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oldset和set都是非空指针,则先将原来的信号屏蔽字备份到oldset里,然后根据set和how参数更改信号屏蔽字。

下面是how参数的可选项

SIG_BLOCK将set指向的信号集添加到当前进程的信号屏蔽字中
SIG_UNBLOCK将set指向的信号集从当前进程的信号屏蔽字中移除
SIG_SETMASK设置当前信号屏蔽字为set所指向的值

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

sigpending

该函数用于读取当前进程的未决信号集

返回值:成功成功返回0,失败返回-1

示例代码

将2号信号阻塞掉,并且不断的获取并打印当前进程的pending信号集,如果我们向该进程发送一个2号信号,我们就可以看到在pending信号集中一个比特位由0->1。 

#include <iostream>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

void handle(int signum)
{
    cout << "捕捉到信号: " << signum << endl;
}

static void printPending(sigset_t &pending)
{
    for (int i = 1; i <= 31; i++)
    {
        if (sigismember(&pending, i))
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

int main()
{
    //捕捉2号信号,不让其退出
    signal(2, handle);
    // 1、定义信号集对象
    sigset_t bset, obset;
    sigset_t pending;

    // 2、初始化
    sigemptyset(&bset);
    sigemptyset(&obset);
    sigemptyset(&pending);

    // 3、添加要屏蔽的信号
    sigaddset(&bset, 2 /*SIGINT*/);

    // 4、设置set到内核中对应的进程内部
    int n = sigprocmask(SIG_BLOCK, &bset, &obset);
    assert(n == 0);
    (void)n;
    cout << "block 2号信号成功……, pid: " << getpid() << endl;

    // 5、重复打印当前进程的pending信号集
    int count = 0;
    while (true)
    {
        // 获取pending信号集
        sigpending(&pending);
        // 打印信号集
        printPending(pending);
        sleep(1);
        count++;
        //count == 20时解除对2号信号的阻塞
        if (count == 20)
        {
            n = sigprocmask(SIG_SETMASK, &obset, nullptr);
            assert(n == 0);
            (void)n;
            cout << "解除2号信号的block成功" << endl;
        }
    }

    return 0;
}

结果演示 

如果我们将所有的信号都进行阻塞掉,我们是不是就写了一个不会被异常或者用户杀掉的进程?

这真的可以吗?用下面的代码进行验证一下 

static void printPending(sigset_t &pending)
{
    for (int i = 1; i <= 31; i++)
    {
        if (sigismember(&pending, i))
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

static void blockSig(int sig)
{
    sigset_t bset;
    sigemptyset(&bset);
    sigaddset(&bset, sig);
    int n = sigprocmask(SIG_BLOCK, &bset, nullptr);
    assert(n == 0);
    (void)n;
}

int main()
{
    cout << "pid: " << getpid() << endl;
    for (int sig = 1; sig <= 31; sig++)
    {
        blockSig(sig);
    }
    sigset_t pending;
    while (true)
    {
        // 获取pending信号集
        sigpending(&pending);
        // 打印信号集
        printPending(pending);
        sleep(1);
    }
    return 0;
}

mykill.cc

#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

using namespace std;

void Usage(string proc)
{
    cout << "Usage:\r\n\t" << proc << " processid" << endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[1]);
        exit(1);
    }
    int processid = atoi(argv[1]);

    for (int i = 1; i <= 31; i++)
    {
        if (i == 9 || i == 19)
            continue;
        kill(processid, i);
        cout << "kill -" << i << endl;
        sleep(1);
    }
    kill(processid, 9);
    return 0;
}

结果演示

其实在上面的代码中是屏蔽了9号信号和19号信号的,如果你将9号信号放开,这个进程立马就会被终止掉,所以将所有信号阻塞掉,还是可以将这个进程终止的——用9号信号。

  

19号信号是中止进程 

✨信号的捕捉

将所有的信号都捕捉,那么这个进程是不是就不会被异常或者用户杀掉了呢

示例代码

void catchSig(int signum)
{
    cout << "获取一个信号: " << signum << endl;
}

int main()
{
    for (int i = 1; i <= 31; i++)
        signal(i, catchSig);

    while (true)
        sleep(1);

    return 0;
}

结果演示 

可见虽然我们自定义捕捉了所有信号,但是9号信号还是有用的,不会失效。 

🔥理解信号捕捉的流程

信号产生之后,信号不是被立即处理而是在合适的时候进行处理。

内核态和用户态的概念

当一个进程执行系统调用而陷入内核代码中执行时,则称其处于内核态

当一个进程在执行用户自己的代码时,则称其处于用户态


怎么理解这个合适的时候呢

我们都知道信号的相关数据字段都是保存在进程PCB内部的,而PCB是属于内核的范畴,所以要检测这个信号是否未决,是否产生,一定是在内核状态进行检测的;而进程大部分时间是在执行你写的代码,而这所处的状态叫做用户态。所以当从内核态返回到用户态时,就会对信号进行检测和处理。这就叫做合适的时候。


如何理解用户态和内核态之间的相互转换

在进程的地址空间中0-3GB是用户区,3-4GB是内核区。

  • 当进程处于用户态时,在地址空间中的用户区的代码和数据,通过用户级的页表映射到物理内存中,CPU就能拿到代码和数据进行执行
  • 当进程处于内核态时,在地址空间中的内核区的代码和数据,用户内核级的页表映射到物理内存中,CPU就能拿到代码和数据进行执行

注意:内核级页表可以被每个进程所看到,因为操作系统只有一个,每个进程通过内核级页表映射到物理内存的同一块区域。

地址空间中的内核区和用户区之间的转换是通过系统调用和上下文切换来实现的。

例如,你在你的代码中使用了系统调用接口open,它就会直接在地址空间中由用户区跳转到内核区,然后再内核区通过内核级页表将代码和数据映射到物理内存上。

注:CPU可以通过寄存器里的值就能知道当前进程是处于用户态还是内核态

当然内核也是可以执行用户的代码的,但是内核不愿意,也不想这样干,因为如果这样干很可能导致内核中的数据和代码被用户给修改了。

信号捕捉的流程图:

假设用户自定义的信号处理函数sighandler,进程当前正在执行用户的代码,这时执行到某条指令时发生了中断,用户态切换到了内核态并保存当前进程的上下文。内核态将中断处理完后需要返回到用户态,在返回之前先检查一下是否有信号需要递达, 如有则要对信号进行处理,如果信号的处理动作是用户自定义的, 那么内核态就要返回到用户态执行sighandler函数(sighandler和主函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程),sighandler函数处理完后,进程又再次返回到内核态,如果没有新的信号需要递达了,那么就需要从内核态返回到用户态并且恢复上下文继续从主逻辑向下执行。

可以简化成下面这幅图

🔥sigaction

sigaction函数可以读取和修改与指定信号相关联的处理动作。和signal作用类似

参数

  • sig:指定信号的编号
  • 若act指针非空,则根据act修改该信号的处理动作。
  • 若oact指针非空,则通过oact传出该信号原来的处理动作

返回值:成功则返回0,失败则返回-1。

说明

sa_handler是一个回调函数,被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本篇的代码把sa_flags设为0,sa_sigaction是实时信号的处理函数,这里不做讨论。

示例代码

static void printPending(sigset_t &pending)
{
    for (int i = 1; i <= 31; i++)
    {
        if (sigismember(&pending, i))
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

void handler(int signum)
{
    cout << "捕捉到一个信号: " << signum << endl;

    int count = 20;
    sigset_t pending;
    while (true)
    {
        // 获取pending信号集
        sigpending(&pending);
        // 打印信号集
        printPending(pending);
        count--;
        if (!count)
            break;
        sleep(1);
    }
}

int main()
{
    cout << "pid: " << getpid() << endl;
    struct sigaction act, oact;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);
    sigaddset(&act.sa_mask, 5);
    sigaddset(&act.sa_mask, 6);

    act.sa_handler = handler;

    sigaction(2, &act, &oact);

    cout << "default action: " << (int)(oact.sa_handler) << endl;

    while (true)
        sleep(1);
}

结果演示

 

❔可重入函数

例如在图中的循环单链表

在主函数中调用insert函数向一个链表的头节点插入节点node1,插入操作分为两步,刚做完第一步时,因为硬件中断使进程切换到内核,当内核态会到用户态之前检查到有信号需要处理,于是切换到自定义的信号处理函数,信号处理函数中也调用了insert函数向同一个链表的头节点中插入node2,插入操作的两步都完成了之后,信号处理函数返回到内核态,再次返回到用户态后就从主函数调用的insert函数中继续向下执行,先前只做完了第一步就被打断了,现在继续做插入操作的第二步,但是在自定义的信号处理函数中已经插入了node2了,node2作为新的头结点,node1在完成剩下的一步时,就会导致node2节点丢失,导致内存泄漏等问题。

所以一个函数在一个特定的时间段内被多个执行流重复进入,如果该函数被重复进入不会导致问题那么就叫做可重入函数,否则就叫做不可重入函数

如果一个函数符合以下条件之一则是不可重入的

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准的I/O库的很多实现都以不可重入的方式使用全局数据结构。

❔volatile

示例代码 

int flag = 0;

void changeFlag(int signum)
{
    (void)signum;
    cout << "change flag: " << flag;
    flag = 1;
    cout << "->" << flag << endl;
}

int main()
{
    signal(2, changeFlag);
    while (!flag);
    cout << "进程正常退出后: " << flag << endl;
    return 0;
}

没给编译器加优化前 

结果演示

给编译器加了优化后

结果演示

导致这样的结果的原因

  • 在没加优化之前,CPU是正常从我们的内存中读取flag的值的,在flag由0->1后,CPU读取到了1,就直接让进程终止掉了。
  • 在加了优化之后,CPU在看了main函数中没有对flag进行修改的相关语句,所以CPU就不在从内存中读取flag的值了,而是从自己的寄存器中读取比如说edx寄存器, 所以CPU一直读取到的flag是0,也就不会让进程终止掉了。

加了优化并且加了volatitle后

结果演示

volatitle作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

仿《Q极速体育》NBACBA体育直播吧足球直播综合体育直播源码

码名称&#xff1a;仿《Q极速体育》NBACBA体育直播吧足球直播综合体育直播源码 开发环境&#xff1a;帝国cms7.5 空间支持&#xff1a;phpmysql 仿《Q极速体育》NBACBA体育直播吧足球直播综合体育直播源码自动采集 - 我爱模板网源码名称&#xff1a;仿《Q极速体育》NBACBA体育直…

编程实战:自己编写HTTP服务器(系列3:处理框架)

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 系列入口&#xff1a;编程实战…

需求分析部分图形工具

描述复杂的事物时,图形远比文字叙述优越得多,它形象直观容易理解。前面已经介绍了用于建立功能模型的数据流图、用于建立数据模型的实体-联系图和用于建立行为模型的状态图,本节再简要地介绍在需求分析阶段可能用到的另外3种图形工具。 1 层次方框图 层次方框图用树形结…

LaTex 模板 - 东北师范大学申研申博推荐信

文章目录 NENU-Letter-Template项目地址示例特性项目结构如何使用main.texletterContent.tex 如何编译方式 1 &#xff1a;在线编译方式 2 &#xff1a;本地编译 参考 NENU-Letter-Template NENU’s recommendation letter template. 东北师范大学推荐信模板 项目地址 GitHu…

Spring框架学习笔记(五):JdbcTemplate 和 声明式事务

基本介绍&#xff1a;通过 Spring 框架可以配置数据源&#xff0c;从而完成对数据表的操作。JdbcTemplate 是 Spring 提供的访问数据库的技术。将 JDBC 的常用操作封装为模板方法 1 JdbcTemplate 使用前需进行如下配置 1.1 在maven项目的pom文件加入以下依赖 <dependencies…

Windows安装并启动Redis服务端(zip包)

一、Redis简介 Redis&#xff08;Remote Dictionary Server&#xff09;是一个开源的基于内存的 Key - Value结构的数据库&#xff0c;遵守 BSD 协议&#xff0c;它提供了一个高性能的键值&#xff08;key-value&#xff09;存储系统&#xff0c;常用于缓存、消息队列、会话存储…

c# sqlite使用

安装包 使用 const string strconn "Data Sourcedata.db"; using (SQLiteConnection conn new SQLiteConnection(strconn)) {conn.Open();var cmd conn.CreateCommand();cmd.CommandText "select 1";var obj cmd.ExecuteScalar();MessageBox.Show(ob…

Python小游戏——打砖块

文章目录 打砖块游戏项目介绍及实现项目介绍环境配置代码设计思路代码设计详细过程 难点分析源代码代码效果 打砖块游戏项目介绍及实现 项目介绍 打砖块游戏是一款经典的街机游戏&#xff0c;通过控制挡板来反弹小球打碎屏幕上的砖块。该项目使用Python语言和Pygame库进行实现…

鲁教版七年级数学上册-笔记

文章目录 第一章 三角形1 认识三角形2 图形的全等3 探索三角形全等的条件4 三角形的尺规作图5 利用三角形全等测距离 第二章 轴对称1 轴对称现象2 探索轴对称的性质4 利用轴对称进行设计 第三章 勾股定理1 探索勾股定理2 一定是直角三角形吗3 勾股定理的应用举例 第四章 实数1 …

【实际项目精选源码】ehr人力资源管理系统实现案例(java,vue)

一、项目介绍 一款全源码可二开&#xff0c;可基于云部署、私有部署的企业级数字化人力资源管理系统&#xff0c;涵盖了招聘、人事、考勤、绩效、社保、酬薪六大模块&#xff0c;解决了从人事招聘到酬薪计算的全周期人力资源管理&#xff0c;符合当下大中小型企业组织架构管理运…

心链2---前端开发(整合路由,搜索页面,用户信息页开发)

心链——伙伴匹配系统 接口调试 说书人&#x1f4d6;&#xff1a;上回书说到用了两种方法查询标签1.SQL查询&#xff0c;2.内存查询&#xff1b;两种查询效率是部分上下&#xff0c;打的是难解难分&#xff0c;是时大地皴裂&#xff0c;天色聚变&#xff0c;老祖斟酌再三最后决…

(十一)统计学基础练习题五(50道选择题)

本文整理了统计学基础知识相关的练习题&#xff0c;共50道&#xff0c;适用于想巩固统计学基础或备考的同学。来源&#xff1a;如荷学数据科学题库&#xff08;技术专项-统计学二&#xff09;。序号之前的题请看往期文章。 201&#xff09; 202&#xff09; 203&#xff09; 2…

指纹识别概念解析

目录 1. 指纹是物证之首 1.1 起源于中国 1.2 发展于欧洲 1.3 流行于全世界 2. 指纹图像 3. 指纹特征 4. 指纹注册 5. 指纹验证 6. 指纹辨识 1. 指纹是物证之首 指纹识别技术起源于中国、发展于欧洲、流行于全世界。自20世纪以来&#xff0c;指纹在侦破刑事案件、解决诉…

《图解支付系统设计与实现》电子书_V20240525

相较于上次公开发布的V20240503版本&#xff0c;变更内容如下&#xff1a; 根据掘金网友zz67373&#xff08;李浩铭&#xff09;的勘误建议&#xff0c;优化了部分描述。增加&#xff1a;金额处理规范&#xff0c;低代码报文网关实现完整代码&#xff0c;分布式流控等内容。扩…

CSS语法介绍

文章目录 前言一、CSS引入方式1.行内操作2.内部操作3.外部操作 二、常用选择器1.标签选择器2.类选择器3.id选择器4.群组选择器5.后代选择器 三、字体常用设置1.字体类型2.字体大小3.字体样式4.字体粗细 四、div盒子模型1.盒子边框2.外边距3.内边距4.浮动 综合实战案例 前言 以…

每日一题 求和

1.题目解析 求和_牛客题霸_牛客网 (nowcoder.com) 这一题&#xff0c;主要描述的就是求满足和为m的子序列&#xff0c;对与子序列的问题可以使用决策树。 2.思路分析 决策树如下图所示: 递归结束条件&#xff1a; 当当前和 sum 等于目标和 m 时&#xff0c;说明找到了一个满…

Java+Swing+Mysql实现飞机订票系统

一、系统介绍 1.开发环境 操作系统&#xff1a;Win10 开发工具 &#xff1a;Eclipse2021 JDK版本&#xff1a;jdk1.8 数据库&#xff1a;Mysql8.0 2.技术选型 JavaSwingMysql 3.功能模块 4.数据库设计 1.用户表&#xff08;users&#xff09; 字段名称 类型 记录内容…

aws 接入awsIOT平台的证书签发逻辑

参考资料 https://aws.amazon.com/cn/blogs/china/certification-vending-machine-intelligent-device-access-aws-iot-platform-solution/ IoT 设备与 AWS IoT Core 的 MQTT 通信使用基于证书的 TLS 1.2双向认证体系。所谓的双向认证&#xff0c;即意味着 IoT 设备端需安装 …

Redis 性能管理

一、Redis 性能管理 #查看Redis内存使用 172.168.1.11:6379> info memory 1. 内存碎片率 操作系统分配的内存值 used_memory_rss 除以 Redis 使用的内存总量值 used_memory 计算得出。内存值 used_memory_rss 表示该进程所占物理内存的大小&#xff0c;即为操作系统分配给…

谈谈你对 vue 的理解 ?

1.谈谈你对 vue 的理解 ? 官方: Vue是一套用于构建用户界面的渐进式框架,Vue 的核心库只关注视图层 2. 声明式框架 Vue 的核心特点,用起来简单。那我们就有必要知道命令式和声明式的区别! 早在 JQ 的时代编写的代码都是命令式的,命令式框架重要特点就是关注过程 声明…