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

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

pending位bit为1处理还是0处理,linux,linux,运维,服务器
上一章主要讲述了信号的产生:【linux】进程信号——信号的产生
这篇文章主要讲后面两个过程。

一、阻塞信号

1.1 信号的相关概念

  • 实际执行信号的处理动作称为信号递达(Delivery)。
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。

因为信号不是被立即处理的,所以在信号产生之后,递达之前的这个时间窗口称作信号未决,也就是把信号暂时保存起来。

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

注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
而且没有信号产生我们也可以选择阻塞某个信号。

1.2 在内核中的构成

我们知道发送信号的本质:修改PCB中的信号位图。 而阻塞和未决也是通过位图的方式来保存信号。它们的位图也存在于进程的PCB内。

pending位bit为1处理还是0处理,linux,linux,运维,服务器

位图的第几个比特位代表第几个信号。
对于block,比特位的内容代表是否阻塞信号
对于pending,比特位的内容代表是否收到信号
对于handler,他是一个函数指针数组,代表处理动作
信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
而只要阻塞位图对应比特位为1,那么信号永远不能递达
如果一个信号想要递达,那么pending位图对应的比特位为1,block位图对应的比特位为0。

总结:

1️⃣ 因为pending和block是两个位图,所以不会互相影响,一个信号没产生并不影响先被阻塞。
2️⃣ 进程能够识别信号是因为,内核当中有这三种结构,它们组合起来就能够识别信号。
3️⃣ 当有多个信号同时来的时候,因为位图只有一个比特位,所以只会处理一次,其他的信号都会被丢失。

二、捕捉信号概念

2.1 内核态和用户态

信号产生后不会立即进行处理,而是在合适的时候进行处理。那么什么时候是合适的时候呢?

从内核态返回用户态的时候进行处理。

如果用户态想要获得操作系统自身资源(getpid……)或者硬件资源(write……)的时候必须通过系统调用接口完成访问。
而我们无法以用户态的身份调用系统调用,必须让自己的状态变成内核态。

所以往往系统调用比较花费时间,我们应该避免频繁调用系统接口。

既然有内核态和用户态,那么我们怎么辨别我们当前是哪个身份呢?

CPU内存有寄存器,而寄存器又分为可见寄存器(EXP)和不可见寄存器(状态寄存器),而所有保存在寄存器跟当前进程强相关的数据叫做上下文数据
CPU里面有一个叫做CR3的寄存器,它表征的就是当前进程的运行级别:
0表示内核态
3表示用户态

那么一个进程是如何进入操作系统中执行方法呢?
pending位bit为1处理还是0处理,linux,linux,运维,服务器
因为操作系统会加载到内存且只有一份,所以内核级页表也只需要一份。在CPU里有一块寄存器指向这个内核级页表,进程切换时这个寄存器不变
所以进程可以在特定的区域内以内核级页表的方式访问操作系统的代码和数据。当进程想要访问OS的接口,直接在自己的进程地址空间跳转即可

而我们知道操作系统有自己的保护机制,用户凭什么能执行访问操作系统数据的接口呢?

当想要跳转到内核区会进行权限认证,如果CR寄存器显示的是内核态就可以访问,反之阻止访问。但是我们怎么把用户态切换成内核态呢?当我们调用系统接口的时候,起始的位置会帮忙改变,先把CR3中的用户态改成内核态,然后再跳转到内核区。

2.2 信号捕捉流程图

pending位bit为1处理还是0处理,linux,linux,运维,服务器
当执行代码要调用系统调用接口时,本来应该调用完成后返回用户态继续执行代码。但是我们知道用户态和内核态的转换消耗时间很大,所以这里不会直接返回。
pending位bit为1处理还是0处理,linux,linux,运维,服务器
它会去找task_struct中的三张表先遍历block,当发现为1就跳过,如果不是1就看pending表如果为1就进入handler完成动作。而默认动作直接杀死进程,忽略动作直接把pending位图中的1置为0。
但是如果时自定义动作,我们自己写的handler方法在用户态,因为内核态不能直接访问用户态(从技术上可以,但是不能,为了安全),所以又要把自己的身份变成用户态再进入用户态执行handler方法。

pending位bit为1处理还是0处理,linux,linux,运维,服务器
当我们执行完handler后能不能直接返回代码区继续执行呢?
答案是不能,因为上下文信息都还在操作系统里。所以要先回到内核,经过特殊的系统调用回到代码区继续执行代码。

pending位bit为1处理还是0处理,linux,linux,运维,服务器
我们可以把线路简化一下方便观察:
pending位bit为1处理还是0处理,linux,linux,运维,服务器

分析:

pending位bit为1处理还是0处理,linux,linux,运维,服务器
这里的绿色部位交点代表身份的切换,而箭头的指向:
向下表示从用户态切换到内核态
向上表示从内核态切换到用户态

三、信号操作

经过上面的的学习我们知道了内核中有block和pending位图,为了方便我们操作,操作系统定义了一个类型sigset_t。

#include <signal.h>
int sigemptyset(sigset_t *set);// 清0
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);// 比特位由0变为1
int sigdelset(sigset_t *set, int signo);// 比特位由1变为0
int sigismember(const sigset_t *set, int signo);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
  • 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
  • sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

3.1 sigset_t信号集

我们能看到阻塞和未决都是用一个比特位进行标记(非0即1),所以在用户层采用相同的类型sigset_t进行描述。这个类型表示每个信号有效无效的状态:在阻塞信号集就表示是否处于阻塞;在未决信号集就表示是否处于未决。
阻塞信号集有一个专业的名词叫做信号屏蔽字

3.2 信号集操作函数

sigset_t对每个信号用一个比特位表示有效或者无效的状态。它的底层操作对于我们用户层来说不必要知道,我们只能调用下面的接口函数来操作sigset_ t变量。

3.2.1 更改block表sigprocmask

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

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

RETURN VALUE
sigprocmask() returns 0 on success and -1 on error.
In the event of an error, errno is set to indicate the cause.

参数介绍:

how:怎么修改。
pending位bit为1处理还是0处理,linux,linux,运维,服务器
set:主要是用来跟how一起使用,用来重置信号。
oldset:输出型参数,把老的信号屏蔽字保存,方便恢复

3.2.2 获取pending信号集sigpending

#include <signal.h>
int sigpending(sigset_t *set);

RETURN VALUE
sigpending() returns 0 on success and -1 on error.
In the event of an error, errno is set to indicate the cause.

读取当前进程的未决信号集,通过set参数传出。 set是输出型参数。

3.3 验证

首先要知道默认情况所有信号都不会被阻塞。获取pending表对应的比特位变成1。
而如果被阻塞了,信号永远不会被递达,获取pending表对应的比特位永远为1。

static void show_pending(const sigset_t &Pending)
{
    // 信号只有1 ~ 31
    for(int signo = 31; signo >= 1; signo--)
    {
        if(sigismember(&Pending, signo))
        {
            std::cout << "1";
        }
        else std::cout << "0";
    }
    std::cout << std::endl;
}

int main()
{
    sigset_t Block, oBlock, Pending;
    // 初始化全0
    sigemptyset(&Block);
    sigemptyset(&oBlock);
    sigemptyset(&Pending);
    // 在Block集添加阻塞信号
    sigaddset(&Block, 2);
    // 修改block表
    sigprocmask(SIG_SETMASK, &Block, &oBlock);
    // 打印
    while(true)
    {
        // 获取pending
        sigpending(&Pending);
        show_pending(Pending);
        sleep(1);
    }
    return 0;
}

pending位bit为1处理还是0处理,linux,linux,运维,服务器
前面我们使用signal函数捕捉信号不能自定义捕捉9号信号,这里也是一样不能屏蔽9号信号。

当然我们也可以解除阻塞,让信号递达,信号一旦递达,pending就会先由1置0,然后就会处理信号,进程退出

static void show_pending(const sigset_t &Pending)
{
    // 信号只有1 ~ 31
    for(int signo = 31; signo >= 1; signo--)
    {
        if(sigismember(&Pending, signo))
        {
            std::cout << "1";
        }
        else std::cout << "0";
    }
    std::cout << std::endl;
}

int main()
{
    sigset_t Block, oBlock, Pending;
    // 初始化全0
    sigemptyset(&Block);
    sigemptyset(&oBlock);
    sigemptyset(&Pending);
    // 在Block集添加阻塞信号
    sigaddset(&Block, 2);
    // 修改block表
    sigprocmask(SIG_SETMASK, &Block, &oBlock);
    // 打印
    int cnt = 8;
    while(true)
    {
        // 获取pending
        sigpending(&Pending);
        show_pending(Pending);
        sleep(1);
        if(--cnt == 0)
        {
            // 恢复
            sigprocmask(SIG_SETMASK, &oBlock, &Block);
            std::cout << "恢复对信号的屏蔽" << std::endl;
        }
    }
    return 0;
}

pending位bit为1处理还是0处理,linux,linux,运维,服务器
而为什么没有打印后面那句话呢?

因为进程在内核态直接退出来,就不会返回到用户态执行代码。

四、捕捉信号操作

4.1 内核捕捉信号sigaction

在上一章【linux】进程信号——信号的产生中我们学习了捕捉信号自定义函数signal

sighandler_t signal(int signum, sighandler_t handler);

sigaction使用起来要比signal使用起来复杂。

#include <signal.h>

int sigaction(int signum, const struct sigaction *act,
              struct sigaction *oldact);

参数说明:

signum代表指定的信号。
act是一个跟函数名同名的结构体输入型参数

struct sigaction {
               void     (*sa_handler)(int); //自己写的方法
               void     (*sa_sigaction)(int, siginfo_t *, void *);// null
               sigset_t   sa_mask;// 信号集
               int        sa_flags;// 设置0
               void     (*sa_restorer)(void);// null
           };

oldact输出型参数,保存过去的数据,方便恢复。

话不多说,直接上代码:

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

void handler(int signo)
{
    std::cout << "catch signo: " << signo << std::endl;
}

int main()
{
    struct sigaction act, oact;
    // 初始化
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaction(SIGINT, &act, &oact);

    while(1) sleep(1);
    return 0;
}

pending位bit为1处理还是0处理,linux,linux,运维,服务器
我们可以看到它可以实现跟signal函数一样的功能。
那么它跟signal有什么区别呢?

我们想象这样一个场景:

假设我们在handler设置等待15秒的倒计时函数,先发送一个SIGINT信号,在自定义处理等待15s的期间再次发送一个SIGINT信号,那么会不会递归似的调用handler呢?

运行中:
pending位bit为1处理还是0处理,linux,linux,运维,服务器
结束:
pending位bit为1处理还是0处理,linux,linux,运维,服务器
现象:

我们发了许多的二号信号,但是只处理了两个
当我们处理第一个信号的时候,后边的信号不会再次被提交,当处理完后,后续信号就会递达,但是一共就两个信号递达了,后续信号全部丢失了。

结论:

当我们正在处理一个递达的信号时,同类信号无法被递达,因为当前信号正在被捕捉时,系统会自动把该信号设置进信号屏蔽字中(block)
当信号完成捕捉动作系统又会自动解除对该信号的屏蔽。

所以为什么我们发送了一堆的二号信号,处理完第一次后会处理第二次?

当一个信号被递达时,pending位图的位置就由1置为0,后边再次发送多个,又由0置为1(只有一个比特位所以只收到一个),当一个信号被解除屏蔽的时候,OS会去检查pending位图,如果被置1,就再次递达。

4.1.1 act.sa_mask参数

上面我们是捕获了2号信号,如果我们想在处理某种信号的时候顺便屏蔽其他信号,就可以添加进sa_mask信号集中。
pending位bit为1处理还是0处理,linux,linux,运维,服务器
可以看到处理二号信号的时候3号信号被屏蔽了,那么为什么最后3号信号会起作用呢?

sa_mask :在执行捕捉函数时,设置阻塞其它信号,sa mask进程阻塞信号集,退出捕捉函数后,还原回原有的阻塞信号集

五、可重入函数

假设一种场景:一个信号的处理方法是给一个链表进行头插,现在我们在main函数调用头插,而在头插的过程触发了信号的捕捉动作,又要进行头插,这样就会导致失去了头节点的位置。
pending位bit为1处理还是0处理,linux,linux,运维,服务器
因为两个执行流重复进入insert函数导致出现错误,我们把insert函数叫做不可重入函数。
没出问题就叫做可重入函数

可不可重入是个特性(中义词),我们用的大部分接口都是不可重入的。

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

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

六、volatile关键字

#include <signal.h>
#include <unistd.h>
#include <iostream>
#include <cstdio>
 
int quit = 0;
void handler(int signo)
{
    printf(" %d signo is being caught\n",signo);
    printf("quit:%d\n",quit);
    quit = 1;
    printf("->%d\n",quit);
}

int main()
{
    signal(2,handler);
    while(!quit);
    printf("i am quit\n");
    return 0;
}

pending位bit为1处理还是0处理,linux,linux,运维,服务器

这样退出是正常情况。但是如果我们让编译器进行优化:

pending位bit为1处理还是0处理,linux,linux,运维,服务器
pending位bit为1处理还是0处理,linux,linux,运维,服务器
可以看到quit确实被改为1了,但是却没有终止循环。
这里是因为编译器把quit数据优化到了寄存器中。

如果不优化,每次判断quit都需要从物理内存获取quit的内容:
pending位bit为1处理还是0处理,linux,linux,运维,服务器
而如果要优化,编译器看到main中while(!quit)并没有被修改,所以直接把quit的值放进寄存器中,不用再从物理内存中获取。
pending位bit为1处理还是0处理,linux,linux,运维,服务器
而我们后边修改quit改的是内存中的quit,并不会印象到寄存器,所以不会退出循环。这是因为寄存器的存在遮盖了物理内存的quit的值。

而加上volatile关键字就可以避免这种情况。
pending位bit为1处理还是0处理,linux,linux,运维,服务器
pending位bit为1处理还是0处理,linux,linux,运维,服务器
volatile的作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作 。文章来源地址https://www.toymoban.com/news/detail-778215.html



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

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

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

相关文章

  • 【探索Linux】—— 强大的命令行工具 P.17(进程信号 —— 信号保存 | 阻塞信号 | sigprocmask() | sigpending() )

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

    2024年02月05日
    浏览(45)
  • 【linux】信号——信号保存+信号处理

    自我名言 : 只有努力,才能追逐梦想,只有努力,才不会欺骗自己。 喜欢的点赞,收藏,关注一下把! 上一篇博客,我们已经学过信号预备知识和信号的产生,今天讲讲信号保存+信号处理以及其他补充知识。 补充一些概念。 实际执行信号的处理动作称为 信号递达(Delive

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

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

    2024年02月09日
    浏览(43)
  • 【Linux】进程信号篇Ⅰ:信号的产生(signal、kill、raise、abort、alarm)、信号的保存(core dump)

    kill -l 可以查看所有信号: 其中,前面的数字就是信号,后面的大写英文就是信号名称,实际就是宏。 我们需要关注的是 1~31 号普通信号,关注他们有没有产生(可以用 0 或者 1 表示)。 所以,进程的 pcb 中,需要对产生的信号先用 位图 保存起来,再按照一定的顺序去处理

    2024年01月25日
    浏览(39)
  • Linux进程信号【信号处理】

    ✨个人主页: 北 海 🎉所属专栏: Linux学习之旅 🎃操作环境: CentOS 7.6 阿里云远程服务器 从信号产生到信号保存,中间经历了很多,当操作系统准备对信号进行处理时,还需要判断时机是否 “合适”,在绝大多数情况下,只有在 “合适” 的时机才能处理信号,即调用信号

    2024年02月11日
    浏览(40)
  • Linux进程信号 | 信号处理

    前面的文章中我们讲述了信号的产生与信号的保存这两个知识点,在本文中我们将继续讲述与信号处理有关的信息。 之前我们说过在收到一个信号的时候,这个信号不是立即处理的,而是要得到的一定的时间。从信号的保存中我们可以知道如果一个信号之前被block,当解除

    2024年02月09日
    浏览(42)
  • 【Linux】进程信号之信号的处理

    在前面我们讲过信号产生和保存以后,我们知道进程对于产生的信号不是立即去处理的,而是在\\\"合适\\\"的时候去处理信号, 这是因为信号的产生的异步的,当前进程可能正在做更重要的事情!。 那么信号可以被立即处理吗?答案的可以的,但是要满足这个条件: 在 Linux 中如果

    2024年02月12日
    浏览(51)
  • Linux进程 ----- 信号处理

    目录 前言 一、信号的处理时机 1.1 处理时面临的情况 1.2 “合适”的时机 二、用户态与内核态 2.1 概念理论 2.2 再现 进程地址空间 2.3 信号处理过程 三、信号的捕捉 3.1 内核实现 3.2 sigaction 四、信号部分小结 从信号产生到信号保存,中间经历了很多,当操作系统准备对信号进

    2024年03月21日
    浏览(75)
  • 【linux】信号的保存和递达处理

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

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

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

    2023年04月14日
    浏览(44)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包