【Linux】进程信号 --- 信号的产生 保存 捕捉递达

文章目录

  • 信号的感知
    • 信号的结构描述
  • 一、信号的产生
    • 1.通过键盘发送信号
    • 2.通过系统调用发送信号
  • 二、信号的保存(PCB内部的两张位图和一个函数指针数组)
    • 理解三张数据结构表block pending haldler
  • 三、通过代码编写 理解 信号的保存和递达
    • 1.信号集操作的库函数
    • 2. 系统调用: sigprocmask 和 sigpending
    • 代码实践


信号的感知

关于信号这个话题我们其实并不陌生,我们想要杀死某个后台进程的时候,无法通过ctrl+c热键终止进程时,我们就会通过kill -9的命令来杀死进程。 查看信号也比较简单,通过kill -l命令就可以查看信号的种类,虽然最大的信号编号是64,但实际上所有信号只有62个信号,1-31是普通信号,34-64是实时信号,只讨论普通信号。

  • 进程之所以能够识别信号,是因为程序员将对应的信号种类和逻辑已经写好了的。
  • 当信号发给进程后,进程不一定要立刻去处理,可能有更加紧急的任务,会在合适的时候去处理。
  • 进程收到信号到处理信号之前会有一个窗口期,这个期间要将收到的信号进行保存。
  • 处理信号也叫信号的捕捉,方式有三种:默认动作,自定义动作,忽略。

本篇文章将从下面的三个阶段解释信号
在这里插入图片描述

信号的结构描述

我们知道信号是发送给进程的,如果进程当前并不处理这个信号,那么信号就需要被保存,以便于将来在合适的时候处理该信号,那么这个信号应该被保存在哪里呢?其实应该被保存在PCB struct task_struct{}里面,进程收到了哪些信号,进程要对信号做怎样的处理,这些信息都属于进程的信息,那么这些信息就理应被保存在PCB里面。

在PCB里面有对应的信号位图,操作系统用信号位图来保存信号的,31个普通信号,我们可以选择用32个比特位的unsigned int signal整数来进行保存。比特位的编号代表信号的编号,比特位的0或1代表进程是否接收到该信号


在这里插入图片描述
在这里插入图片描述

一、信号的产生

1.通过键盘发送信号

最常用的发送信号方式就是一个热键ctrl+c,这个组合键其实会被操作系统解释成2号信号SIGINT,通过man 7 signal就可以查看到对应的信号和其默认处理行为等等信息。
在这里插入图片描述

我们并未对2号信号做任何特殊处理,所以进程处理2号信号的默认动作就是Term,也就是终止进程。平常在我们终止前台进程的时候,大家的第一感受就是只要我们按下组合键ctrl+c,进程就会被立马终止,所以我们感觉进程应该是立马处理了我们发送的信号啊,怎么能是待会儿处理这个信号呢?值得注意的是,我们的感官灵敏度和CPU的灵敏度是不在同一个level的,我们直觉感受到进程是立马处理该信号的,但其实很大可能进程等待了几十毫秒或几百毫秒,而这个过程我们是无法感受到的,但事实就是如此,进程需要保存信号等待合适的时候再去处理信号。

在这里先介绍下面介绍一个接口叫做signal,它可以用来捕捉对应的信号,让进程在递达处理信号时不再遵循默认动作,而是按照我们所设定的函数进行递达处理,这个自定义的方法函数就是handler,signal的第二个参数其实就是接收返回值为void参数为int的函数的函数指针,所以在使用handler时我们需要传信号编号和处理该信号编号时所遵循的自定义方法的函数名即可。
signal函数的返回值我们一般不关注,signal函数调用成功时返回handler方法的函数指针,调用失败则返回SIG_ERR宏。

在这里插入图片描述
下面是正常退出进程,我们使用Ctrl+c或者kill -9 进程号终止进程

#include<iostream>
#include<unistd.h>
#include <sys/types.h>using namespace std;
void handler(int signo)
{cout << "signo:" << signo << endl;
}int main()
{//signal(2,handler);while(true){cout << "running......" << "getpid:" << getpid() <<endl;sleep(1);}return 0;
}

在这里插入图片描述
在这里插入图片描述
2号信号的默认行为是终止进程,我们接下来使用signal接口,自定义信号处理函数,让信号的处理成为我们想要的状态

#include<iostream>
#include<unistd.h>
#include <sys/types.h>
#include <signal.h>using namespace std;
void handler(int signo)
{//因为2号信号的默认行为是中断进程,如果我们自己定义的handler方法里面没有exit(),那么2号信号也就不会退出cout << "signo:" << signo  << "现在是我自定义的信号处理函数"<< endl;
}int main()
{signal(2,handler);while(true){cout << "running......" << "getpid:" << getpid() <<endl;sleep(1);}return 0;
}

在这里插入图片描述
我们看到的现象就是:通过键盘的热键将2号信号递达,执行我们的handler方法,此时Ctrl+c没有作用了,因为此时的handler方法里面没有exit函数,此时终止进程的方法就是通过kill -9 进程号 ,9号信号没办法被捕捉,即使这么做,os也不会响应

2.通过系统调用发送信号

1.kill系统调用

pid 表示接收信号的进程 ID,sig 表示要发送的信号类型。如果函数调用成功,则返回 0,否则返回 -1 并设置 errno
在这里插入图片描述

我们利用kill调用实现一个kill命令


//实现kill命令 kill -X idint main(int argv, char* argc[])
{if(argv != 4){cout << "User should Enter: kill -x process_id" << endl;exit(2);}else{pid_t id = atoi(argc[3]);int sig = atoi(argc[2]+1);int ret = kill(id, sig);if(ret != 0){perror("kill");}}
}

在这里插入图片描述
2. raise 函数
raise 函数是一个简单的发送信号的函数,可以用来向当前进程发送信号。raise 函数的原型如下:在这里插入图片描述

二、信号的保存(PCB内部的两张位图和一个函数指针数组)

未决 阻塞 递达概念的抛出

  1. 信号会在合适的时候被进程处理,执行信号处理的动作,称为信号递达,信号递达前的动作被称为信号捕捉,我们一般通过signal()或sigaction()进行信号的捕捉,然后对应的handler方法会进行信号的递达处理。当然如果你不自定义handler方法的话,那递达处理的动作就不会由handler执行,操作系统自己会根据默认或忽略行为对信号进行递达处理。

  2. 信号被保存,但并未被递达处理叫做信号未决!意思就是此时进程已经收到信号了,但信号尚未被进程递达,此时称之为信号未决。

  3. 注意阻塞和忽略是两种完全不同的概念,阻塞指的是信号被阻塞,无论进程是否收到该信号,进程永远都不会递达这个信号。而忽略是进程收到该信号后,对信号进行递达时的一种处理行为,进程在递达时可以选择忽略该信号,也就是直接将信号位图(实际是pending位图)中对应的比特位由1置0之后不再做任何处理。

  4. 还有一种状态是信号阻塞,此状态下即使信号已经被收到,但永远不会被递达,只有信号解除阻塞之后,该信号才会被递达。 信号是否产生和信号阻塞是无关的,就算一个信号没有被产生,没有被发送给进程,但进程依旧可以选择阻塞该信号,意味着将来如果进程收到了该信号,那该信号也不会被递达,只有解除阻塞之后才可以被递达。

理解三张数据结构表block pending haldler

  • 在内核中操作系统为了维护信号,为其创建了三个内核数据结构,也就是三张表,分别为pending表,block表,handler表,前两个表有专业的称呼叫做pending信号集和block信号集,当进程收到信号时,对应pending位图中的比特位就会由0置1,当某个进程被阻塞时,对应block位图中的比特位就会由0置1。
  • 当调用signal捕捉函数时,如果处理行为采取自定义,则用户层定义的handler函数的函数名就会被加载到对应的内核数据结构handler表里面,内核调用handler进行自定义处理时,就会去handler表里面进行查找。指针数组的下标代表不同的信号编号,指针数组的内容代表对应信号被递达时调用的handler方法。
  • 如果一个信号想要被递达,最多需要进行两次检测,第一次判断其是否为阻塞信号,如果是则判断结束,该信号一定不会被递达。如果不是则进行第二次判断,pending信号集中比特位是否为1 ,如果为1说明该进程确实收到了对应的信号,那就进行递达即可,如果为0说明该进程没有收到对应信号,则不进行递达。
    -在这里插入图片描述

三、通过代码编写 理解 信号的保存和递达

1.信号集操作的库函数

#include <signal.h>
int sigemptyset(sigset_t *set);  //将信号集所有的比特位都置0
int sigfillset(sigset_t *set); //将信号集所有的比特位都置1
int sigaddset (sigset_t *set, int signo); //向信号集中添加某个信号
int sigdelset(sigset_t *set, int signo); //删除信号集中某个信号
int sigismember(const sigset_t *set, int signo); //判断signo信号时候在set所指向的信号集中

2. 系统调用: sigprocmask 和 sigpending

我们之前所说的block位图,其实还有一些其他的称呼:信号屏蔽字,阻塞信号集。
sigprocmask是一个可以读取或修改进程信号屏蔽字的函数,set和oset均为输出型参数,函数内部会对set和oldset指针指向的sigset_t类型变量做修改。如果oset为非空指针,则读取当前进程的信号屏蔽字通过oset指针变量传出。如果set为非空指针,则更改当前进程的信号屏蔽字,how通过传递宏的方式实现sigprocmask的不同功能,SIG_BLOCK用于添加某些信号到信号屏蔽字当中,SIG_UNBLOCK用于移除信号屏蔽字的某些信号,SIG_SETMASK用于通过set参数将函数外sigset_t类型的信号集 设置到 内核中PCB里面的信号屏蔽字。如果set和oset同时为非空指针,则先将原来的信号屏蔽字(set指向的信号集)备份到oset指向的信号集里面,然后再通过how和set参数对内核中PCB的信号屏蔽字做修改。
在这里插入图片描述
下面便是how参数的选项,其实就是宏
在这里插入图片描述

sigpending用于将内核PCB中的pending位图掩码返回到set参数,进行传出。
我们可以通过这个函数取到内核中pending信号集的内容,将其放到用户层set所指向的sigset_t类型的变量里面,用户层就可以输出sigset_t信号集变量的内容,进行观察等一系列操作。
在这里插入图片描述

代码实践

  1. 在了解上面与信号有关的库函数接口以及系统调用接口之后,我们可以来实现一段代码,我们想屏蔽一下2,3号信号,此时向进程发送对应信号,信号一定是不被递达的,但是pending位图中的第2和第3个比特位一定被置为1了,我也想看看pending位图的变化。以上现象我们通过代码运行结果来观察。
//屏蔽2号 3号信号,看结果
int main()
{signal(2,handler);  //捕捉信号signal(3,handler);sigset_t set, oset;sigemptyset(&set);  //首先将信号集所有比特位清0sigemptyset(&oset);  //首先将信号集所有比特位清0sigaddset(&set, 2);  //向信号集中添加2号信号sigaddset(&set, 3); //向信号集中添加3号信号//检查两个信号是否添加到信号集if(sigismember(&set, 2) != 1 || sigismember(&set, 3) != 1){cout << "信号添加失败, 重新添加"  <<endl;exit(2);}sigprocmask(SIG_BLOCK, &set, &oset);  //屏蔽2号 3号信号,将原来的信号输出到oset中while(true){sleep(1);cout << "running...."  << "getpid:" << getpid() << endl;}return 0;
}

在这里插入图片描述
此时信号被阻塞,我们使用热键没办法进行信号的递达,自然也就无法调用handler方法

  1. 这段代码在理解上有一个关键点就是用户层和内核层的分辨,在开始屏蔽数组sigarr内部的信号之前所做的工作,其实都是在用户层准备的工作,对内核中的block信号集,pending信号集未产生任何影响,第一行的signal会陷入内核,因为他要把myhandler的函数地址设置进信号处理函数的方法表里面,所以进程会陷入内核。而其他我们定义的block oblock pending等sigset_t类型的变量实际都是为使用系统调用接口做的准备工作,用一些库函数sigemptyset() sigaddset() 进行变量的初始化,做完这些准备工作之后,我们才调用系统调用接口,比如sigprocmask将用户层定义的block信号集设置进内核的信号屏蔽字当中,让进程对2和3信号进行阻塞,我们想看看在阻塞过程中,如果我们向进程发送信号,进程是否会递达呢?并且还想看到pending信号集的变化,所以需要调用sigpending系统调用接口,将内核中的pending信号集不断的加载到用户层的pending对象里面来,然后我们多次打印这个pending对象的内容即可。我们当然无法通过调用某个函数输出pending对象内容,但可以利用一下sigismember来判断所有的信号是否在pending位图中,如果是就输出1,不是就输出0,这样打印出的一行结果正好就相当于32个比特位。在10s之后,我们对信号解除阻塞,解除的方式也很简单,调用sigprocmask,将oblock的内容设置到内核即可,oblock中的比特位全部都是0,则相当于解除对所有信号的屏蔽,解除屏蔽之后,此时进程刚好处于内核态(因为调用了sigprocmask系统调用),检测到有信号需要被递达,那么直接递达该信号即可

#define MAX_SIGNO 31
const  vector<int> sigarr = {2,3};void show_pending(const sigset_t& pending)
{for(int i  = MAX_SIGNO; i > 0; i--){if(sigismember(&pending, i))cout << "1" ;else cout << "0";}cout << endl;
}
int main()
{cout << "running......" << "getpid:" << getpid() <<endl;//捕捉信号signal(2,handler);  signal(3,handler);sigset_t block, oblock, pending;//首先将信号集所有比特位清0sigemptyset(&block);  sigemptyset(&oblock);  sigemptyset(&pending);  //添加信号集for(auto signo : sigarr){sigaddset(&block, signo);}//检查两个信号是否添加到信号集if(sigismember(&block, 2) != 1 || sigismember(&block, 3) != 1){cout << "信号添加失败, 重新添加"  <<endl;exit(2);}//屏蔽2号 3号信号,将原来的信号输出到oset中sigprocmask(SIG_BLOCK, &block, &oblock);  //打印pending表int cnt = 10;while(true){sigemptyset(&pending);sigpending(&pending);show_pending(pending);sleep(1);//解除阻塞if(cnt-- == 0){sigprocmask(SIG_SETMASK, &oblock, nullptr);cout << "解除阻塞" << endl;}}return 0;
}

在这里插入图片描述

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

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

相关文章

基于java SSM springboot+redis网上水果超市商城设计和实现以及文档

基于java SSM springbootredis网上水果超市商城设计和实现以及文档 博主介绍&#xff1a;多年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收藏 …

分布式一致性算法-Raft

分布式一致性算法Raft 分布式一致性问题Raft算法细节节点状态节点状态演变选举leader过程日志复制过程 选举leader初始的选举领导者故障后的选举拆分投票 日志复制网络分区 再看分布式一致性问题写在最后 分布式一致性问题 假设有一个单节点的系统&#xff0c;这个系统是一个数…

【日常聊聊】程序员是如何看待“祖传代码”的?

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;日常聊聊 ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 方向一&#xff1a;祖传代码的历史与文化价值 方向二&#xff1a;祖传代码的技术挑战与机遇 方向三&#xff1a;祖传代码与现…

【零基础入门TypeScript】对象

目录 句法 示例&#xff1a;对象文字表示法 TypeScript 类型模板 示例&#xff1a;Typescript 类型模板 示例&#xff1a;对象作为函数参数 示例&#xff1a;匿名对象 鸭子打字 例子 对象是包含一组键值对的实例。这些值可以是标量值或函数&#xff0c;甚至是其他对象的…

react 路由的基本原理及实现

1. react 路由原理 不同路径渲染不同的组件 有两种实现方式 ● HasRouter 利用hash实现路由切换 ● BrowserRouter 实现h5 API实现路由切换 1. 1 HasRouter 利用hash 实现路由切换 1.2 BrowserRouter 利用h5 Api实现路由的切换 1.2.1 history HTML5规范给我们提供了一个…

GB28181 —— Ubuntu20.04下使用ZLMediaKit+WVP搭建GB28181流媒体监控平台(连接带云台摄像机)

最终效果 简介 GB28181协议是视频监控领域的国家标准。该标准规定了公共安全视频监控联网系统的互联结构, 传输、交换、控制的基本要求和安全性要求, 以及控制、传输流程和协议接口等技术要求,是视频监控领域的国家标准。GB28181协议信令层面使用的是SIP(Session Initiatio…

FPFH特征匹配以及ransac粗配准

一、代码 Python import open3d as o3d import numpy as npdef extract_points(point_cloud, salient_radius5, non_max_radius5, gamma_210.95, gamma_320.95, min_neighbors6):keypoints o3d.geometry.keypoint.compute_iss_keypoints(point_cloud,salient_radiussalient_…

【MATLAB源码-第151期】基于matlab的开普勒化算法(KOA)无人机三维路径规划,输出做短路径图和适应度曲线。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 开普勒优化算法&#xff08;Kepler Optimization Algorithm, KOA&#xff09;是一个虚构的、灵感来自天文学的优化算法&#xff0c;它借鉴了开普勒行星运动定律的概念来设计。在这个构想中&#xff0c;算法模仿行星围绕太阳的…

Windows下查看端口占用以及关闭该端口程序

打开命令窗口 windowsR 输入 cmd 查看所有运行端口 netstat -ano 该命令列出所有端口的使用情况。 在列表中我们观察被占用的端口&#xff0c;比如是 80&#xff0c;首先找到它。 查看被占用端口对应的 PID netstat -aon|findstr "80" 回车执行该命令&#xff…

StarRocks——Stream Load 事务接口实现原理

目录 前言 一、StarRocks 数据导入 二、StarRocks 事务写入原理 三、InLong 实时写入StarRocks原理 3.1 InLong概述 3.2 基本原理 3.3 详细流程 3.3.1 任务写入数据 3.3.2 任务保存检查点 3.3.3 任务如何确认保存点成功 3.3.4 任务如何初始化 3.4 Exactly Once 保证…

windows安装部署node.js并搭建Vue项目

一、官网下载安装包 官网地址&#xff1a;https://nodejs.org/zh-cn/download/ 二、安装程序 1、安装过程 如果有C/C编程的需求&#xff0c;勾选一下下图所示的部分&#xff0c;没有的话除了选择一下node.js安装路径&#xff0c;直接一路next 2、测试安装是否成功 【winR】…

Python 实现Excel自动化办公(中)

在上一篇文章的基础上进行一些特殊的处理&#xff0c;这里的特殊处理主要是涉及到了日期格式数据的处理&#xff08;上一篇文章大家估计也看到了日期数据的处理是不对的&#xff09;以及常用的聚合数据统计处理&#xff0c;可以有效的实现你的常用统计要求。代码如下&#xff1…