linux信号相关概念

signal

  • 信号引入
  • 什么是信号?
  • 如何产生信号?
    • 通过按键产生信号
    • 调用系统函数向进程发信号
      • 系统调用函数发送信号的流程:
    • 由软件条件产生信号
      • 软件发送信号的流程:
    • 硬件异常产生信号
      • 硬件异常的流程:
  • Deliver、Pending、Block概念
    • 信号在内核表示示意图
    • sigset_t
    • 信号集操作函数
    • 注意
  • 信号捕捉
    • 捕捉信号的时机:
  • 可重入函数
  • volatile

信号引入

我们在linux编写代码时,如果想提前结束一个进程,通常我们会按ctrl+c组合键:
在这里插入图片描述
其实这就是想OS传递了一个中断进程的信号,我们平常就有意无意的在使用它!

  • 如何理解组合键变成信号呢?‘
    OS解释组合键->查找进程列表->前台运行的进程->OS写入对应的信号到进程内部的位图结构中(OS直接修改进程PCB的位图结构)。

什么是信号?

  • 信号是进程之间事件异步通知的一种方式,属于软中断。(进程无论怎么运行,我们都能使用信号来通知他们执行动作)。

  • 使用kill -l 命令,可查看linux下的信号。(1~31是常用的信号,34~64是实时信号)
    在这里插入图片描述

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,如下图: 在这里插入图片描述

  • 对于信号,一般有三种处理函数:忽略此信号、执行该信号的默认处理动作、修改信号的默认处理动作(本来在OS内核执行的默认动作,被切换到用户态执行用户自己的函数,这种方式被称为Catch一个信号)。

如何产生信号?

我们先认识一个,可以修改信号处理动作的函数:signal()
在这里插入图片描述

第一个参数是信号,可填宏定义可填数字。
第二个参数是回调函数,用于定义用户想要执行的动作。

#include<iostream>
#include <unistd.h>
#include<signal.h>
using namespace std;void catchSignal(int signum)
{cout<<"我收到了一个信号,正在处理:"<<signal<<" Pid:"<<getpid()<<endl;
}int main()
{int i = 0;signal(SIGINT, catchSignal);while(1){sleep(1);cout<<"这是一个死循环"<<++i<<endl;}return 0;
}

该例子我们修改了SIGINT的默认处理动作,所以当我们按Crtl+C的时候,进程并没有中止,而是执行了自己定义的函数。
在这里插入图片描述

通过按键产生信号

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump。

  • 什么是Core Dump?
    当一个进程要异常终止时,可以选择把进程的用户控件内存数据全部保存到磁盘上,文件名通常是core,这就叫Core Dump。然后事后可以检查core文件,查看错误原因,这叫Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中)。默认不允许产生core文件,因为core可能包含密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制。

ulimit -c 1024
修改shell进程的Resource Limit,允许core文件最大为1024K
ulimit -a
查看相关信息

在这里插入图片描述
在这里插入图片描述
只要动作是action的都会产生core文件:
在这里插入图片描述

下面演示如何产生core 文件:设置了一段除0的代码

int main()
{int i = 10;while(1){sleep(1);i/=0;}return 0;
}

在这里插入图片描述

如何使用core文件呢?使用gdb调试命令:
这样就可以直接根据core文件,定位出错误的地方了。

在这里插入图片描述

调用系统函数向进程发信号

我们平常使用的kill命令,其实就是调用的系统kill函数:
在这里插入图片描述
这个函数的功能就是给指定的进程发送信号:
在这里插入图片描述


raise函数:
自己给自己发信号,成功返回0,错误返回-1;

int main()
{int i = 10;while(1){sleep(1);raise(SIGSEGV);  //段错误信号}return 0;
}

在这里插入图片描述


abort函数:

void abort(void)
使当前进程接收到信号而终止;

int main()
{int i = 10;while(1){sleep(1);abort();   //什么也不填,相当与exit}return 0;
}

在这里插入图片描述

系统调用函数发送信号的流程:

用户调用系统调用接口->执行OS对用的系统调用代码->OS提取参数,或者设置特定数值->OS向目标进程写信号->修改对应进程的信号标记位(PCB里)->进程后续执行对应的处理动作

由软件条件产生信号

在这里插入图片描述
该函数可以理解为设置一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程(也可以通过修改默认处理动作,去完成用户的需求)

软件发送信号的流程:

OS先识别到某种软件条件触发或者不满足->OS构建信号,发送给指定的进程。

硬件异常产生信号

CPU除0错误,访问非法内存地址,都是硬件异常。

硬件异常的流程:

除0错误:
1.CPU在计算时,发现状态寄存器的溢出标记位是1->OS系统识别出有溢出问题,立即找到谁在运行这个程序->OS给这个进程发送信号,进程会在合适的时候,进行处理。
2. 出现硬件异常,进程一定会退出吗?不一定!默认是退出,但是我们即使不退出,也做不了什么。
3. 为什么会死循环?如果你把除0的默认动作改了之后,溢出标志位就一直是1(没有人改它),所以会一直执行你改正的动作。

指针越界问题:
4. 指针必须通过地址找到目标位置
5. 而语言层面的地址,是虚拟地址
6. 将虚拟地址转化成物理地址需要(页表+MMU内存管理单元)
7. 如果是野指针,越界->非法地址->MMU转化的时候,OS一定会报错!

Deliver、Pending、Block概念

  • 信号递达(Deliver):执行信号的处理动作
  • 信号未决(Pending):还没有响应的信号
  • 阻塞(Block):阻塞某个信号
  • 阻塞和忽略是不同的,阻塞是未处理,忽略是处理动作是忽略。
    被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

信号在内核表示示意图

在这里插入图片描述

信号产生会修改PCB(pcb有指向信号数据结构的指针),Block、pending都是位图结构,handler是信号相应的动作

  • block表示的位图为是否阻塞该信号、pending表示接收到该信号。
  • 信号处理流程:OS->pending->block(如果被阻塞了就不处理呢)否则就进入handler处理。

sigset_t

该类型是系统的位图变量(用来描述上面的位图结构),并且OS提供了对它的操作函数。

信号集操作函数

#include<signal.h>
//set是信号集[]
int sigemptyset(sigset_t *set);   //把set都置为0
int sigfillset(sigset_t *set);     //把set都置为1
int sigaddset(sigset_t *set, int signo);      //把signo 数字的信号 置为1
int sigdelset(sigset_t * set, int signo);  //删除signo ,位图置为0
int sigismember(sigset_t *set, int signo);  //该信号集有效信号是否有signo,有就返回1,没有返回0,出错返回-1;

前四个函数成功返回0,出错返回-1;


功能:读取或更改进程的信号屏蔽字(阻塞信号集)
int sigprocmask (int how, const sigset_t *set, sigset_t *oset);
返回值:成功返回0,出错返回-1.

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

  • SIG_BLOCK:set包含了希望添加到当前信号屏蔽字的信号,相当于mask = mask|set
  • SIG_UNBLOCK:set包含希望解除阻塞的信号,相当于mask = mask&~set
  • SIG_SETMASK:设置当前信号屏蔽字为set指向的值, 相当于mask = set

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达


读取当前进程pending位图信息,通过set传出
int sigpending(sigset_t *set);
调用成功返回0,调用失败返回-1

注意

如果我们对所有信号都进行block,是不是就可以写出一个无法杀死的进程了?
不对,比如说9号信号是无法被屏蔽的!

信号捕捉

捕捉信号的时机:

在这里插入图片描述

解释:因为信号相关字段在PCB中,所以信号的检测是一定会在内核态进行。主程序遇到异常后,OS要转到内核态处理异常,当处理完准备返回用户态的时候,此时,进行信号的处理,检测信号是否被屏蔽,未屏蔽再中断,回到用户态,执行信号处理函数,然后再返回内核态,接着被中断的位置继续返回用户态,执行函数。


signal.h
功能:读取和修改与指定信号相关联的处理动作(更高级的signal)
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

  • signo:指定的信号编号
  • 若act非空,则根据act修改该信号的处理动作
  • 若oact非空,则将原来的信号处理动作,保留在此。
  • 成功返回0,出错返回-1

其中该结构体我们只关心画圈圈的两个参数,其他不用管。
在这里插入图片描述

下面的例子屏蔽了2号信号。并获得了2号信号的默认动作。


//makefile
mytext:mytext.ccg++ -o $@ $^ -std=c++11 -fpermissive
.PHONY:clean
clean:rm -f mytext//ytext.cc
#include<iostream>
#include<signal.h>
#include <unistd.h>
using namespace std;void handler(int signum)
{cout<<"处理信号:"<<signum<<endl;
}int main()
{// cout<<"hello world"<<endl;//内核从数据类型,用户栈定义struct sigaction act, oact;act.sa_flags = 0;sigemptyset(&act.sa_mask);act.sa_handler = handler;//设置到当前进程的PCB中sigaction(2, &act,&oact);cout<<"默认处理动作oact:"<<(int)(oact.sa_handler)<<endl;while(1) sleep(1);return 0;}

可重入函数

简单理解说,就是多个进程可同时进入的函数,并且多次运行的结果唯一,此函数就是可重入函数。反之如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant)函数。

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

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

volatile

volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
我们有如下代码逻辑,全局flag,当收到信号时,全局flag变成1,进程结束。

int flag = 0;
void handler(int sig)
{printf("change flag 0 to 1\n");flag = 1;
}int main()
{signal(2, handler);while(!flag);printf("process quiit normal\n");return 0;
}//makefile
mytext:mytext.ccg++ -o $@ $^ -std=c++11 
.PHONY:clean
clean:rm -f mytext

但是当我们加上O2优化的时候,此时进程就不退出了!
在这里插入图片描述
这是为什么呢?(因为编译器把代码优化了)
优化情况下,键入 CTRL-C,2号信号被捕捉,执行自定义动作,修改 flag=1,但是 while 条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显,while循环检查的flag并不是内存中最新的flag,这就存在了数据二异性的问题。while 检测的flag其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?需要 volatile。

volatile int flag = 0;   //volatile 防止编译器优化!
void handler(int sig)
{printf("change flag 0 to 1\n");flag = 1;
}int main()
{signal(2, handler);while(!flag);printf("process quiit normal\n");return 0;
}

加了volatile后,问题就被解决了。

在这里插入图片描述

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

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

相关文章

Vue 查看真实请求地址

当你在项目中配置了proxy代理&#xff0c;前端在浏览器开发调试的时候&#xff0c;是看不到真是的请求地址的。 这时候&#xff0c;后端要说话了&#xff1a;你这连的是我的地址吗&#xff1f;网络里这显示的也不对吧~ 前端: 额、不是在这里看的。既然你不相信我&#xff0c;…

替代普通塑料吸头的PFA移液吸头

目前市场上的规格&#xff1a;0.01ml、0.05ml、0.1ml、0.2ml、0.5ml、1ml、2ml、5ml、10ml等均可定制加工PFA材质枪头&#xff0c;可以适配市场上大部分移液枪&#xff0c;普兰德&#xff0c;大龙&#xff0c;赛默飞&#xff0c;赛多利斯&#xff0c;力辰、吉尔森&#xff0c;瑞…

K8S哲学 - probe 探针

探针分类&#xff1a; liveness probe readiness probe startup probe Liveness Probe&#xff1a;用于检查容器是否还在运行。如果 Liveness Probe 失败&#xff0c;Kubernetes 会杀死容器&#xff0c;然后根据你的重启策略来决定是否重新启动容器。常见的做法是使用与 Readin…

error解决expression before ‘static‘

问题现象 报警如下 跳转到提示第125行&#xff0c;但是这行明显是没有问题的。 问题分析 经过排查可以看到&#xff0c;是120行的末尾\在S32DS编译器里面被认为是“接下一行”的意思&#xff0c;120行注释掉之后&#xff0c;后面的121行、122行、123行均被注释掉&#xff0c;…

2024年3月 青少年软件编程(图形化) 等级考试试卷(一级)

2024.3青少年软件编程&#xff08;图形化&#xff09; 等级考试试卷&#xff08;一级&#xff09; 一、 单选题(共 25 题&#xff0c; 共 50 分) 1.单击下列哪个按钮&#xff0c; 能够让舞台变为“全屏模式” &#xff1f; &#xff08; &#xff09; A. B. C. D. 标准答案&am…

Redis系列3:高可用之主从架构

1 主从复制介绍 上一篇《Redis系列2&#xff1a;数据持久化提高可用性》中&#xff0c;我们介绍了Redis中的数据持久化技术&#xff0c;包括 RDB快照 和 AOF日志 。有了这两个利器&#xff0c;我们再也不用担心机器宕机&#xff0c;数据丢失了。 但是持久化技术只是解决了Redi…

Redis进阶——相互关注Feed流推送

目录 关注和取消关注业务需求实现步骤效果如下 共同关注业务需求实现步骤效果如下 Feed流实现方案Feed流简介三种Timeline方式三种模式对比 推送到粉丝收件箱业务需求Feed流的滚动分页 实现分页查询收件箱业务需求具体步骤如下 关注和取消关注 业务需求 当我们进入到笔记详情…

如何用C++写一个日期计算器

目录 前言 代码的布局 设计数据 方法声明 方法的实现 获取某年某月的天数 *全缺省的构造函数 * 拷贝构造函数 *赋值运算符重载 *析构函数 日期天数 日期天数 日期-天数 日期-天数 前置 后置 后置-- 前置-- 实现比较大小运算符重载思路 >运算符重载 运算…

互联网通信原理

互联网通信原理 ISO/OSI(开放系统互连)的七层模型 注意事项 上三层是为用户提供服务的&#xff0c;下四层负责实际数据传输下四层的传输单位 传输层&#xff08;数据段&#xff09;、网络层&#xff08;数据包&#xff09;、数据链路层&#xff08;数据帧&#xff09;、物理层…

图文教程 | 2024年最新Typora激活使用教程合集

前言 汇总一下网上的三种方法。 &#x1f4e2;博客主页&#xff1a;程序源⠀-CSDN博客 &#x1f4e2;欢迎点赞&#x1f44d;收藏⭐留言&#x1f4dd;如有错误敬请指正&#xff01; 关于安装教程&#xff1a;http://t.csdnimg.cn/SCIQ8http://t.csdnimg.cn/SCIQ8自行跳转安装 一…

35. 【Android教程】视频页面:ViewPager

ViewPager 是一种可以让用户通过左右滑动来切换页面的控件&#xff0c;通过它我们可以展示超过屏幕尺寸大小的内容&#xff0c;在某种程度上它可以说是实现多页面的最佳方式&#xff0c;同时 ViewPager 还支持任意动态的添加/删除页面。比如我们可以将不同的类别的内容分别放在…

java 创建和请求sse服务

主要依赖 <!--spring-boot父工程--><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.2.RELEASE</version></parent><dependency><gro…

AOP基础

一、AOP概述 AOP&#xff1a;Aspect Oriented Programming&#xff08;面向切面编程、面向方面编程&#xff09;&#xff0c;其实就是面向特定方法编程。 使用场景&#xff1a;①记录操作日志&#xff1b;②权限控制&#xff1b;③事务管理等。 优势&#xff1a;①代码无侵入…

学校管网的仿写

工字形布局完成 效果 代码部分 在这里插入代码片 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport…

密码学 | Random Oracle 随机预言机

​ &#x1f951;原文&#xff1a;究竟什么才是随机预言机呢&#xff1f; - 玄星的回答 &#x1f951;答主指出&#xff1a; 英文维基明明对 随机预言机 给出了两个完全不同的理解&#xff0c;但这两个理解之间的连接词却是 “Stated differently”&#xff0c;即 “换句话说…

Unity ECS

一&#xff1a;前言 ECS与OOP不同&#xff0c;ECS是组合编程&#xff0c;而OOP的理念是继承 E表示Entity&#xff0c;每个Entity都是一个有唯一id的实体。C表示Component&#xff0c;内部只有属性&#xff0c;例如位置、速度、生命值等。S表示System&#xff0c;驱动实体的行为…

npm i 依赖下载失败

git config --global url."https://".insteadOf git://解决npm install 报错 npm ERR code 128 Permission denied_please make sure you have the correct access right-CSDN博客

怎么把相机储存卡里的照片导出来?介绍两种方法

随着摄影技术的不断发展和普及&#xff0c;相机已成为我们记录生活、捕捉美好瞬间的设备。然而&#xff0c;对于许多摄影爱好者来说&#xff0c;如何将相机储存卡里的照片安全、高效地导出到电脑或其他设备中&#xff0c;却成为了一个令人头疼的问题。本文将为您详细介绍从相机…

c++IO

前言 大家好&#xff0c;我是jiantaoyab&#xff0c;本篇文章给大家介绍c中文件操作。 先回忆一下c语言文件操作 void Test_c_bin() {//二进制写ServerInfo info { "127.0.0.1", 8080 };FILE* fout fopen("test.bin", "wb");fwrite(&in…

18 统计网站每日的访问次数

1.将竞赛的数据上传HDFS,查看数据的格式 通过浏览器访问hdfs,查看该文档前面的部分数据 每条数据的字段值之间使用逗号隔开的 &#xff0c;最终时间是第五个自动&#xff0c;获取第五个字段值的中的年月日。 2.通过Idea创建项目mr-raceData ,基础的配置 修改pom.xml,添加依赖 …