【linux】信号的保存和递达处理

这篇具有很好参考价值的文章主要介绍了【linux】信号的保存和递达处理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

        上节我们了解到了预备(信号是什么,信号的基础知识)再到信号的产生(四种方式)。今天我们了解信号的保存。信号产生,进程不一定立马就去处理,而是等合适的时间去处理,那么在这段时间内,进程就需要保存信号,到了合适时间再去执行!

【linux】信号的保存和递达处理

目录

一、递达,阻塞,未决

二、信号的保存

2.1 信号在内核中的数据结构构成

2.3 用户态和内核态

2.3 信号的捕捉流程

三、sigset_t 信号集        

四、信号的处理细节

4.1 对于同类型信号的处理

4.2 可重入函数和不可重入函数

4.3 volatile关键字


一、递达,阻塞,未决

        我们知道,信号是发送给进程的,而进程又是被操作系统创建pcb(信号的相关信息被保存到进程pcb中)而进行管理的,所以修改或者访问进程pcb都需要操作系统来进行,那么信号发送的本质就是:操作系统在向进程发送信号。

       信号产生,进程不一定立马就去处理,而是等合适的时间去处理,那么在这段时间内,进程就需要保存信号,到了合适时间再去执行!那么实际执行信号的处理动作称为信号递达;信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞 (Block )某个信号。

        被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。我们之前知道,进程递达之后的动作有三种:默认动作、自定义动作、忽略动作(执行动作,只不过这个动作就是什么都不做)。注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。


二、信号的保存

        我们知道信号是保存到进程pcb中的,信号产生、信号递达、信号阻塞、信号未决这些到底怎么实现的呢?我们来看:

2.1 信号在内核中的数据结构构成

【linux】信号的保存和递达处理

        上图就是信号在内核中的数据结构构成,我们来慢慢了解。首先信号的相关信息都在进程pcb中存储,判断信号发送给进程后的状态都是位图来实现的。

        unsigned int pending = 0;这是信号未决的位图结构,一共有32个比特位,分别代表32个进程信号的编号,当然比特位的内容(0/1)也代表进程是否收到了对应的信号,收到信号但未递达,对应编号的比特位就会由0改为1。

        unsign int block =0 ;这是信号阻塞的位图结构,一共有32个比特位,分别代表32个进程信号的编号,当然比特位的内容(0/1)也代表进程是否阻塞了对应的信号,收到信号被阻塞,对应编号的比特位就会由0改为1。

        如果某个信号被阻塞,那么阻塞位图结构中对应的比特位(信号编号)就会置为1,那么在此信号阻塞未被解除之前,会一直处于信号未决(信号产生但未被处理)非阻塞被解除。

        handler_t handler[32] :信号递达后要处理动作,那么handler这个数组中一定存放着信号编号所对应的处理动作。handler_t 其实是函数指针类型,typedef void(*handler)(int signo); 参数是信号编号,返回值是void的函数指针。数组的下标就是对应的信号编号,数组下标中的内容就是对应信号的处理方法(函数指针)。

        当调用signal(signo,handler); ,就会把信号对应的处理方法设置为自定义方法,内核中就是将数组下标(信号编号)中的内容(处理方法)设置为自定义方法的函数指针。从而在递达后执行处理方法。

        所以我们知道,为什么进程可以识别信号呢?原来是因为程序员在设计进程的时候,已经为进程设计好了这三种结构,从而去识别信号!


2.3 用户态和内核态

        信号产生时,进程可能不会立马去处理,而是等待合适的时机,那么这个合适的时机是什么时候呢?是从内核态返回到用户态!哦吼,那什么是用户态和内核态呢?我们来看:

【linux】信号的保存和递达处理

        我们编写的代码一般都是用户层级的代码,那当我们去调用接口去访问os自身的资源(getpid等等),去printf(访问硬件资源)的时候,这就需要我们切换身份为内核态去执行这些操作!访问不同的资源始终是进程,但是当他的身份不同的时候,那么可以访问的资源就是不同的!

        用户为了访问内核或者硬件资源,必须通过系统接口完成访问。那么系统调用肯定是比进程互相调用用户态层级的代码慢得多,因为他需要身份的切换等等,所以我们尽量避免频繁的调用系统接口。(这就是为什么vector中的扩容他需要一次性去扩充1.5/2倍的空间,因为这样就可以避免频繁的扩容,导致频繁的去调用系统接口,导致速度和效率大大下降)

        那么我们就会想,那到底是怎么操作这个身份的呢?如何就知道它是内核态或者用户态的呢?我们都知道进程在执行时,会将此进程的上下文投递到cpu的寄存器中,那么此时cpu中还有很多寄存器存放着不同的信息:

【linux】信号的保存和递达处理

        cpu内部的寄存器分为:1.可见寄存器 2.不可见寄存器。其中,有存放着进程pcb的起始地址的寄存器(这样就可以访问进程的所有信息),有存放页表起始地址的寄存器,也有存放着当前进程的运行级别的寄存器(利用位图结构,来表示不同的级别),所以当进程去访问内核的资源的时候,os就会到cpu的CR3去看进程的运行级别,如果处于内核态,那可以访问,反之

        我们了解了访问的条件,但是他到底是如何到os中访问资源呢?来看:

【linux】信号的保存和递达处理

        每一个进程都有[3,4]G的内核空间,[1,3]G的用户空间,且都享有同一个内核级页表。 

        之前我们知道,当动态库加载到物理内存时,是可以通过页表映射到进程空间的共享区,之后在执行代码若执行到共享区的代码时,就会在当前地址空间(起始地址+偏移量的方式)去跳转到共享区去执行代码,执行完毕后,再回到对应执行的代码。每一个进程他都有自己的一套内核结构(进程的独立性),且都有不同的用户级页表。

        但若去访问操作系统的资源,因为操作系统只有一个,当开机时,操作系统的资源会被加载到物理内存,进程访问时,通过同一个内核级页表。所以无论进程怎么切换,都不会更改3-4G的内核空间。

        那什么时候从用户态切换到内核态呢?系统调用的最开始。(根据 Int 80(汇编代码),会把寄存器中的进程运行级别状态修改。(系统调用最开始就设计了这样))


2.3 信号的捕捉流程

        我么们了解了内核态和用户态以后,就可以了解到,原来信号产生,不会立即被进程所处理动作,而是等到合适的时机去处理,这个合适的时机就是内核态切到用户态的时候。那我们一定之前就进入了内核态,我们来看:

【linux】信号的保存和递达处理

         当进程需要访问内核资源的时,就会通过系统调用来切换身份,由用户态切换到内核态,之后进行系统调用(cpu中改变身份,通过内核级页表去访问内核资源),到这里本应该就是切换到用户态返回的,但是来都来了,而且切换到内核态确实不容易。所以就会通过进程中的pending,block,headler进行信号的检测过程(先在pending中查看信号是否存在,再到block中查看是否被阻塞,如果阻塞则该信号处于未决,继续查看pending中的下一个信号,如果没有被阻塞,那就信号递达,通过handler去处理动作(默认、自定义、忽略)。当然在信号递达前,会将pending中该信号对应的比特位由1变为0,再去执行。

        忽略其实最容易执行,只需要将pending中1改为0以后,啥都不做;而自定义就需要再将身份切换为用户态,然后去执行handler中的方法。那为什么不直接在内核态中去执行用户态中的方法呢?是因为操作系统不信任任何人,如果用户态的代码是问题代码,那么就会导致操作系统出现严重问题,所以会先切换用户态,再去执行handler中对应的方法(用户态执行一些代码会受到限制)。递达后为什么不直接回到进程中呢?是因为我们没办法直接回到当前进程执行的位置,这个过程需要操作系统的操作。所以只能再回到内核态,再由内核态切到用户态回到进程执行的位置。

        我们直接抽象看本质:

【linux】信号的保存和递达处理

        四个交点(四次身份切换)

        在用户态中因为一些原因陷入内核,执行系统调用后,在内核态中再进行信号的检测过程,再由内核态切换到用户态执行方法,完毕后再切换身份回到内核态,通过信号检测结束后,再身份切换,回到进程执行流中上次中断的地方。


三、sigset_t 信号集        

        我们知道信号是在进程的pcb中,即内核中。所以用户级操作难免会困难一些。所以sigset_t 信号集就是为了更好的在用户级操作信号所产生的类型。sigset_t 信号集包括 pending信号集、信号屏蔽字(block信号集)。sigset_t 底层就是一个大数组实现的位图结构。

信号集操作函数:

#include <signal.h>
int sigemptyset(sigset_t *set);                  //初始化set信号集
int sigfillset(sigset_t *set);                        //将信号集set全部设置为1
int sigaddset (sigset_t *set, int signo);     //往set信号集添加信号
int sigdelset(sigset_t *set, int signo);       //删除set信号集中的信号
这四个函数都是成功返回0,出错返回-1
int sigismember(const sigset_t *set, int signo);        //判断信号是否在set中
sigismember 是一个布尔函数 , 用于判断一个信号集的有效信号中是否包含某种信号, 若包含则返回 1, 不包含则返回 0, 出错返回 -1
sigprocmask
调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字 ( 阻塞信号集)
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
how就是下面的几种方式:
【linux】信号的保存和递达处理
返回值 : 若成功则为 0, 若出错则为 -1
sigpending
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值 : 若成功则为 0, 若出错则为 -1
读取当前进程的未决信号集 , 通过 set 参数传出。调用成功则返回 0, 出错则返回 -1。

下面我们利用上面所学,来实现一个观察pending信号集,通过信号屏蔽子来观察pending信号集的变化:

#include <iostream>
#include <vector>
#include <signal.h>
#include <unistd.h>

// #define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31

using namespace std;


static vector<int> sigarr = {2}; 

//输出pending信号集
static void show_pending(const sigset_t &pending)
{
    for(int signo = MAX_SIGNUM; signo >= 1; signo--)
    {
        if(sigismember(&pending, signo))
        {
            cout << "1";
        }
        else cout << "0";
    }
    cout << "\n";
}

//递达自定义动作
static void myhandler(int signo)
{
    cout << signo << " 号信号已经被递达!!" << endl;
}

int main()
{
    for(const auto &sig : sigarr) signal(sig, myhandler);

    // 1. 先尝试屏蔽指定的信号
    sigset_t block, oblock, pending;

    // 1.1 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);

    // 1.2 添加要屏蔽的信号
    for(const auto &sig : sigarr) sigaddset(&block, sig);  //批量化屏蔽

    // 1.3 开始屏蔽,设置进内核(进程)
    sigprocmask(SIG_SETMASK, &block, &oblock);

    // 2. 遍历打印pengding信号集
    int cnt = 10;
    while(true)
    {
        // 2.1 初始化
        sigemptyset(&pending);
        // 2.2 获取它
        sigpending(&pending);
        // 2.3 打印它
        show_pending(pending);
        // 3. 慢一点
        sleep(1);
        if(cnt-- == 0)
        {
            sigprocmask(SIG_SETMASK, &oblock, &block); // 一旦对特定信号进行解除屏蔽,一般OS要至少立马递达一个信号!
            cout << "恢复对信号的屏蔽,不屏蔽任何信号\n";
        }
    }
}

四、信号的处理细节

4.1 对于同类型信号的处理

        当我们正在递达一个信号期间,同类型的信号无法被递达!(信号的处理细节)

        当信号正在被递达中,又来了同类型的信号,此时当前信号会被加入到进程的信号屏蔽字,且会将pending中该信号对应的那一位由0变为1。(因为该信号被递达前,会将pending中对应的那一位由1改为0),若结束递达后,同类型仍发送,则会继续重复上面的动作。但若结束递达后,同类型的信号没有发送了,进程就只会再捕捉一次,将pending中的1改为0。递达后则继续检其他信号进行递达。

        进程处理信号的原则是穿行的处理同类型的信号,不允许递归处理!


4.2 可重入函数和不可重入函数

        举例说明:

【linux】信号的保存和递达处理

         在main执行流中,没有头结点的单链表进行头插,如上图所示:在执行到第一步时,此时被信号中断,结果导致main中还没有执行完又进入insert()中,最后回到main执行流中,再执行完剩下的代码结果导致内存泄漏等问题。

        1.一般而言,main执行流和信号捕捉执行流是两个执行流!

        2.如果在main中,和在handler中,该函数被反复进入:1出现问题的就是不可重入函数;2.没有出现问题的就是可重入函数。当然可重入和不可重入只是他们的特性,没有好坏之分。


4.3 volatile关键字

        我们在读取变量的值时,一般会从内存中读取,但是由于编译器的优化,就会将内存中的值加载到cpu的寄存器中,从而之后访问该变量的值只会从寄存器中读取,如果这个变量的值被修改了,自然而然内存上的值也被修改了,但是寄存器中的值仍然没有变化,还是修改之前的值,所以为了避免这种优化产生的后果,我们就会在变量前加上volatile,意为一直从内存中读取值!


总结:

        我们了解了信号的保存原来是通过进程pcb中的pending、block位图,handler函数指针数组来进行保存,从而信号递达。 【linux】信号的保存和递达处理文章来源地址https://www.toymoban.com/news/detail-477477.html

到了这里,关于【linux】信号的保存和递达处理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【linux】进程信号——信号的保存和处理

    上一章主要讲述了信号的产生:【linux】进程信号——信号的产生 这篇文章主要讲后面两个过程。 实际执行信号的处理动作称为 信号递达 (Delivery)。 信号从产生到递达之间的状态,称为 信号未决 (Pending)。 因为信号 不是被立即处理的 ,所以在信号产生之后,递达之前的这个

    2024年02月03日
    浏览(40)
  • 【Linux从入门到精通】信号(信号保存 & 信号的处理)

      本篇文章接着信号(初识信号 信号的产生)进行讲解。学完信号的产生后,我们也了解了信号的一些结论。同时还留下了很多疑问: 上篇文章所说的所有信号产生,最终都要有OS来进行执行,为什么呢? OS是进程的管理者 。 信号的处理是否是立即处理的? 在合适的时候。

    2024年02月09日
    浏览(43)
  • 【Linux学习】信号——信号保存 | 信号处理 | 不可重入函数,volatile,SIGCHLD信号

    🐱作者:一只大喵咪1201 🐱专栏:《Linux学习》 🔥格言: 你只管努力,剩下的交给时间! 信号的产生以及详细讲解了,有兴趣的小伙伴可以去看看,传送门。接下来介绍信号的保存和信号处理。 首先介绍几个新的概念: 信号递达(Delivery):实际执行信号的处理动作。 信号

    2023年04月14日
    浏览(43)
  • Linux信号之信号的保存

    (。・∀・)ノ゙嗨!你好这里是ky233的主页: 这里是ky233的主页,欢迎光临~ https://blog.csdn.net/ky233?type=blog 点个关注不迷路⌯\\\'▾\\\'⌯ 目录 一、阻塞信号 1.信号递达、未决、阻塞  2.内核表示图 3.sigset_t 4.sigpending 5.sigprocmask 二、验证问题 1.问题一 2.问题二 3.问题三 实际执行信号的

    2024年01月16日
    浏览(41)
  • 【Linux】进程信号之信号的保存

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

    2024年02月13日
    浏览(40)
  • 【Linux】 信号的保存 | 捕捉

    对于信号,主要涉及到信号的产生、保存和捕获,之前谈到了信号的产生,这里主要介绍信号产生后如何进行保存和捕捉处理的原理。 相关概念 实际执行处理信号的动作称为 信号递达Delivery 信号从产生到递达的过程称为 信号未决Pending 进程可以阻塞、忽略某个信号。 被阻塞

    2024年02月21日
    浏览(37)
  • 【Linux】信号的保存

    信号的小细节真的很多~ 文章目录 前言 一、信号的保存 总结 首先我们先引出一个新的概念,叫 核心转储 。linux系统提供了一种能力,操作系统可以将一个进程在异常的时候将核心代码部分进行核心转储,将内存中进程的相关数据全部dump到磁盘中,一般这个文件会在当前进

    2024年02月04日
    浏览(37)
  • 【Linux】信号的保存和捕捉

    我们知道,操作系统是进程的管理者,只有操作系统才有资格向进程发信号,具体点,是给进程的PCB发信号。 更具体点,就是将进程的task_struct中的signal整形的某一个比特位由0置1!!! 那么该信号如何被保存下来呢? 实际执行信号的处理动作称为信号递达(Delivery) 信号从产

    2024年02月05日
    浏览(34)
  • 【Linux】进程信号(完整版) --- 信号产生 信号保存 信号捕捉 可重入函数 volatile SIGCHLD信号等

    🍎 作者: 阿润菜菜 📖 专栏: Linux系统编程 我们想要杀死某个后台进程的时候,无法通过ctrl+c热键终止进程时,我们就会通过kill -9的命令来杀死信号。 查看信号也比较简单,通过 kill -l 命令就可以查看所有信号的种类,虽然最大的信号编号是64,但实际上所有信号只有6

    2024年02月04日
    浏览(50)
  • 【探索Linux】—— 强大的命令行工具 P.17(进程信号 —— 信号保存 | 阻塞信号 | sigprocmask() | sigpending() )

    在计算机科学领域,信号是一种重要的通信机制,用于处理各种系统事件和进程间的通信。Linux作为一个开源操作系统,以其稳定性和高度可定制性而闻名。在Linux下,信号的处理是实现进程间通信和事件处理的关键机制之一。 本文将继续探讨Linux下信号的相关主题,着重介绍

    2024年02月05日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包