【1++的Linux】之信号(二)

news2024/9/21 1:42:02

👍作者主页:进击的1++
🤩 专栏链接:【1++的Linux】

文章目录

  • 一,信号的保存
  • 二,信号处理
    • 1. 信号处理的时间

一,信号的保存

我们在上一篇文章中讲述了信号的概念和信号的产生,并且我们知道了信号在发送给对应进程后,并不会被进程立即处理,而是进程会在合适的时间去进行处理。那么,在信号未处理的这段时间,信号在哪呢?这就是我们今天所要说的信号的保存。

我们先来看一幅图。
在这里插入图片描述

我们将实际信号处理的动作叫做信号递达
将信号产生和递达之间的状态叫做信号未决
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

上述图中就是我们信号在内核中的表示示意图。
我们在上一篇中提到过,信号的数量是有限的,并且其本质是一些编号,因此我们可以用位图将其存储起来。我们的pending位图就是用来保存未决信号的。block位图是用来保存阻塞信号的,而handler中则存储的是,我们对应信号的处理方法的指针。我们的signal系统调用,实质上就是在该进程中,找到其handler表,若我们是在自定义二号信号的捕捉方法,则将handler[2]中的内容替换为我们自定义的处理方法的函数指针。(数组的下标就是我们要处理的信号) 。

在这里插入图片描述
信号的处理有默认,忽略,自定义,三种方式。当我们自定义处理信号时,进程先会判断handler[signum]==SIG_DFL?若等于则执行默认动作,若不等于,则判断handler[signum]==SIG_IGN?若等于则执行忽略,否则才会执行我们自定义的捕捉动作。

若signal()函数出错则返回SIG_ERR

二,信号处理

当我们的一个信号产生后,其先是会成为未决信号,pending位图中的对应位将会被置为1 。接着进程会去检查block位图中该信号是否被阻塞,若被阻塞,则不会进行递达,直到对该信号的阻塞解除,若没有阻塞,则会直接进行递达。并将pending位图中对应信号的位置0 。

识别一个信号采用三元组方式,是否被block是否被pendinghandler方法是什么,结合这三个信息我们就可以知道这个信号该被怎么处理,这三个信息合起来就叫做进程是可以识别信号的。

下面我们演示信号位图的相关操作

在这里插入图片描述

sigpending ()获取当前调用进程的pending信号集。

在这里插入图片描述

sigismember()用来测试参数signum 代表的信号是否已加入至参数set信号集里。如果信号集里已有该信号则返回1, 否则返回0。

sigaddset()函数是允许您将一个指定的信号添加到一个自定义信号集中,也就是将该信号的标准位设为1,表示阻塞这个信号。当您需要创建或修改信号集,以便在信号处理、信号屏蔽等操作中使用时,可以使用此函数。

sigdelset函数允许您从一个自定义信号集中删除一个指定的信号,也就是将该信号的标准位设为0,不阻塞这个信号。当您需要调整信号集,以便在信号处理、信号屏蔽等操作中使用时,可以使用此函数。

sigfillset()函数初始化一个信号集,使其包含所有可接受的信号。sigempty()则相反。

在这里插入图片描述

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

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
在这里插入图片描述
下面我们来看一段代码:

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;

void handler(int signum)
{
    cout<<"正在处理"<<endl;
}

int main()
{
    signal(2,handler);
    sigset_t set;
    //让二号命令阻塞
     sigset_t bset,obset;
     sigemptyset(&bset);
     sigaddset(&bset,2);
     sigprocmask(SIG_BLOCK,&bset,&obset);//将位图设置到进程中的block中去
    while(true)
    {
     cout<<"我正在运行"<<endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
通过结果我们发现,确实二号信号阻塞了。

那么要是我把所有的信号都进行阻塞了,或者说是将所有信号都进行自定义捕捉,那么是不是我们就写了一个用户杀不掉的进程呢?。我们接下来一 一进行验证。

void ShowPending(sigset_t& set)
{
    for(int i=1;i<=31;i++)
    {
        if(sigismember(&set,i))
        {
            cout<<1;
        }
        else
        {
            cout<<0;
        }
    }
    cout<<endl;
}

void Set_block(int sign)
{
    sigset_t bset,obset;
    sigemptyset(&bset);
    sigaddset(&bset,sign);
    sigprocmask(SIG_BLOCK,&bset,&obset);//将位图设置到进程中的block中去
}
int main()
{
    sigset_t set;
    cout<<"pid"<<getpid()<<endl;
    for(int i=1;i<=31;i++) Set_block(i);
    while(true)
    {
        sigpending(&set);
        ShowPending(set);
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
我们发现到九号信号时,其无法被阻塞,直接就杀掉了该进程。
除了九号信号还有没有其他信号也能够无法被阻塞呢?
我们继续验证!
在这里插入图片描述
19号信号也无法被阻塞,它可以使得进程暂停。

void handler(int signum)
{
    cout<<signum<<"号信号正在处理"<<endl;
}

int main()
{
    cout<<"pid"<<getpid()<<endl;

    for(int i=1;i<=31;i++)
    {
        signal(i,handler);
    }
    while(true)
    {
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
在这里插入图片描述
同样9和19号信号也是无法被捕捉的,所以我们就可以回答前面的疑惑,是不会发生有杀不死的进程这样的情况的。

1. 信号处理的时间

因为信号的产生是异步的,它在任何时候都可能产生信号,在信号产生期间,我的进程可能一直都在运行,当前进程可能会在做着更重要的事情。所以我们会将信号做延时处理,这个取决于OS和进程。

那么这个合适的时间是什么时候?
信号相关的数据段都是在进程的PCB中,属于内核的范畴。当该进程的执行流从内核态返回用户态的时候就会检测是否有信号需要处理并进行递达。
那么什么是内核态,什么是用户态呢?

用户态就是用户代码和数据被执行和访问时的状态,进程执行我们自己所写的代码就是处于用户态。
内核态就是OS执行自己的代码和数据,例如我们的系统调用。

那么他们还有什么区别呢?

内核态的权限是远大于用户态的,若是在用户态中出现野指针,除0这样的错误,OS是可以通过发送信号将该进程杀死的,而OS中若出现这样的问题,则会导致OS奔溃。因此用户态是被OS管制的一种状态。
我们的系统调用,中断,或者异常都会使得进程陷入内核态。我们以系统调用open为例,当我们调用open,进程会进入内核态去运行内核中open的实现代码,然后返回用户态将并带回了返回值。并且从用户态到内核态我们的身份也发生了改变:用户–》OS。

进入内核态后会执行系统自己的代码和数据。那么OS的代码是怎么被执行到的呢?
在这里插入图片描述

如图,是我们的进程地址空间。0-3G是我们的用户空间,3-4G是内核空间,我们之前所学到的在用户态执行时,我们是通过用户及页表去寻找存储在物理内存中的数据和代码,并且进程间是独立的,因此每个进程都有一个用户级的页表。我们的OS只有一个,因此其仅有一个内核级的页表,该页表可以被所有进程看到。在我们的CPU寄存器中,有个CR3寄存器,表示当前CPU执行的权限(即内核态还是用户态)。
当我们要执行系统调用时,进入内核态,通过内核级页表找到对应的系统调用代码,在进程的上下文当中执行。

下图就是我们进行信号处理的流程图。
在这里插入图片描述

进程在用户态运行时,遇到中断或者系统调用,进入内核内核态,处理完异常后在回到用户态之前会进行信号的检测和处理,若我们的信号捕捉动作是默认或者是忽略,则在内核态就可以完成相关的处理动作

如果是默认,比如是终止进程,就直接把进程的相关资源释放掉就可以了,如果是暂停,我把进程状态设置成stop,并把进程的PCB放到等待队列里就可以了;然后再直接返回用户态下一行。
如果是忽略,将pending由1置0,直接返回用户态的下一行代码;

若是自定义动作,则会回到用户态,执行信号的处理函数,处理完后会再次返回内核态(因为信号处理函数在用户态,其无法直到上次中断的地方在哪里)返回内核态后,再返回到用户态中上次中断的位置。

那么为什么内核不直接执行信号处理函数呢?是不能执行,还是不想要执行呢
答案是不想要执行,OS的权限是很高的,其当然可以执行用户态的代码,但OS不相信任何人,它担心用户态中的代码有非法操作,因此它并不想执行用户级的代码。

下面我们用一幅对上述表的抽象图来进行总结:
在这里插入图片描述
一个橙色的圈代表一次状态切换。

sigaction函数

其作用与signal相同,只是用法不同。
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

struct sigaction是一个OS的结构体。
在这里插入图片描述

这个函数要比signl复杂,因为它考虑了实时信号,act是一个输入性参数就是说你想对这个信号执行什么动作,你可以把你的动作方法填入到这个结构体里,当信号就绪时执行;oact是一个输出型参数,你设置这个信号的老的方法是什么,它会带回老的信号的方法,不想要设为NULL。

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。
signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体: 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函
数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。

void handler(int signum)
{
    cout<<signum<<"号信号正在处理"<<endl;
    //sleep(3);
}

int main()
{
struct sigaction sigc,osigc;
sigc.sa_handler=handler;
sigaction(2,&sigc,&osigc);
while(true)
{
    cout<<"我正在运行"<<endl;
    sleep(1);
}


    return 0;
}

在这里插入图片描述
我们通过结果发现其确实可以对信号的捕捉动作进行自定义。

当某个信号的处理函数被调用时 , 内核自动将当前信号加入进程的信号屏蔽字 , 当信号处理函数返回时自动恢复原来的信号屏蔽字, 这样就保证了在处理某个信号时 , 如果这种信号再次产生 , 那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时, 除了当前信号被自动屏蔽之外 , 还希望自动屏蔽另外一些信号 , 则用 sa_mask 字段说明这些需要额外屏蔽的信号, 当信号处理函数返回时自动恢复原来的信号屏蔽字。

sa_mask 本质是位图。

这也是为什么要有block的本质

如果我把2号信号屏蔽了,我给你的进程发送100个2号信号,此时你的进程只能记住一个,Linux对普通信号是可能丢失的(2个以上),因为记录信号的标志位只有一个比特位,如果把2号信号屏蔽了,发送100个,OS最终只记住一个(最新的那一个,发一次写一个)。
不可能丢失的信号叫做实时信号,在内核中是以链表,队列的形式来把所有的实时信号链到PCB里面的,来一个链接一个。本质是底层数据结构的区别。

可重入函数
在这里插入图片描述

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从 sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只 有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

我们学到的大部分函数,STL,boost库中的函数,大部分都是不可重入的。

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

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

volatile关键字
我们来看下面这段代码:

int flag=0;
void handler(int signum)
{
    cout<<signum<<"号信号正在处理"<<endl;
    flag=1;
    cout<<"flag"<<"--->"<<0<<"--->"<<flag<<endl;
}
int main()
{
struct sigaction sigc,osigc;
sigc.sa_handler=handler;
sigaction(2,&sigc,&osigc);
while(!flag);

cout<<"退出"<<endl;
    return 0;
}

在这里插入图片描述

刚刚我们的编译器是常规情况,看到的就是这种现象,但是我们的编译器是有各种优化的。gcc/g++默认是普通编译,但也可以让用户自己设置优化级别,存在O0-O4的优化级别的。

下面我们进行优化后再次进行测试:

在这里插入图片描述

在这里插入图片描述
此时我们发现,flag变为了1却退出不了了。这是为什么呢?
while 循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。 while 检测的flag其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要 volatile 。

加volatile后

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

SIGCHILD信号

多进程中我们可以用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。

其实 , 子进程在终止时会给父进程发 SIGCHLD 信号 , 该信号的默认处理动作是忽略 , 父进程可以自 定义 SIGCHLD 信号的处理函数, 这样父进程只需专心处理自己的工作 , 不必关心子进程了 , 子进程 终止时会通知父进程 , 父进程在信号处理函数中调用wait 清理子进程即可。换言之如果我是可以直接在handler方法里调用waitpid,回收子进程的。此时父进程就不用主动等待子进程退出。

但是父进程就不想回收,压根就不关心这个子进程的退出码等信息,并且子进程退出的时候不形成僵尸进程,不要影响父进程。我们就可以显示设置忽略17号信号。此方法对于linux有用。

系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。

SIGCHILD信号

来看一段代码:

void handler(int signum)
{
        while(waitpid(-1,nullptr,WNOHANG)>0)
        {
            cout<<"进程退出"<<endl;
        }
       

    cout<<"all quit"<<endl;
}

int main()
{

signal(17,handler);
int count=3;
while(count--)
{
    pid_t pid=fork();
    if(pid==0)
    {
        cout<<"pid"<<getpid()<<endl;
       sleep(2);
        exit(1);
    }

}

while(true)
{
    cout<<"father is doing"<<endl;
    sleep(1);
}


}

在这里插入图片描述
我用的是while循环和WNOHANG(非阻塞等待),用循环的原因是为了满足各种子进程退出的情况,eg:我创建了10个子进程,10个子进程同时退出了,每个子进程都同时向父进程发送信号,可是pending位图只有一个比特位记录信号,如果只wait一次就只能wait一个子进程,剩下9个就wait不到。通过while循环我们就可以把所有的子进程都读到。

用非阻塞的原因:假如你是阻塞等待,有10个子进程,5个退出了,5个没退出。你循环读,也没有任何问题,但是当你读第6次的时候,子进程没退出,你就在信号捕捉函数这里卡住了。(子进程不退出父进程不返回,这就叫做阻塞等待)。当你读取一个子进程,只有当你读取失败的时候,你才知道底层没有子进程退出了。所以这里要用非阻塞。

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

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

相关文章

你写的Python代码到底多快?这些测试工具了解了解

当我们写完一个脚本或一个函数&#xff0c;首先能保证得到正确结果&#xff0c;其次尽可能的快&#xff08;虽然会说Py慢&#xff0c;但有的项目就是得要基于Py开发&#xff09; 本期将总结几种获取程序运行时间的方法&#xff0c;极大的帮助对比不同算法/写法效率 插播&…

【Python工具】简介cmd安装pip及常见错误

简介cmd安装pip以及第三方库 1 检查电脑是否安装pip常用pip命令1.1 未设置环境配置1.2 未安装pip 2 常见错误2.1 Requirement already satisfied 参考 pip是Python中最常用的包管理工具&#xff0c;也是最常用的在线安装方法。 命令如下&#xff1a;package_name就是你所需要安…

软件测试报告所需周期和费用简析

软件测试报告是在软件开发和测试过程中生成的重要文档之一。它提供了对软件系统经过全面测试后的状态和质量的详细描述&#xff0c;以记录软件测试的过程和结果。 生成一个完整的测试报告需要根据软件项目的规模和复杂性来确定时间。较大规模和复杂的软件项目可能需要更长的时…

default约束

一、default约束 1、语句释义 add constraint &#xff08;添加约束&#xff09; 约束的名字 default &#xff1a;表示添加的是default约束&#xff0c;如果是外键约束就是“foreign key” 99999 for Phone&#xff1a;Phone列&#xff0c;默认值为99999 2、defualt约束效…

实时数仓-hologres使用总结

我们回顾下&#xff0c;Hologres是一款实时HSAP产品&#xff0c;隶属阿里自研大数据品牌MaxCompute&#xff0c;兼容 PostgreSQL 生态、支持MaxCompute数据直接查询&#xff0c;支持实时写入实时查询&#xff0c;实时离线联邦分析&#xff0c;低成本、高时效、快速构筑企业实时…

Wonder3D安装完美教程

话不多说,先附上地址: https://github.com/xxlong0/Wonder3D#wonder3dhttps://github.com/xxlong0/Wonder3D#wonder3d 目录 一、预览 二、环境配置

基于SSM的新枫之谷游戏攻略设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

浪潮信息“拓荒”:一场面向大模型时代的性能“压榨”

文 | 智能相对论 作者 | 沈浪 全球人工智能产业正被限制在了名为“算力”的瓶颈中&#xff0c;一侧是供不应求的高端芯片&#xff0c;另一侧则是激战正酣的“百模大战”&#xff0c;市场的供求两端已然失衡。 然而&#xff0c;大多数人的关注点仍旧还是在以英伟达为主导的高…

喜欢 Android 14 的 14 个理由

和去年 8 月中旬发布的 Android 13 正式版不同&#xff0c;今年的 Android 14 正式版延后到了 10 月 4 日——也就是 Pixel 8 系列发布的同一天。原因我们似乎也能从 Google 宣传新特性中略窥一二&#xff1a; 除了明确表示会率先向特定 Pixel 机型推送的 AI 壁纸生成&#xf…

网络层 IP协议

网络层&#xff1a;为分组交换网上的不同主机提供分组交换服务。 IP协议 协议格式 4位版本&#xff1a;ipv4就是4. 4位首部长度&#xff1a;20字节固定40字节选项。 8位服务类型&#xff1a;TOC&#xff0c;高三位表示优先级&#xff0c;已弃用&#xff0c;其次从高到底依次为…

内网穿透 cpolar

内网穿透可以使本地启动的服务让他人访问&#xff0c;不受局域网的限制。常见的是使用第三方服务&#xff0c;厉害的自己搭建。对于我这种水平来说&#xff0c;肯定是使用第三方服务。常见的 frp、ngrok、PortForward、cpolar 花生壳等等。 为什么需要内网穿透&#xff0c;因为…

性能测试常用术语

之前在性能测试过程中&#xff0c;对于某些其中的术语一知半解&#xff0c;导致踩了很多坑。这篇博客&#xff0c;就常见的一些性能测试术语进行一次浅析。。。 负载 对被测系统不断施加压力&#xff0c;直到性能指标超过预期或某项资源使用达到饱和&#xff0c;以验证系统的处…

windows 运行 Mysql Command Line Client 自动关闭闪退原因分析

目录 原因分析一 原因分析二 原因分析三 第一次使用 MySQL Command Line Client 有可能输入密码后一按下回车键&#xff0c;程序窗口就自动关闭&#xff0c;出现闪退现象。本节主要分析产生闪退现象的原因以及如何处理这种情况。 原因分析一 首先可以查看程序默认执行文件…

java--构造器

1.构造器是什么样子 构造器分为无参构造(就相当于你有车子&#xff0c;但是里面是空的)和带参构造(就相当于你有车子&#xff0c;里面还有几个妹纸&#xff0c;你真该死啊) 2.构造器有什么特点 创建对象时&#xff0c;对象会去调用构造器。 3.构造器的常见应用场景 创建对象…

物联网中的ESP8266该这么用!

&#x1f64c;秋名山码民的主页 &#x1f602;oi退役选手&#xff0c;Java、大数据、单片机、IoT均有所涉猎&#xff0c;热爱技术&#xff0c;技术无罪 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; 获取源码&#xff0c;添加WX 目录 1. 前言…

win10 esd文件转iso

想装个win10虚拟机&#xff0c;在系统之家下载了iso&#xff0c;然后开始装&#xff0c;发现虚拟机找不到系统&#xff0c; 插&#xff0c;啥原因&#xff0c;解压出iso一看&#xff0c;有个exe文件&#xff0c;要运行他才安装&#xff0c;不地道啊&#xff0c;现在为什么搞这…

Viessmann Vitogate远程代码执行漏洞(CVE-2023-45852)

Viessmann Vitogate远程代码执行漏洞&#xff08;CVE-2023-45852&#xff09; 免责声明漏洞描述漏洞影响漏洞危害网络测绘Fofa: body"vitogate 300" 漏洞复现1. 构造poc2. 执行命令查看用户 免责声明 仅用于技术交流,目的是向相关安全人员展示漏洞利用方式,以便更好…

SpringCloudGateway--过滤器(自定义filter)

目录 一、概览 二、通过GatewayFilter实现 三、继承AbstractGatewayFilterFactory 一、概览 当使用Spring Cloud Gateway构建API网关时&#xff0c;可以利用Spring Cloud Gateway提供的内置过滤器&#xff08;filter&#xff09;来实现对请求的处理和响应的处理。过滤器可以…

无需专线、无需固定公网IP,各地安防数据如何高效上云?

某专注于安防领域的企业&#xff0c;供机场、金融、智慧大厦等行业&#xff0c;包括门禁系统、巡更系统、视频监控在内的整体解决方案。 在实际方案交付过程中&#xff0c;往往需要在多地分支机构分别部署相应的安防设备&#xff0c;并将产生的数据实时统一汇总至云平台进行管理…