【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 id

int 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);  //首先将信号集所有比特位清0
    sigemptyset(&oset);  //首先将信号集所有比特位清0
    sigaddset(&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;

    //首先将信号集所有比特位清0
    sigemptyset(&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;
}

在这里插入图片描述

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

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

相关文章

看到递归就晕?带你理解递归的本质!【基础算法精讲 09】

104 . 二叉树的最大深度 链接 : . - 力扣&#xff08;LeetCode&#xff09; 思路 : 对于题意&#xff0c;可以拆分为 : ans max(左子树的最大深度 &#xff0c; 右子树的最大深度) 1 ; 原问题 : 计算整颗树的最大深度 &#xff1b; 子问题 : 计算左右子树的最大深度 ;…

Postgresql中dblink扩展的使用

一、介绍 Postgresql数据库提供了一个dblink扩展的插件&#xff0c;能够直接在一个数据库中操作另外一个远程数据库&#xff0c;比如&#xff1a;一个数据库在服务器A上&#xff0c;另外一个数据库在服务器B上&#xff0c;我可以在A这台服务器数据库上面建立一个到B服务器数据库…

Redis是单线程还是多线程?

说Redis是单线程或者是多线程这种说法并不严谨&#xff0c;要拿版本说话&#xff0c;Redis的版本有很多3.x、4.x和6.x&#xff0c;版本不同架构也是不同的&#xff0c;不限定版本问是否单线程是不太严谨的。 版本3.x&#xff0c;最早版本&#xff0c;此时Redis是单线程的版本4…

精品ssm人事办公考勤报销管理系统

《[含文档PPT源码等]精品基于ssm办公管理系统[包运行成功]》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 软件开发环境及开发工具&#xff1a; Java——涉及技术&#xff1a; 前端使用技术&#xff1a;HTML5,CSS3、JavaS…

webrtc

stun服务 阿里云服务器安全组添加端口开放 webrtc-streamer视屏流服务器搭建 - 简书

安科瑞Acrel-2000ES 储能柜能量管理系统

安科瑞戴婷 安科瑞储能能量管理系统Acrel-2000ES&#xff0c;专门针对工商业储能柜、储能集装箱研发的一款储能EMS&#xff0c; 具有完善的储能监控与管理功能,涵盖了储能系统设备(PCS、BMS、电表、消防、空调等)的详细信息,实现了数据采集、数据处理、数据存储、数据查询与分…

浅谈 Linux 网络编程 - 网络字节序

文章目录 前言核心知识关于 小端法关于 大端法网络字节序的转换 函数 前言 在进行 socket 网络编程时&#xff0c;会用到字节流的转换函数、例如 inet_pton、htons 等&#xff0c;那么为什么要用到这些函数呢&#xff0c;本篇主要就是对这部分进行介绍。 核心知识 重点需要记…

4-如何进行细分市场的分析-02 细分行业的构成和基本情况

如何快速摸清行业的构成&#xff0c;通常会看同行或自己做过的相似的行业&#xff0c;会根据不同的行业来采用不同的研究方法。对于成熟的行业和不同的行业都会有一些比较通用的研究方式。 假设我们是在分析某一个行业&#xff0c;在分析行业的时候它的本质还是市场分析&#…

Leetcode300. 最长递增子序列 -代码随想录

题目&#xff1a; 代码(首刷看解析 2024年2月29日&#xff09;&#xff1a; class Solution { public:int lengthOfLIS(vector<int>& nums) {int n nums.size();if (n < 1) return 1;vector<int> dp(n, 1);int res 0;for (int i 1; i < n; i) {for(i…

springboot+vue实现oss文件存储

前提oss准备工作 进入阿里云官网&#xff1a;阿里云oss官网 注册 搜OSS&#xff0c;点击“对象存储OSS” 第一次进入需要开通&#xff0c;直接点击立即开通&#xff0c;到右上角AccessKey管理中创建AccessKey&#xff0c;并且记住自己的accessKeyId和accessKeySecret&#…

使用 Gradle 版本目录进行依赖管理 - Android

/ 前言 / 在软件开发中&#xff0c;依赖管理是一个至关重要的方面。合理的依赖版本控制有助于确保项目的稳定性、安全性和可维护性。 Gradle版本目录&#xff08;Version Catalogs&#xff09;是 Gradle 构建工具的一个强大功能&#xff0c;它为项目提供了一种集中管理依赖…

使用Python对数据进行rsa加密

#!/usr/bin/python3 import base64 import json import jsonpath import requests from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5 from base64 import b64decode, b64encodedef get_public_key():"""备注&#…

网络工程师笔记3

IP地址类型 A类 255.0.0.0B类 255.255.0.0C类 255.255.255.0D类 E类 子网掩码&#xff1a;从左到右连续的确定网络位 2-4-8-16-32-64-128-256 128 &#xff1a; 1000 0000 64 &#xff1a; 0100 0000 32 &#xff1a; 0010 0000 16 &#xff1a; 0001 0000 8 &am…

vue3 开发记录

1.引入nprogress插件&#xff0c;显示未声明文件 无法找到模块“nprogress”的声明文件。 解决方法&#xff1a; vite-env.d.ts // 解决引入模块的报错提示 declare module "nprogress";2.在 .evn 文件中创建了自定义环境变量 VITE_APP_BASE_URL 但在项目中使用时出…

【c语言】探索联合和枚举---解锁更多选择

前言 上一篇 讲解的是结构体相关知识&#xff0c;接着本篇主要讲解的是 联合和枚举 相关知识 结构体、联合体和枚举都属于 自定义类型。 那么接下来就跟上我的节奏&#xff0c;准备发车~ 欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xf…

如何在群晖NAS中开启FTP服务并实现公网环境访问内网服务

文章目录 1. 群晖安装Cpolar2. 创建FTP公网地址3. 开启群晖FTP服务4. 群晖FTP远程连接5. 固定FTP公网地址6. 固定FTP地址连接 本文主要介绍如何在群晖NAS中开启FTP服务并结合cpolar内网穿透工具&#xff0c;实现使用固定公网地址远程访问群晖FTP服务实现文件上传下载。 Cpolar内…

同局域网共享虚拟机(VMware)

一、前言 首先我们先来了解下 VMware 的三种网络模式桥接模式、NAT模式、仅主机模式&#xff0c;网络类型介绍详情可以参考下我之前的文档 Linux系统虚拟机安装&#xff08;上&#xff09;第三章 - 第9步指定网络类型。了解三种网络模式的原理之后&#xff0c;再来剖析下需求&…

Python进阶学习:axis=0和axis=1的区别和用法

Python进阶学习&#xff1a;axis0和axis1的区别和用法 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448; 希望得到您的订阅和…

Python is not set from command line or npm configuration 报错解决

问题 在 npm install 的过程中提示 Python is not set from command line or npm configuration 的报错&#xff0c;相信不少朋友都遇到过&#xff0c;出现这个问题的原因是缺少 python 环境所导致的。 解决方法 1、安装 python 官网&#xff1a;https://www.python.org/dow…

Halcon 求孔洞的大小和数量

文章目录 适用场景汽车按钮案例 适用场景 在工业中可以利用孔洞的多少和孔洞的大小来分析出产品的缺陷问题&#xff0c;例如一个产品有8个孔洞&#xff0c;孔洞多和少都会被识别为不合格产品&#xff0c;或者求出孔洞的面积&#xff0c;如果孔洞的大小超出一定的范围将视为不合…