Linux: 系统内核中的信号

目录

 一 前言

二  信号在内核中的表示

三 sigset_t   

四 信号集操作

1. sigpending() 

2. sigemptyset()

3. sigfillset()

4. sigaddset ()和sigdelset() 

5. sigismember()

6. sigprocmask()

五 深入理解信号的捕捉流程 




 一 前言

在Linux: 进程信号初识-CSDN博客信号的初识这一篇我们已经了解了什么是信号,和信号的产生及信号的捕捉,但是那些都是信号在用户层的理解,同时也产生了几个问题: 

  • 之前讲到的所有进程信号的产生都需要OS来执行,为什么?

  • 我们提到进程在接收到信号之后通常有着三种处理方式(1. 忽略此信号。 2. 执行该信号的默认处理动作。 3. 自定义处理动作),那么针对这三种处理方式,进程是立即处理的吗?如果不是立即处理,那么信号是否需要暂时被进程记录下来?记录下来放在哪里呢?

  • 一个进程在没有收到信号的时候,怎么知道自己应该对合法信号作何处理呢?

  • 怎么理解OS向进程发送信号?是怎么发送的?具体情况是什么?

接下来我们将从系统内核层面着重讨论和理解进程信号产生之后进程处理信号的详细操作以及进程信号的产生到进程接收之间内核做了哪些事情。

为了后续学习,我们需要知道信号其他相关概念

  1. 执行信号的处理动作称为信息递达(Delivery)
  2. 信号从产生到递达之间的状态成为信号未决(Pending)
  3. 进程可以选择阻塞(Block)某个信号。
  4. 被阻塞的信号产生时将保持在未决状态,直到进程接触对信号的阻塞。才执行递达的动作。
  5. 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是递达之后的可选的一种处理动作。

二  信号在内核中的表示

在上篇Linux: 进程信号初识-CSDN博客,我们简单提到过,进程信号保存是以位图的方式存储在进程的PCB中的,通常每个信号对应位图中的一个特定位置,即一个比特位,如果该比特位设置为1,表示对应信号已经收到但是尚未处理;若为0.则表示没有收到该信号。事实上,在PCB中描述着一个有关进程信号的位图和一个有关进程信号的指针数组如下

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志(pending),位图比特位设为1,直到信号递达才清除该标志。在上图的例子 中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前 不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,也就是说阻塞信号,可以在没有信号传递时就可以阻塞,它的处理动作是用户自定义函数sighandler。
  • 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理? POSIX.1允许系统递送该信号一次 或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可 以依次放在一个队列里。本章不讨论实时信号。
  • pending位图:pending表示信号未决,所以它是未决位图,用来表示进程收到了信号,对应位置即为对应编号的信号,当该位图中的某个位置设置为1时,即表示此位的信号在进程中处于未决状态,即接收到了信号但是还未处理。

  • handler指针数组:显而易见,存储的是信号处理方法的数组,每位对应一个处理方法。

  • block位图:阻塞位图,表示对应位置的进程信号是否阻塞,当指定位置为1时,即表示此位置的信号会被阻塞  


三 sigset_t   

 从上面来看,pending位图和block位图所表示的信息能力都是有限的,其每一位的0 1都只能表示进程信号是否存在或者阻塞并不能表示有多少信号产生 并 发送给了进程。

在Linux操作系统中pending和block并不是以整型来表示位图的,而是以一个结构体的形式sigset_t

sigset_t 是一个 typedef 出来的类型, 实际上是一个结构体 __sigset_t, 不过这个结构体内部只有一个 unsigned long int 类型的数组 ,也就是说pending和block位图其实是以数组的形式表现出来的。

其中, 实际以 sigset_t 形式表现的 pending位图, 被称为未决信号集; 同样以 sigset_t 形式表现的 block位图, 被称为阻塞信号集, 也叫信号屏蔽集。


四 信号集操作

信号集实际上是以数组来表示位图的,且系统为用户提供了相关的系统调用接口:

int sigpending(sigset_t *set);
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(const sigset_t *set, int signo);

1. sigpending() 

该接口的作用是检查未决信号集,即获取进程的未决信号集。其参数是一个输出型参数,获取的未决信号集的內容会存储在传入的变量中,成功则返回0,错误返回-1. 

2. sigemptyset()

调用此接口会将传入的信号集初始化为空, 即所有信号、阻塞会被消除, 信号集的所有位设置为0成功返回0, 错误返回-1

3. sigfillset()

调用此函数, 会将传入的信号集所有位设置为1.成功返回0, 错误返回-1

4. sigaddset ()和sigdelset() 

前者的作用是, 给指定信号集中添加指定信号, 即将指定信号集中的指定位置设置为1

后者的作用是, 删除指定信号集中的指定信号, 即将指定信号集中的指定位置设置为0

这两个函数, 都是成功返回0, 失败返回-1

5. sigismember()

调此函数, 可以判断信号集中是否有某信号  即  判断信号集的某位是否为1

如果信号在信号集中返回1,如果不在返回0,如果出现错误 则返回-1

6. sigprocmask()

这个接口的作用是:获取 和 修改 信号屏蔽集合(阻塞信号集) 

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信 号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后 根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

下面我用图片的形式 解释一下:

  1.为指定位置添加阻塞

2.为指定信号解除阻塞

3.直接设置信号屏蔽字

 直接将传入的set覆盖进程原来的信号屏蔽字, 将传入的set作为进程新的信号屏蔽字 

 测试:

#include <iostream>
#include <signal.h>
#include <unistd.h>#define BLOCK_SIGNAL 2 //需要屏蔽信号2
#define MAX_SIGNUM 31static void show_pending(const sigset_t & pending)
{for(int signo=MAX_SIGNUM;signo>=1;signo--){if(sigismember(&pending,signo)){std::cout<<"1";}else  std::cout<<"0";}std::cout<<"\n";
}int main()
{//先尝试屏蔽指定信号sigset_t block,oblock,pending;//1.初始化sigemptyset(&block);sigemptyset(&oblock);sigemptyset(&pending);//1.2添加要屏蔽的信号sigaddset(&block,BLOCK_SIGNAL);//1.3开始屏蔽sigprocmask(SIG_SETMASK,&block,&oblock);//2.遍历打印pending 信号集while(true){//2.1初始化sigemptyset(&pending);//2.2获取sigpending(&pending);//2.3打印show_pending(pending);sleep(1);}return 0;
}

五 深入理解信号的捕捉流程 

在前言部分,我们曾说过信号产生的时候,不会立即被处理,而是在合适的时候,那是在什么时候呢?

🍉:从内核态返回用户态的时候,进行处理。所以什么是内核态?什么又是用户态呢?

1.进程的内核态与用户态

我们知道对于系统中的每一个进程其都有自己的一份独立的程序地址空间

且进程地址空间与物理内存是通过页表映射的。但是之前讲到的Linux : 进程地址空间-CSDN博客只是用户空间部分与物理内存之间的相互映射。 

事实上 ,对于1GB的内核空间,也存在着一张页表,用于内核空间和物理内存之间的相互映射,称为内核级页表

如上图所示,所有的进程都用着同一张内核级页表,也就是说每个进程的内核空间的内容是一样的,也就是说物理内存中只加载着一份有关于进程内核空间内容的数据代码。

如果每个进程都可以访问及随意修改内核空间中的数据代码,这是一件很恐怖的事情,毕竟操作系统做了那么多工作,提供了那么多系统调用封装了那么多系统接口,就是为了不让用户直接操作系统内核。所以为了保护这部分数据代码,进程会分为两种状态: 内核态 和 用户态

🌔当进程 需要访问、调用、执行 内核数据或 代码(系统调用等)时, 就会 陷入内核, 转化为内核态, 因为只有进程处于内核态时, 才有权限访问内核级页表, 即有权限访问内核数据与代码。

🌖当进程不需要访问、调用、执行内核数据或代码或系统调用结束时, 就会返回用户, 转化为用户态 , 此时 进程将不具备访问内核级页表的权限, 只能访问用户级页表

 那么系统如何分清当前的进程处于哪一种状态下呢?

事实上,在CPU内部存在着一个 状态寄存器CR3,此寄存器内有比特标识位标识当前进程的状态。

若标识位 表示0, 则表明进程此时处于内核态
若标识位 表示3, 则表明进程此时处于用户态 

在操作系统中,当进程处于运行时,它会有两种状态,用户态和内核态,且在进程的整个周期内会发生着无数次的状态转换。我们平常写的代码大部分情况下是没有资格直接访问系统的软硬件资源的 ,本质上我们都是通过调用系统所提供的接口,通过系统去访问这些资源,这样的情况下,进程需要访问硬件资源地时候,就会无数次地陷入内核(切换状态、切换页表),再访问内核代码数据, 然后完成访问,再将结果返回给用户(切换状态,切换页表),最终用户得到结果。

还有一种情况,在用户不调用任何函数的时候,这时候还会发生进程状态的转换吗?答案是会的。因为只要是进程, 那么他就有一定的时间片. 即使是一个什么都不执行的死循环, 只要时间片用完了, 那么就需要将此进程从CPU上剥离下来, 而剥离操作一定是操作系统做的, 那么也就是说将 进程从CPU上剥离下来也是需要陷入内核执行内核代码的. 将进程从CPU上剥离下来的时候, 需要维护一下进程的上下文, 以便下次接着执行进程的代码.剥离下来之后, 操作系统执行调度算法, 将下一个需要运行的进程的上下文加载到CPU中。

🍰:下面我们举个例子详细分析一下在进程切换状态的时候,信号在什么时候处理。

如图所示:

  • 代码在运行到需要执行系统调用signal()接口的时候,此时进程就需要陷入内核态执行signal()代码
  • 陷入内核并执行完signal()代码后, 需要将signal()结果返回给用户, 需要转换回用户态
  • 在转换回用户态之前, 需要先在进程PCB中检测进程的未决信号集
  • 在未决信号集中, 检测到1和2信号未决, 并且均未被屏蔽(阻塞). 就需要在handler数组中寻找指定的处理方法
  • 1信号默认处理, 需要执行内核中的默认处理方法(一般为进程终止); 2信号忽略处理, 直接将未决信号集中2信号改为0
  • 处理完信号, 再将signal()结果返回给用户, 这个过程需要转换为用户态

🍅:上面的信号的处理方式都是默认或者是忽略,但是如果我们捕捉了一个信号并且让它按照自定义的方式处理,这时候在最后一步怎么办?

    进程首先会从内核态切换为用户态去执行用户自定义的信号处理动作,(虽然内核态也能处理用户态的代码,但是操作系统不会这样做,因为万一用户自己写个Bug,内核态去执行的话会影响操作系统。)

           其次进程现在是用户态,此时进程是无法返回到进程原本代码的执行位置的。因为进程执行系统调用之后的返回信息还在内核中,以用户态的身份是无法访问并返回给用户的。 所以, 进程还需要再次陷入内核,转换成内核态 然后根据内核中的返回信息使用特定的返回调用 返回到用户.

图示如下:

🍎可以看到如果处理信号需要执行用户自定义的处理方法时, 那么 从调用内核代码到返回用户的整个过程一共需要经历4次状态转换

🍏而, 如果处理信号不需要执行用户自定义处理方法时, 那么 从调用内核代码到返回用户的整个过程 就只需要经历2次状态转换

简化图如下:

缩略图:

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

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

相关文章

Nginx-keepalived-高可用

Nginx 高可用 通常 借助 Keepalived 实现&#xff0c; Keepalived 能通过 VRRP &#xff08;虚拟路由冗余协议&#xff09;让多个 Nginx 服务器 组成一个 热备集群&#xff0c;当主服务器故障时自动切换到备用服务器&#xff0c;保障服务不间断。 一、环境准备 角色IP 地址主…

使用python完成手写数字识别

入门图像识别的第一个案例,看到好多小伙伴分享,也把自己当初的思路捋捋,写成一篇博客,作为记录和分享,也欢迎各位交流讨论。 实现思路 数据集:MNIST(包含60,000个训练样本和10,000个测试样本) 深度学习框架:Keras(基于TensorFlow) 模型架构:卷积神经网络(CNN) 实…

Java学习总结-多线程-三种创建方法

什么是线程&#xff1f; 线程&#xff08;Thread&#xff09;是程序内部的一条执行流程。 程序如果只有一条执行流程&#xff0c;那这个程序就是单线程程序。 什么是多线程&#xff1f; 多线程是指从软硬件上实现的多条执行流程的技术&#xff08;多条线程由CPU负责调度执行…

电动垂直起降飞行器(eVTOL)

电动垂直起降飞行器&#xff08;eVTOL&#xff09;的详细介绍&#xff0c;涵盖定义、技术路径、应用场景、市场前景及政策支持等核心内容&#xff1a; 一、定义与核心特性 eVTOL&#xff08;Electric Vertical Take-off and Landing&#xff09;即电动垂直起降飞行器&#xf…

ensp 网络模拟器 思科华为基于VLANIF的公司网络搭建

该文章仅记录作业配置过程 如有雷同纯属巧合 一. 其它&#xff08;共1题&#xff0c;100分&#xff09; 1. (其它) 为大学生公司创建部门VLAN 1.项目 背景 为大学生公司现有财务部、技术部和业务部&#xff0c;出于数据安全的考虑&#xff0c;各部门的计算机需进行隔离。公…

使用`sklearn`中的逻辑回归模型进行股票的情感分析,以及按日期统计积极和消极评论数量的功能

以下是完成上述任务的Python代码&#xff0c;可在Jupyter Notebook中运行。此代码包含了使用sklearn中的逻辑回归模型进行情感分析&#xff0c;以及按日期统计积极和消极评论数量的功能。 import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer f…

oracle批量删除分区

为了清理数据&#xff0c;往往需要删除一些分区 简单查看当前分区 附件 --创建测试表 -- drop table test_part purge;CREATE TABLE test_part (sales_id NUMBER,sale_date DATE,amount NUMBER ) PARTITION BY RANGE (sale_date) INTERVAL (INTERVAL 1 MONTH) -- 每个月创建…

java流程控制08:For循环

For循环 虽然所有循环结构都可以用while或者do…while表示&#xff0c;但Java提供了另一种语句-----for循环&#xff0c;使一些循环结构变得更加简单。 for循环语句是支持迭代的一种通用结构&#xff0c;是最有效、最灵活的循环结构。 for循环执行的次数是在执行前就确定的。…

嵌入式软件开发调试方法

文章目录 1. 利于函数返回值,retrurn 定位错误位置2. 合理使用逻辑分析仪&#xff08;正点原子 厉害&#xff01;&#xff01;&#xff09; 1. 利于函数返回值,retrurn 定位错误位置 如下图所示&#xff0c;设置不同的返回值&#xff0c;0是ok的&#xff0c;其他值均为失败&…

P1025 [NOIP 2001 提高组] 数的划分(DFS)

题目描述 将整数 n 分成 k 份&#xff0c;且每份不能为空&#xff0c;任意两个方案不相同&#xff08;不考虑顺序&#xff09;。 例如&#xff1a;n7&#xff0c;k3&#xff0c;下面三种分法被认为是相同的。 1,1,5; 1,5,1; 5,1,1. 问有多少种不同的分法。 输入格式 n,k …

设计模式简述(三)工厂模式

工厂模式 描述简单工厂&#xff08;静态工厂&#xff09;工厂方法模式 抽象工厂增加工厂管理类使用 描述 工厂模式用以封装复杂的实例初始化过程&#xff0c;供外部统一调用 简单工厂&#xff08;静态工厂&#xff09; 如果对象创建逻辑简单且一致&#xff0c;可以使用简单工…

批量将 JSON 转换为 Excel/思维导入等其它格式

json 格式相信对大家来说都不陌生&#xff0c;这是一种轻量级的结构化数据&#xff0c;可以对对象进行描述。json 格式也是一种普通的文本文件格式&#xff0c;用记事本就能够打开编辑 json 格式的文件&#xff0c;可以很方便的转换为其他格式。今天要给大家介绍的就是如何将 j…

电脑有时出现检测不到音箱设备怎么办?

问题 有时候电脑开机之后就检测不到音箱&#xff0c;经过我一顿检查发现是检测不到声卡&#xff0c;即使拔插了音箱也没用&#xff0c;但是当我重启或者休眠之后再重启发现就检测到了 解决方案 方案一 重启或者休眠之后再开启 方案二 使用powershell指令将声卡弹出和载入…

Qwen-Agent框架的文件相关操作:从Assistant到BasicDocQA

在前面的几篇文章如《针对Qwen-Agent框架的Function Call及ReAct的源码阅读与解析&#xff1a;Agent基类篇》 、《基于Qwen-Agent框架的Function Call及ReAct方式调用自定义工具》、 《针对Qwen-Agent框架的源码阅读与解析&#xff1a;FnCallAgent与ReActChat篇》中&#xff0c…

RSSI定位程序,N个锚点、三维空间,使用CKF对轨迹进行滤波,附MATLAB代码的下载链接

本文所述的程序实现三维空间中基于RSSI信号的多锚点定位&#xff0c;并采用容积卡尔曼滤波&#xff08;CKF&#xff09;对动态轨迹进行降噪优化。代码包含完整的定位仿真流程&#xff0c;涵盖环境建模、信号强度模拟、定位解算、轨迹滤波及可视化分析模块 文章目录 程序介绍概述…

开源软件与自由软件:一场理念与实践的交锋

在科技的世界里&#xff0c;“开源软件”和“自由软件”这两个词几乎无人不知。很多人或许都听说过&#xff0c;它们的代码是公开的&#xff0c;可以供所有人查看、修改和使用。然而&#xff0c;若要细究它们之间的区别&#xff0c;恐怕不少朋友会觉得云里雾里。今天&#xff0…

C++ - 头文件基础(常用标准库头文件、自定义头文件、头文件引入方式、防止头文件重复包含机制)

一、头文件 在 C 中&#xff0c;头文件&#xff08;.h&#xff09;用于函数声明、类定义、宏定义等等 在 Visual Studio 中&#xff0c;头文件通常放在头文件目录中&#xff0c;头文件实现通常放在源文件目录中 二、常用标准库头文件 1、输入输出 <iostream> 标准输入…

CSS 背景属性学习笔记

一、CSS 背景属性概述 CSS 背景属性用于定义 HTML 元素的背景效果&#xff0c;主要包括以下几种属性&#xff1a; background-color&#xff1a;定义元素的背景颜色。 background-image&#xff1a;定义元素的背景图像。 background-repeat&#xff1a;定义背景图像如何重复…

Qt实现鼠标拖动窗口

Qt实现鼠标拖动窗口 1、设置窗口无边框2、重写鼠标点击&#xff0c;移动函数2.1添加头文件2.2 重写函数2.3 添加定义 3、定义一个偏移值4、判断鼠标左键是否按下并计算偏移值5、移动窗口6、.h文件和.cpp文件6.1 .h文件6.2 .cpp文件 7、总结 1、设置窗口无边框 this->setWin…

MDX语言的数论算法

MDX语言的数论算法探讨 引言 数论作为数学的一个重要分支&#xff0c;主要研究整数及其性质。在计算机科学和信息技术领域&#xff0c;数论算法被广泛应用于密码学、算法设计、数据加密等领域。MDX&#xff08;Multi-Dimensional Expressions&#xff09;语言&#xff0c;虽然…