Linux进程信号(3)--信号的处理

目录

前置知识

捕捉信号

内核如何实现信号的捕捉

sigaction

信号的其他补充问题

可重入函数

volatile关键字

SIGCHILD信号


前置知识

什么是用户态,内核态呢?

这里我们再来看看进程的地址空间:

我们知道每一个进程都会有自己的地址空间:把0-3GB的空间叫做用户空间,3-4GB的叫做内核空间。用户空间是用来记录,我们所编写的代码,数据及定义的变量等,那么内核空间使用来干嘛的呢?

我们知道操作系统也是一个进程,也要加载到内存,并且是计算机启动后第一个执行的进程,因此操作系统的内容也要被记录到地址空间里,而内核空间就是用来记录操作系统的。与普通进程一样操作系统也有一张页面去映射物理内存上的内容。

为了区分:把操作系统的页表叫:内核级页表。普通进程的页表叫做:用户级页表。

因为在开启计算机时操作系统只执行一次,可以管理各个进程的,因此每一个进程都共享一个操作系统,一个内核级页表。

由于操作系统并不信任用户,用户不能直接访问硬件等重要资源,需要调用系统调用函数,那么是如何调用系统调用函数呢?

这里我们先看普通函数的调用,这里我们看汇编代码,一般是用call+函数的地址,在进程地址空间中找到函数,然后通过页表的映射找到物理内存中执行内容。而系统调用函数的调用也是类似的。

用call+函数地址,由于系统调用属于操作系统函数的地址空间在内核空间里。因此需要在进程用户空间,跳转到对应的内核空间,然后在内核页表的映射中找到物理内存存储的内容。在执行完系统调用时,就重新会到地址空间完成后续代码。

注:在进程空间跳转到内核空间需要完成从用户态转变成内核态。从内核空间到用户空间也要进行状态转换。

我们知道在执行代码时,CPU会进行参与运算等,而在CPU里有一个CS寄存器,里面用两个比特位来表示表示所处的状态,1为内核态,3为用户态。所以状态的转换就是修改了CS寄存器里的内容。

注:这里修改CS里的内容是在系统调用最开始的入口处完成的。 

因此我们可以知道:在用户空间执行就是用户态,在内核空间执行就是内核态。

捕捉信号

在前面提到,如果进程在接受信号时,正在执行更加重要的任务,进程并不会立即处理而是在合适的时候。那么这个合适的时候是什么呢?

这里我们知道信号相关的数据字段是在进程的PCB中存储着的,PCB内部属于内核范畴,普通用户无法对信号直接进行处理。要想对信号处理,就必须是内核态。当调用系统调用或被系统调用时,进程所处的状态就是内核态,不执行操作系统代码时,进程所处的状态就是用户态。

结论:在内核中,从内核态返回用户态的时候,进行信号的检测与处理。

内核如何实现信号的捕捉

在内核态处理信号时,就会到进程的PCB里查看block,pending,handler表中的内容,这里我们知道在handler表中的动作可以分为三种:默认动作,忽略动作,用户自定义动作。我们知道默认动作,忽略动作在操作系统早已写好(在内核里),而第三种是由我们自己编写的(在用户空间里)。这里我们在处理信号时,是内核态,那么在我们执行我们自定义的函数时,是以内核态还是用户态执行的呢?

当然是用户态。如果我们用内核态去执行,一旦函数里面有恶意访问或修改内核的代码,就会直接绕过操作系统,进行破坏。所以我们在执行用户自定义信号动作时,要把内核态变为用户态。

在执行完函数时,我们还要返回到内核态里!为什么呢?

这里就不得不提到,我们学习C语言时,函数栈帧的创建于销毁这个知识点了:

在函数调用时,我们会在栈上面为函数开辟一段空间,用来记录函数的各种数据数据(如函数在结束后如何回到调用前的位置继续执行后面的代码)。当函数结束时,该函数的栈帧会被销毁,然后回到调用该函数的函数的栈帧上,然后按照这个依次执行,只到main函数结束。

这里我们在main函数上为了处理信号调用了系统调用,建立一块栈帧,在执行时,又调用了自定义函数,建立栈帧。当自定义函数结束时,我们理应继续执行系统调用函数,所以我们还要从用户态转换成内核态。其次我们知道在信号处理完后(如果信号动作不会终止程序),我们要还返回到main函数里继续执行下面的代码,但是这里如何返回main函数的方法是记录在系统调用上的,这里当自定义函数执行完后,我们只知道如何返回系统调用函数。

这里自定函数结束后,回到内核态是靠sigreturn这个系统调用再次进入内核态的。当进程所有接受的信号被处理完后(如果进程还没有结束),如果还没有新的信号被进程接受到,这时会再次从内核态返回用户态,恢复到main函数之前的地方继续执行下面的代码。


sigaction

sigaction 函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回 0, 出错则返回 - 1 signo 是指定信号的编号。若act 指针非空 , 则根据 act 修改该信号的处理动作。若 oact 指针非 空 , 则通过 oact 传出该信号原来的处理动作。act oact 指向 sigaction 结构体 :

  • sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动 作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回 值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信 号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
  • sa_sigactios是实时信号的处理方法,不许要关心。
#include<iostream>
#include<unistd.h>
#include<signal.h>

using namespace std;

void handler(int signum)
{
    cout<<"handler: "<<signum<<endl;
}
int main()
{
    struct sigaction act,oact;
    act.sa_handler=handler;
    
    sigaction(2,&act,&oact);//改变2号信号的处理动作
    
    while(true)
    {
        sleep(1);
    }
    return 0;
}

在处理信号,执行自定义动作时,如果在处理信号期间,又来了同样的信号,操作系统该如何处理呢?

Linux的设计方案是:在任何时候,操作系统只能处理一层信号,不允许出现信号正在处理又来一个相同的信号再被处理的情况。虽然操作系统无法决定信号什么时候发送信号,倒是可以决定什么时候处理信号。这里就让我们来看为什么要有信号屏蔽字block!

当某个信号的处理函数被调用时 , 内核自动将当前信号加入进程的信号屏蔽字 , 当信号处理函数返回时自动恢复原来 的信号屏蔽字, 这样就保证了在处理某个信号时 , 如果这种信号再次产生 , 那么 它会被阻塞到当前处理结束为止。 如果 在调用信号处理函数时, 除了当前信号被自动屏蔽之外 , 还希望自动屏蔽另外一些信号 , 则用 sa_mask 字段说明这些需 要额外屏蔽的信号, 当信号处理函数返回时自动恢复原来的信号屏蔽字 sa_flags 字段包含一些选项 , 这里代码都把sa_flags 设为 0,sa_sigaction 是实时信号的处理函数 ,就· 不详细解释这两个字段 , 有兴趣的朋友可以再了解一下。
#include<iostream>
#include<unistd.h>
#include<signal.h>

using namespace std;

void PrintPending(sigset_t* pending)//打印pending表中的内容
{
    for(int i=31;i>=1;i--)
    {
        if(sigismember(pending,i))
        cout<<'1';
        else
        cout<<'0';
    }
    cout<<endl;
}

void handler(int signum)
{
    int cnt=6;
    sigset_t s;
    cout<<"handler: "<<signum<<endl;
    while(cnt)
    {
        cnt--;
        sigpending(&s);
        PrintPending(&s);//实时打印pending表内容
        sleep(1);
    }
}
int main()
{
    cout<<"getpid: "<<getpid()<<endl;
    struct sigaction act,oact;
    act.sa_handler=handler;
    
    sigaction(2,&act,&oact);//改变2号信号的处理动作
    
    while(true)
    {
        sleep(1);
        cout<<"running.."<<endl;
    }
    return 0;
}


处理2号信号的同时,屏蔽3~7号信号:

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

using namespace std;

void PrintPending(sigset_t* pending)
{
    for(int i=31;i>=1;i--)
    {
        if(sigismember(pending,i))
        cout<<'1';
        else
        cout<<'0';
    }
    cout<<endl;
}

void handler(int signum)
{
    int cnt=20;
    sigset_t s;
    cout<<"handler: "<<signum<<endl;
    while(cnt)
    {
        cnt--;
        sigpending(&s);
        PrintPending(&s);
        sleep(1);
    }
}
void handler2(int signum)
{
    cout<<"handler: "<<signum<<endl;
}

int main()
{
    cout<<"getpid: "<<getpid()<<endl;
    struct sigaction act,oact;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask,3);
    sigaddset(&act.sa_mask,4);
    sigaddset(&act.sa_mask,5);
    sigaddset(&act.sa_mask,6);
    sigaddset(&act.sa_mask,7);
    act.sa_handler=handler;
    
    sigaction(2,&act,&oact);//改变2号信号的处理动作
    signal(3,handler2);
    signal(4,handler2);
    signal(5,handler2);
    signal(6,handler2);
    signal(7,handler2);
    while(true)
    {
        sleep(1);
        cout<<"running.."<<endl;
    }
    return 0;
}

注:这里我们我们可以看到当2号执行完后,就会检查pending表中还有没有信号,若有便会将信号都处理完,再回到main函数里执行剩下的代码。

但是这里我们可以看到,再执行其他信号时,并没有按照他们的接受顺序去执行。

这是因为,在信号之间也是有优先级的,默认先执行有限级高的,再执行优先级低的。


信号的其他补充问题

可重入函数

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

volatile关键字

该关键字在 C 当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下
#include<iostream>
#include<unistd.h>
#include<signal.h>

using namespace std;

int flag=0;

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

int main()
{
    signal(2,changeFlag);
    while(!flag);
    cout<<"flag quit normal."<<endl;
    return 0;
}

编译器有时会自动的给我们进行代码优化!

这里即使对flag进行修改,也没有办法结束进程。这是为什么呢?

正常情况下,每次循环通过flag进行检测时,都需要到内存里取数据,但是由于编译器的优化,导致编译器认为main函数里的代码没有对flag进行修改,所以为了提高效率,第一次从内中取出来flag的数据后就不会到内存中取数据了,而是直接读取CPU寄存器里的数据进行循环检测。

编译器的优化使CPU无法看到内存,而关键字volatile就是为了保持内存的可见性,每次都读取内存中的数据。

SIGCHILD信号

  • 进程一章讲过用waitwaitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进 程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父 进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
  • 其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号 的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理 函数中调用wait清理子进程即可。
  • 事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigactionSIGCHLD的处理动作 置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽 略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证 在其它UNIX系统上都可 用。
#include<iostream>
#include<unistd.h>
#include<signal.h>

using namespace std;

void handler3(int signum)
{
    cout<<"子进程退出: "<<signum<<endl;
}

int main()
{
    signal(SIGCHLD,handler3);
    pid_t id=fork();
    if(id==0)
    {
        cout<<"child pid:"<<getpid()<<endl;
        sleep(1);
        exit(0);
    }

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

自动等待进程

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<sys/types.h>

void handler3(int sig)
{
    pid_t id;
    //-1表示等待任意一个子进程
    while(id=waitpid(-1,nullptr,WNOHANG));
    {
        printf("wait child success: %d\n",id);
    }
    printf("child is quit! %d",getpid());
}

int main()
{
    signal(SIGCHLD,handler3);
    if(fork()==0)
    {
        printf("child: %d\n",getpid());
        sleep(3);
        exit(1);
    }
    while(1)
    {
        printf("father proc is doing something!\n");
        sleep(1);
    }
    return 0;
}


#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<sys/types.h>

using namespace std;

//不等待子进程,并且还可以让子进程在退出之后,自动释放僵尸进程
int main()
{
    signal(SIGCHLD,SIG_IGN);//手动设置对子进程进行忽略

    if(fork()==0)
    {
        cout<<"child: "<<getpid()<<endl;
        sleep(5);
        exit(0);
    }

    while(1)
    {
        cout<<"parent: "<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

这里手动释放设置忽略,操作系统会自动释放处于僵尸状态的子进程,所以并没有看见处于僵尸状态的子进程子。

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

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

相关文章

大数据Zookeeper--案例

文章目录 服务器动态上下线监听案例需求需求分析具体实现测试 Zookeeper分布式锁案例原生Zookeeper实现分布式锁Curator框架实现分布式锁 Zookeeper面试重点选举机制生产集群安装多少zk合适zk常用命令 服务器动态上下线监听案例 需求 某分布式系统中&#xff0c;主节点可以有…

unity 增加系统时间显示、FPS帧率、ms延迟

代码 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;using UnityEngine;public class Frame : MonoBehaviour {// 记录帧数private int _frame;// 上一次计算帧率的时间private float _lastTime;// 平…

Docker 一小时从入门到实战 —— Docker commands | Create your own image | vs VM ... 基本概念扫盲

Docker crash course 文章目录 Docker crash course1. What and Why of Docker?2.1 What2.2 What problem does it solve?2.2.1 before containers2.1.2 with containers 2. Docker vs Virtual Machines2.1 Difference2.2 Benefits 3. Install docker locally4. Images vs Co…

微信小程序(三十四)搜索框-带历史记录

注释很详细&#xff0c;直接上代码 新增内容&#xff1a; 1.搜索框基本模板 2.历史记录基本模板 3.细节处理 源码&#xff1a; index.wxml <!-- 1.点击搜索按钮a.非空判断b.历史记录&#xff08;去重&#xff09;c.清空搜索框d.去除前后多余空格2.删除搜索 3.无搜索记录不…

springboot158基于springboot的医院资源管理系统

简介 【毕设源码推荐 javaweb 项目】基于springbootvue 的 适用于计算机类毕业设计&#xff0c;课程设计参考与学习用途。仅供学习参考&#xff0c; 不得用于商业或者非法用途&#xff0c;否则&#xff0c;一切后果请用户自负。 看运行截图看 第五章 第四章 获取资料方式 **项…

HTTP相关问题

目录 1.从输入URL到页面展示到底发生了什么&#xff1f; 2.HTTP状态码有哪些&#xff1f; 2.1 2XX(成功状态码) 2.2 3XX(重定向状态码) 2.3 4XX(客户端错误状态码) 2.4 5XX(服务端错误状态码) 3.HTTP 请求头中常见的字段有哪些&#xff1f; 4.HTTP和HTTPS有什么区别&…

[C++] 如何使用Visual Studio 2022 + QT6创建桌面应用

安装Visual Studio 2022和C环境 [Visual Studio] 基础教程 - Window10下如何安装VS 2022社区版_visual studio 2022 社区版-CSDN博客 安装QT6开源版 下载开源版本QT Try Qt | 开发应用程序和嵌入式系统 | Qt Open Source Development | Open Source License | Qt 下载完成&…

AI人工智能怎么自动抠图去除背景

抠图在许多场合中都发挥着重要的作用&#xff0c;它可以帮助我们去除图片的背景&#xff0c;从而将图片转换成透明底色&#xff0c;方便我们为图片更换其他的背景。为了实现照片去除背景只提取人物&#xff0c;我们需要使用专业的图片处理工具&#xff0c;并进行一系列的操作。…

算法效率的度量-时间空间复杂度

常对幂指阶 1.时间复杂度 事前预估 算法 时间开销 T(n) 与 问题规模 n 的关系&#xff08; T 表示 “ time ”&#xff09; 一般默认问题规模为n。 1.单循环 2.嵌套两层循环都为n 3.单层循环指数递增型 4.搜索型 链接 &#xff1a;第七章查找算法&#xff01;&#xff01…

elementUI 表格中如何合并动态数据的单元格

elementUI 表格中如何合并动态数据的单元格 ui中提供的案例是固定写法无法满足 实际开发需求 下面进行改造如下 准备数据如下 //在表格中 设置单元格的方法 :span-method"spanMethodFun" <el-table :data"tableData" border :span-method"spa…

2024第八届生物饲料高质量发展论坛会议通知

饲料工业发展空间大&#xff0c;产量持续增长&#xff0c;品质与质量也在不断提高&#xff0c;饲料工业是支撑现代畜牧水产养殖业发展的基础产业&#xff0c;是关系到城乡居民动物性食品供应的民生产业。“十四五”时期是我国由全面建设小康社会向基本实现社会主义现代化迈进的…

LeetCode、790. 多米诺和托米诺平铺【中等,二维DP,可转一维】

文章目录 前言LeetCode、790. 多米诺和托米诺平铺【中等&#xff0c;二维DP&#xff0c;可转一维】题目与分类思路二维解法二维转一维 资料获取 前言 博主介绍&#xff1a;✌目前全网粉丝2W&#xff0c;csdn博客专家、Java领域优质创作者&#xff0c;博客之星、阿里云平台优质…

性能测试工具LoadRunner与登录性能测试分析

1. LoadRunner与Jmeter Jmeter是开源免费的&#xff0c;LoadRunner是商业收费的。 但是LoadRunner具有非常强大的录制功能&#xff0c;具有丰富且灵活的场景&#xff0c;具备丰富的报告性能。 1&#xff09;Jmeter没有录制功能 2&#xff09;LoadRunner可以设计非常丰富的测试…

构造回文数组

目录 原题描述&#xff1a; 题目描述 时间&#xff1a;1s 空间&#xff1a;256M 题目描述&#xff1a; 输入格式&#xff1a; 输出格式&#xff1a; 样例1输入&#xff1a; 样例1输出&#xff1a; 样例2输入&#xff1a; 样例2输出&#xff1a; 约定&#xff1a; 作…

JVM 性能调优 - JVM 参数基础(2)

查看 JDK 版本 $ java -version java version "1.8.0_151" Java(TM) SE Runtime Environment (build 1.8.0_151-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode) 查看 Java 帮助文档 $ java -help 用法: java [-options] class [args...] …

CSS:三列布局

三列布局是指左右两列定宽&#xff0c;中间自适应。最终效果如下&#xff1a; HTML&#xff1a; <div class"container"><div class"left"></div><div class"center"></div><div class"right">…

javaEE - 23( 21000 字 Servlet 入门 -1 )

一&#xff1a;Servlet 1.1 Servlet 是什么 Servlet 是一种实现动态页面的技术. 是一组 Tomcat 提供给程序猿的 API, 帮助程序猿简单高效的开发一个 web app. 构建动态页面的技术有很多, 每种语言都有一些相关的库/框架来做这件事&#xff0c;Servlet 就是 Tomcat 这个 HTTP…

404. Sum of Left Leaves(左叶子之和)

问题描述 给定二叉树的根节点 root &#xff0c;返回所有左叶子之和。 问题分析 我们可以查看如果一个叶子是左叶子就加上其值然后返回&#xff0c;如果是右叶子则不用关。 代码 int sumOfLeftLeaves(struct TreeNode* root) {int sum 0;if(root!NULL){if(root->left!…

板块零 IDEA编译器基础:第二节 创建JAVA WEB项目与IDEA基本设置 来自【汤米尼克的JAVAEE全套教程专栏】

板块零 IDEA编译器基础&#xff1a;第二节 创建JAVA WEB项目与IDEA基本设置 一、创建JAVA WEB项目&#xff08;1&#xff09;普通项目升级成WEB项目&#xff08;2&#xff09;创建JAVA包 二、IDEA 开荒基本设置&#xff08;1&#xff09;设置字体字号自动缩放 &#xff08;2&am…

安全SCDN有什么作用

当前网络安全形势日益严峻&#xff0c;网络攻击事件频发&#xff0c;攻击手段不断升级&#xff0c;给企业和个人带来了严重的安全威胁。在这种背景下&#xff0c;安全SCDN作为一种网络安全解决方案&#xff0c;受到了广泛的关注。那么&#xff0c;安全SCDN真的可以应对网络攻击…