Linux进程信号 | 信号处理

news2024/11/26 20:20:11

前面的文章中我们讲述了信号的产生与信号的保存这两个知识点,在本文中我们将继续讲述与信号处理有关的信息。

信号处理

之前我们说过在收到一个信号的时候,这个信号不是立即处理的,而是要得到的一定的时间。从信号的保存中我们可以知道如果一个信号之前被block,当解除block的时候,对应的信号会立即被递达。因为信号的产生是异步的,当前进程可能在做更重要的事情,当进程从内核态切换回用户态的时候,进程就会在OS的指导下进行信号的检测与处理。

用户态、内核态

首先我们先来讲讲这两个状态,用户态:执行自己写的代码的时候,进程所处的状态;内核态:执行OS的代码的时候,进程所处的状态。

  • 当进程的时间片到了需要切换时,就要执行进程切换逻辑。
  • 系统调用

之前在进程地址空间中我们学习过进程地址空间的相关知识,我们知道PCB连接到进程地址空间,然后通过页表的映射,映射到物理内存中。之前我们只学习了用户空间,里面有堆、栈、代码等。我们知道操作系统也是一段代码,而在进程地址空间中的内核空间就是存储的OS的代码与数据映射的地方,因此同样需要一张内核级的页表。以32位的系统为例子,所有的进程地址空间中的0-3GB都是不同的存放的是该进程自己的代码与数据,匹配了自己的用户级页表;所有进程的3-4GB都是一样的存放的是OS的代码与数据,每一个进程都可以看到同样的一张内核级页表,所有进程都可以通过统一的窗口看到同一个OS;OS运行的本质:其实都是在进程的地址空间中运行的;所以所谓的系统调用,其实就如同调用.SO中的方法,在自己的地址空间中进行函数跳转并返回即可。

此时就会出现一个问题,正应为OS的代码与数据跟用户的代码与数据在同一个地址空间中,为了防止用户随意的访问OS的数据与代码,因此就有了用户态与内核态。当执行自己的代码,对应的状态就是用户态,要对系统调用进行访问,OS就会对身份,执行级别进行检测,检测到不是内核态就会终止进程。在CPU中存在一种寄存器叫做CR3,里面有对应的比特位,比特位为0表征正在运行的进程是用户态,比特位为3表征正在运行的进程级别是内核态。由于用户无法直接对级别进行修改,因此OS提供的系统调用,内部在正式执行调用逻辑的时候会去修改执行级别。

进程是如何被调度的?

首先我们要讲一下OS。OS的本质是软件,本质是一个死循环;OS时钟硬件,每个很短的时间向OS发送时钟中断,然后OS要执行对应的中断处理方法。进程被调度就是时间片到了,然后OS将进程对应的上下文等进行保存并切换,选择合适的进程,这通过系统函数schedule()函数执行上面的保存任务。

内核如何实现信号的捕捉

 如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

sigaction

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

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

下面我们看一个简单的例子:

static void PrintPending(const sigset_t &pending) {
    cout << "当期进程的pending信号集:";
    for (int signo = 1; signo <= 31; ++signo) {
        if (sigismember(&pending, signo)) // 用于打印信号集
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

static void handler(int signo) { // 添加了static之后该函数只能在本文件中使用 
    cout << "对特定信号:" << signo << "执行捕捉动作" << endl;
    int cnt = 10;
    while (cnt) {
        cnt--;
        sigset_t pending;
        sigemptyset(&pending);
        sigpending(&pending);
        PrintPending(pending);
        cout << "打印完成pending信号集" << endl;
        sleep(1);
    }
}
int main() {
    struct sigaction act, oldact;
    memset(&act, 0, sizeof(act));
    memset(&oldact, 0, sizeof(oldact));
    act.sa_handler = handler; 
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3); // 可以添加其他信号的阻塞方式,在自定义捕捉时将其余收到的信号阻塞
    sigaddset(&act.sa_mask, 4);
    sigaddset(&act.sa_mask, 5);
    sigaction(2, &act, &oldact);

    while (true) {
        cout << getpid() << endl;
        sleep(1);
    }
}

通过在对2号信号实行自定义捕捉的时候给进程发送3,4,5号信号,就可以通过打印信号集来查看该信号是否被阻塞。 

其余知识点

可重入函数

我们以链表结点指针的头插为例子:

一般头插分为两步首先将新节点插入在链表之前,然后再将头指针指向新节点的地址,如果在第一步的时候进行了信号的自定义动作保存了当前函数执行的状态,在自定义动作之中又执行了一次链表头插的动作,那么当自定义动作处理结束之后,返回至用户态函数执行的地方,就会继续原先的插入动作,那么我们在自定义函数中的插入结点就会丢失,导致内存泄漏。

 main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。

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

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

volatile

下面我们来看一个关键字volatile,首先我们来看一个例子:

int quit = 0; // 保证内存可见性

void handler(int signo) {
    printf("change quit from 0 to 1\n");
    quit = 1;
    printf("quit : %d\n", quit);
}

int main() {
    signal(2, handler);

    while(!quit); //注意这里我们故意没有携带while的代码块,故意让编译器认为在main中,quit只会被检测

    printf("main quit 正常\n");

    return 0;
}

运行上述的代码,就与我们之前学习的一样会让全局变量quit由0变1,进行打印然后退出。

我们在编译的时候是有优化的级别的,可以根据不同的优化级别记性优化。我们选择-O2来对上述的代码记性优化,可以发现我们虽然可以自定义捕捉信号,变量quit同样也变成了1,但是却无法让程序退出。

下面我们来解释一下为什么? CPU匹配的运算种类只有两种,算术运算与逻辑运算,while循环的代码需要在CPU上执行,因为只有CPU能够进行计算,因此需要我们先将quit加载到CPU中,然后再进行真假的判断,在CPU中还有记录当前程序位置的指针,当判断条件生效之后,指针就会指向下一句代码。这就是为什么我们能够退出的原因。

 while循环是一种运算,这样的运算是需要运算源的,每次都需要将数据从内存加载到CPU中,编译器发现在main函数中quit的值并没有修改,而只是进行判断,编译器就会认为每次的quit数据都是一样的,那么就会进行优化将数据第一次load进CPU中,然后就不再进行加载工作,只检测CPU中寄存器的保存的quit数据,相当于让CPU中的quit替换掉了内存中的quit。这就导致了quit为什么进行了修改但是并没有退出的问题。为了告诉编译器,保证每次检测都要从内存中进行数据读取不要用寄存区中的数据覆盖,让内存数据可见,因此就有了volatile。

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

SIGCHLD

在进程等待的哪里我们学习过子进程退出之后如果父进程不进行处理,子进程就会变为僵尸进程,然后我们就学习了waitpid和wait函数清理僵尸进程。父进程可以以非阻塞或者阻塞的方式进行主动检测,由于子进程推出了,父进程暂时不知道。子进程在退出的时候会给父进程发送SIGCHLD信号,该信号的默认处理动作是忽略(SIG_DFL)什么都不做。

我们就可以使用自定义捕捉的方法进行检测 :

那么我们就设想可以在自定义捕捉中进行对僵尸进行的处理,这样就可以让父进程做自己的事情,可以自动对子进程进行回收。

pid_t id ;
void waitProcess(int signo) {
    printf("捕捉到一个信号: %d, who: %d\n", signo, getpid());
    sleep(5);
    while (1) {
        // 这里若设置的为0,那么如果有些子进程退出了,有部分子进程没有退出导致自定义捕捉的函数无法返回会一直阻塞在里面,因此要设置为非阻塞的等待方式
        pid_t res = waitpid(-1, NULL, WNOHANG); // -1表示等待任意一个子进程
        if (res > 0)
        {
            printf("wait success, res: %d, id: %d\n", res, id);
        }
        else break; // 如果没有子进程了?
    }
    printf("handler done...\n");
}

void handler(int signo) {
    printf("捕捉到一个信号: %d, who: %d\n", signo, getpid());
}

int main() {
    signal(SIGCHLD, waitProcess);
    // signal(SIGCHLD, handler);
    int i = 1;
    for (; i <= 10; i++) {
        id = fork();
        if (id == 0) {
            // child
            int cnt = 5;
            while (cnt)
            {
                printf("我是子进程, 我的pid: %d, ppid: %d\n", getpid(), getppid());
                sleep(1);
                cnt--;
            }

            exit(1);
        }
    }
    // 如果你的父进程没有事干,你还是用以前的方法
    // 如果你的父进程很忙,而且不退出,可以选择信号的方法
    while (1) {
        sleep(1);
    }

    return 0;
}

由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。

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

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

相关文章

CSP-J组初赛历年真题讲解第1篇

一、二进制基础 1.二进制数 00100100 和 00010100 的和是( )。 A.00101000 B.01100111 C.01000100 D.00111000 来源&#xff1a;模拟试题正确答案&#xff1a;D 讲解&#xff1a; 2.在二进制下&#xff0c;1011001()11001101011001( )1100110 A. 1011 B. 1101 C. 1010…

仓库Vuex

1. 搭建vuex仓库 1.1 安装 npm install vuexnext 1.2 引入 创建store文件夹&#xff0c;里面创建index.js&#xff0c;该js文件中写&#xff1a; import { createStore } from vuex // 引入子仓库 import model1 from "./model1.js" import model2 from "…

行为型设计模式05-备忘录模式

&#x1f9d1;‍&#x1f4bb;作者&#xff1a;猫十二懿 ❤️‍&#x1f525;账号&#xff1a;CSDN 、掘金 、个人博客 、Github &#x1f389;公众号&#xff1a;猫十二懿 备忘录模式 1、备忘录模式介绍 备忘录模式是一种行为型设计模式&#xff0c;用于在不破坏封装性的前提…

Spring Resources资源操作

文章目录 1、Spring Resources概述2、Resource接口3、Resource的实现类3.1、UrlResource访问网络资源3.2、ClassPathResource 访问类路径下资源3.3、FileSystemResource 访问文件系统资源3.4、ServletContextResource3.5、InputStreamResource3.6、ByteArrayResource 4、Resour…

H桥级联型五电平三相逆变器MATLAB仿真模型

H桥级联型五电平逆变器MATLAB仿真模型资源-CSDN文库https://download.csdn.net/download/weixin_56691527/87899094 模型简介&#xff1a; MATLAB21b版本 逆变器采用H桥级联的形式连接&#xff0c;加设LCL滤波器&#xff0c;三相负载构成主电路。 采用SPWM调制&#xff0c;可…

不宜使用Selenium自动化的10个测试场景

尽管在很多情况下测试自动化是有意义的&#xff0c;但一些测试场景是不应该使用自动化测试工具的&#xff0c;比如Selenium、WebDriver。 下面有10个示例&#xff0c;来解释为什么自动化在这种情况下使用时没有意义的&#xff0c;我还将为您提供每种方法的替代方法。 01.验证…

TreeView 简单使用

本文主要介绍 QML 中 TreeView 的基本使用方法&#xff0c;包括&#xff1a;TreeView的适用场景&#xff1b; 控件简介 QML TreeView 是 Qt Quick 中的一个组件&#xff0c;用于显示树形结构的数据。它提供了一种以层次结构方式展示数据的方式&#xff0c;其中每个节点可以包含…

ESP32学习之定时器和PWM

一.定时器代码如下&#xff1a; #include <Arduino.h>hw_timer_t *timer NULL; int interruptCounter 0;// 函数名称&#xff1a;onTimer() // 函数功能&#xff1a;中断服务的功能&#xff0c;它必须是一个返回void&#xff08;空&#xff09;且没有输入参数的函数 //…

【动态规划】路径问题

冻龟算法系列之路径问题 文章目录 【动态规划】路径问题1. 不同路径1.1 题目解析1.2 算法原理1.2.1 状态表示1.2.2 状态转移方程1.2.3 初始化1.2.4 填表顺序1.2.5 返回值 1.3 编写代码 2. 不同路径Ⅱ2.1 题目解析2.2 算法原理2.2.1 状态表示2.2.2 状态转移方程2.2.3 初始化2.2.…

性能测试学习之数据驱动性能测试

了解数据驱动测试理念、能够如何在jmeter中用多种方式实现数据驱动测试。 知识点&#xff1a;字符串拼接、计数器、循环控制器 1. 数据驱动的理念 1.1 定义 从数据文件中读取测试数据,驱动测试过程的一-种测试方法数据驱动可以理解为更高级的参数化 1.2 特点 测试数据与测试…

【Linux】socket 编程(socket套接字介绍、字节序、socket地址、IP地址转换函数、套接字函数、TCP通信实现)

目录 1、socket套接字介绍2、字节序简介字节序转换函数 3、socket地址专用socket地址 4、IP地址转换函数5、套接字函数6、TCP通信实现&#xff08;服务器端和客户端&#xff09; 橙色 1、socket套接字介绍 所谓套接字&#xff0c;就是对网络中不同主机上的应用进程之间进行双…

深入理解深度学习——Transformer:整合编码器(Encoder)和解码器Decoder)

分类目录&#xff1a;《深入理解深度学习》总目录 相关文章&#xff1a; 注意力机制&#xff08;Attention Mechanism&#xff09;&#xff1a;基础知识 注意力机制&#xff08;Attention Mechanism&#xff09;&#xff1a;注意力汇聚与Nadaraya-Watson核回归 注意力机制&…

国内唯一可以在本地搭建Stable Diffusion WebUI教程-安装时无需魔法安装全程流畅到尖叫

Stable Diffusion是什么 Stable Diffusion简称SD是一款Ai图片生成工具。“输入几句话,生成精美图片。” 比如说我一开头这幅图片就是用的SD生成的。 我在我的“ChatGPT让我变成了“超人”-如何提升团队30%效能质量提高100%的阶段性总结报告”里提到过midjourney,但是midjou…

使用Google工具类Guava自定义一个@Limiter接口限流注解

在Springboot中引用RateLimiter工具类依赖 <dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>30.1-jre</version> </dependency> 需要注意的是&#xff0c;Guava 的不同版本可能会有…

新手第一次做性能测试?性能测试流程详全,从需求到报告一篇打通

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、确认需求 确定…

3、互联网行业及产品经理分类

上一篇文章&#xff1a;2、产品经理的工作内容_阿杰学编程的博客-CSDN博客 1、产品经理分类 我们把产品经理划分成这样两个大的类型&#xff0c;一个是传统行业的&#xff0c;一个是互联网行业的。这个简单了解一下就行。 这个里面会发现绝大多数也是体育劳动&#xff0c;你比…

软件测试岗位都是女孩子在做吗?

听我一朋友说&#xff0c;测试岗位基本都是女孩子做。” 不知道是不是以前“软件测试岗”给人印象是“不需要太多技术含量”的错觉&#xff0c;从而大部分外行认为从业软件测试的人员中女生应占了大多数。比如有人就觉得&#xff1a;软件测试主要是细心活&#xff0c;所以女生…

2023 年各大互联网公司常见面试题(Java 岗)汇总

很多人都说今年对于 IT 行业根本没有所谓的“金三银四”“金九银十”。在各大招聘网站或者软件上不管是大厂还是中小公司大多都是挂个招聘需求&#xff0c;实际并不招人&#xff1b;在行业内的程序员基本都已经感受到了任老前段时间口中所谓的“寒气”。 虽然事实确实是如此&a…

30个接口自动化测试面试题,赶紧收藏

1. 什么是接口自动化测试&#xff1f; 答&#xff1a;接口自动化测试是指使用自动化工具对接口进行测试&#xff0c;验证接口的正确性、稳定性和性能等方面的指标。2. 为什么要进行接口自动化测试&#xff1f; 答&#xff1a;接口自动化测试可以提高测试效率&#xff0c;减少人…

新能源行业如何进行数据防泄漏

客户情况 某新能源电池企业专业从事于新能源锂离子动力电池和储能电池的研发、生产和销售&#xff0c;具备电芯、模组、BMS及Pack的完整资源开发能力。公司致力于通过持续不断地改进电池技术&#xff0c;为全球锂离子动力和储能领域提供数字化精准高效的新能源解决方案。 该企…