进程信号(2)

一、信号的处理

        进程对应信号的处理的一般步骤就是:先去遍历pending位图,找到比特位为1的位置对应的信号,然后再去检测block位图对应位置的比特位是否为1。若不为1,就hander表的对应位置去调用信号的处理动作函数,若为1,不做任何事。

        好了,既然我们已经知道了信号处理的一般步骤了,那么进程是在什么时候进行那几个步骤的呢?前面我们说了,是在合适的时候,那什么时候是合适的呢?其实,信号处理发生在由内核态返回用户态的时候。

1.1、内核态与用户态

        CPU在执行我们自己写的代码的时候,我们就称为用户态,但是在自己的代码中我们会使用系统调用接口(write,getpid...),这样我们必然就会访问os的内核数据或硬件资源,此时我们就称为内核态。用户不能以用户态的身份执行系统调用,必须让自己的身份变成内核态。

那么CPU怎么知道进程是处于用户态还是内核态呢?

        在CPU中,存在大量的寄存器,进程在执行的时候,会将自己的上下文数据加载到寄存器中。CPU中的寄存器分为可见寄存器和不可见寄存器。在不可见寄存器中有一个寄存器叫做CR3,它的作用是表示CPU运行级别,0表示内核态,3表示用户态,这就能够辨别是用户态还是内核态。

那么如何理解代码在操作系统上运行呢?

        在进程地址空间中,0—3G的部分我们称为用户空间,是用户自己写的代码,这些数据通过用户级页表映射到物理内存中。3—4G的部分我们称为内核空间,是操作系统的相关数据,这些数据通过内核级页表映射到物理内存中。开机时OS加载到内存中,OS在物理内存中只会存在一份,因为OS只有一份,所以OS的代码和数据在内存中只有独一份。

        内核级页表只有一份,不同的进程通过同一个内核级页表就可以访问同一个操作系统。

所以,进程进行系统调用的步骤就是:用户空间中的代码调用了系统调用——进程由用户态转成内核态——跳转到内核空间该接口的位置——通过内核级页表——访问物理内存中的os代码

内核态和用户态之间是怎么切换的呢?

        从用户态切换为内核态通常有如下几种情况:1、需要进行系统调用时。2、当前进程的时间片到了,导致进程切换(进程切换由os执行自己的调度算法完成)。3、产生异常、中断、陷阱等。其中,由用户态切换为内核态我们称之为陷入内核。

所以,如果系统调用完成时,进程切换完毕或者异常、中断、陷阱等处理完毕,进程将由内核态转变成用户态,此时就会对信号进行处理。

二、信号的捕捉

2.1内核如何进行信号捕捉

        当一个执行流正在执行我们的代码时,可能会因为某些原因,陷入内核,去执行操作系统的代码。当操作系统的代码执行完毕准备返回到用户态时,os会检查pending表(此时仍处于内核态,有权力查看当前进程的pending位图),如果某个信号处于未决状态,那就再去检测block位图,看该信号是否被阻塞。如果阻塞,就直接返回,接着执行用户的代码。

        如果未决信号没有被阻塞,那么此时就需要对该信号进行处理。

        如果待处理信号的处理动作是默认或者忽略,而这两种处理动作已经由os写好,可以直接在内核态下进行处理。执行该信号的处理动作后清除对应的pending标志位,如果没有新的信号要递达,就直接返回用户态,接着上次被中断的地方继续向下执行。

        但是,如果未决信号的处理动作是被自定义捕捉了的,那么我们就需要返回用户态,去执行用户自定义的处理动作的代码,执行完后再通过特殊的系统调用sigreturn再次陷入内核并清除对应的pending标志位,如果没有新的信号要递达,就直接返回用户态,接着上次被中断的地方继续向下执行。

信号的捕捉:

为什么不能在内核态下直接执行自定义捕捉动作的代码呢? 

        从理论上来说,是可以的,因为内核具有最高的执行权限。

        但是,我们不能这样做。因为如果在用户自定义的捕捉函数里面有非法操作,比如清空数据,如果在内核态执行这样的代码,后果将不堪设想。所以,不能让操作系统直接去执行用户的代码

2.2、sigaction(信号捕捉)

        信号捕捉除了前面用过的signal函数之外,我们还可以使用sigaction函数对信号进行捕捉。该函数可以读取和修改与指定信号相关联的处理动作,该函数调用成功返回0,出错返回-1。

NAMEsigaction - examine and change a signal actionSYNOPSIS#include <signal.h>int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

参数act和oldact都是结构体指针变量,该结构体的定义如下

struct sigaction
{void(*sa_handler)(int);void(*sa_sigaction)(int, siginfo_t *, void *);sigset_t   sa_mask;int        sa_flags;void(*sa_restorer)(void);
};

说明:

        sa_handler:该结构体变量就是信号的处理方法。我们可以给其赋值:SIG_IGN 或者 SIG_DFL 或者 自定义函数。

        sa_flags:直接将sa_flags设置为0即可。

我们写一段代码来使用一下sigaction:

#include <iostream>
#include <signal.h>
#include <assert.h>
#include <unistd.h>using namespace std;void hander(int signum)
{cout << "pid: " << getpid() << " "<< "获取了一个信号: " << signum << endl;
}int main()
{struct sigaction sig;struct sigaction osig;sigemptyset(&sig.sa_mask);sigemptyset(&osig.sa_mask);sig.sa_flags = 0;sig.sa_handler = hander;sigaction(2, &sig, &osig);while (true)sleep(1);return 0;
}

        sa_mask:当某个信号的处理函数被调用,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字。这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞,直到处理结束,该信号会在下次合适的时候被处理。

如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时,自动恢复原来的信号屏蔽字。

三、可重入函数

有下面这样的一个链表。

                                                 

对于该链表,我们有下面的头插函数:

void insert(Node* p)
{p->next = head;head = p;
}

main函数中我们调用了它

int main()
{
...Node p1;insert(&p1)
...
}

 信号捕捉函数中也调用了它:

void hander(int signum)
{
...insert(&p2);
...
}

        ~ 首先,main函数中调用了insert函数,想将结点p1插入链表,但插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回到用户态之前检查到有信号待处理,于是切换到handler函数。

 ~ 在hander函数中,我们需要插入p2,将p2插入后,返回用户态。此时链表结构如下:

~ 返回用户态后,继续执行插入p1的insert的第二步。此时链表结构如下:

        最终结果是,main函数和handler函数先后向链表中插入了两个结点,但最后只有p1结点真正插入到了链表中,而p2结点就再也找不到了,造成了内存泄漏。

        像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入。

        insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数。反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。

如果一个函数符合以下条件之一则是不可重入的:
1、调用了malloc或free,因为malloc也是用全局链表来管理堆的。
2、调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

四、关键字volatile

volatile是C语言的一个关键字,该关键字的作用是保持内存的可见性。

我们来看一看下面的代码:

#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;int flag = 0;void hander(int signum)
{(void)signum;cout << "change flag: " << flag;flag = 1;cout << "->" << flag << endl;
}int main()
{signal(2, hander);while (!flag);cout << "进程退出后flag: " << flag << endl;return 0;
}

        该程序的运行结果好像都在我们的意料之中,但是,如果我们使用的编译器优化程度太高,就会出现一些问题。

        代码中的main函数和handler函数是两个独立的执行流,而while循环是在main函数当中的,在编译器编译时只能检测到在main函数中对flag变量的使用,而且main函数中只是对变量flag进行了检测, 并没有对其值进行修改。所以在编译器优化级别较高的时候,会直接将flag的值保存到CPU的寄存器中。

        在编译器优化程度高的情况下,当进程运行起来,flag初始值0,就会被保存到CPU的寄存器里面,每次while循环检测的时候,CPU会直接到寄存器里面检测flag的值(CPU无法看到内存了),但是这个值一直是0。虽然我们对flag的值进行了修改,但是也只是将内存里面flag的值修改成了1,CPU寄存器里的值任然为0。while循环永远不会自动结束。

        在编译代码时携带-O3选项使得编译器的优化级别最高,此时再运行该代码,就算向进程发生2号信号,该进程也不会终止。

        为了解决这个问题,我们就可以使用volatile关键字对flag变量进行修饰,告知编译器,对flag变量的任何操作都必须真实的在内存中进行,即保持了内存的可见性。

#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;volatile int flag = 0;void hander(int signum)
{(void)signum;cout << "change flag: " << flag;flag = 1;cout << "->" << flag << endl;
}int main()
{signal(2, hander);while (!flag);cout << "进程退出后flag: " << flag << endl;return 0;
}

进程正常退出。 

五、SIGCHLD信号

        在进程等待的文章中,我们讲到,为了避免出现僵尸进程,父进程需要使用wait或waitpid函数等待子进程结束,父进程可以阻塞等待子进程结束,但是父进程阻塞就不能处理自己的工作了;当然也可以非阻塞地查询的是否有子进程结束等待清理,即轮询的方式,这样父进程在处理自己的工作的同时还要记得时不时询问一下子进程是否退出以及子进程的情况,程序实现复杂且效率低。

        其实,子进程在退出时会给父进程发生SIGCHLD信号,该信号的默认处理动作是忽略。

        于是,由于Linux的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用signal或者sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在退出时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

下面的代码中父进程就没有等待子进程:

​​#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;int main()
{signal(SIGCHLD, SIG_IGN);pid_t id = fork();if (id == 0){// 子进程cout << "我是子进程: pid: " << getpid() << endl;sleep(10);exit(0);}while (true){cout << "我是父进程: pid: " << getpid() << endl;sleep(1);}return 0;
}

最开始有两个进程,后面进程退出直接被回收了,并没有形成僵尸进程

        还有一种方法就是:父进程可以自定义SIGCHLD信号的处理动作,这样父进程就只需专心处理自己的工作,不必关心子进程了,子进程退出时会通知父进程,父进程在自定义信号处理函数中调用wait或waitpid函数回收子进程即可。这样,子进程退出后向父进程发送17号信号,父进程就会去调用自定义的处理动作,回收子进程。

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

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

相关文章

JS入门学习

JS JavaScript是一门解释型的脚本语言&#xff0c;其是弱类型的&#xff0c;对变量的数据类型不做严格的要求&#xff0c;变量的类型可以在运行过程中变化 JavaScript能改变HTML内容&#xff0c;属性&#xff0c;样式 大纲 使用方式变量运算符数组JS函数自定义对象事件补充 …

mysql中InnoDB的表空间--独立表空间

大家好&#xff0c;上篇文章我们在讲mysql数据目录的时候提到了表空间这个名词&#xff0c;它是一个抽象的概念&#xff0c;对于系统表空间来说&#xff0c;对应着文件系统中一个或多个实际文件&#xff1b;对于每个独立表空间来说&#xff0c;对应着文件系统中一个名为表名.ib…

node.js学习P3-P10

P3 npm package.json&#xff08;package解读npm工具换镜像源&#xff09; 一个package.json文件可以的作用 作为一个描述文件&#xff0c;描述了你的项目依赖哪些包 &#xff0c;用来干什么的允许我们使用“语义版本规则”&#xff0c;指明你项目依赖的版本让你的构建更好的…

Java绩效考核系统源码 springboot员工绩效考核系统源码

Java绩效考核系统源码 springboot员工绩效考核系统源码-009 源码下载地址&#xff1a;https://download.csdn.net/download/xiaohua1992/89352195 项目介绍 本系统的功能分为管理员和员工两个角色 管理员的功能有&#xff1a; &#xff08;1&#xff09;个人中心管理功能&a…

一文搞定cuda版本、显卡驱动及多CUDA版本管理

安装cuda是每个AI从业人员必经之路。网上关于cuda、显卡驱动已经相关命令很多都解释不清楚&#xff0c;于是本文梳理一下&#xff0c;既方便自己记忆&#xff0c;也方便小白学习。 CUDA 首先&#xff0c;CUDA版本&#xff0c;一般指cuda-toolkit&#xff0c;即cuda开发工具包…

XShell免费版的安装配置

官网下载 https://www.xshell.com/zh/free-for-home-school/ 下载地址 通过邮箱验证 新建会话 通过ssh登录树莓派 填写主机IP 点击用户身份验证 成功连接

高项案例分析知识点总结

文章目录 纠错题计算题进度估算成本管理立项管理版本管理组合管理知识产权信息技术计算题运筹学 纠错题 人&#xff1a;人员经验、能力、数量、缺少培训&#xff1b;自己一个人完成需求和计划不正确流程&#xff1a;先做什么&#xff0c;后做什么&#xff0c;流程是否正确。是…

c++ (命名空间 字符串)

思维导图&#xff1a; 定义自己得命名空间myspace,在myspace中定义string类型变量s1,再定义一个函数完成字符串逆置 #include <iostream> #include <cstring> //定义自己得命名空间myspace,在myspace中定义string类型变量s1,再定义一个函数完成字符串逆置 using n…

抽屉网关停,Digg类网站退出互联网舞台

关注卢松松&#xff0c;会经常给你分享一些我的经验和观点。 别人我不清楚&#xff0c;至少在松松我心中&#xff1a;抽屉网是世界著名的网站&#xff0c;而近期抽屉新热榜突然宣布关站了&#xff0c;我内心充满遗憾。因为抽屉网站收集的内容&#xff0c;让我看到了更大的世界…

【学习记录】服务器转发使用tensorboard

场景 代码在服务器上运行&#xff0c;想使用tensorboard查看训练的过程。 但是服务器上不能直接访问地址&#xff0c;所以要转发端口到本地&#xff0c;从而在本地网页中能够打开tensorboard。 参考&#xff1a;https://zhuanlan.zhihu.com/p/680596384 这时我们需要建立本地…

C++ 函数模板与模板函数

一 代码重用技术 函数 类与对象 继承与派生 多态&#xff08;函数重载、运算符重载、虚函数、纯虚函数与抽象类&#xff09; 泛型程序设计 通用的代码需要补受数据类型的影响&#xff0c;并且可以自动适应数据类型的变化&#xff0c;这种程序设计类型称为泛型程序设计。 二 模…

Logstash笔记

目录​​​​​​​ 一、简介 二、单个输入和输出插件 三、多个输入和输出插件 四、pipeline结构 五、队列和数据弹性 六、内存队列 七、持久化队列 八、死信队列 (DLQ) 九、输入插件 1)、beats 2)、dead_letter_queue 3)、elasticsearch 4)、file 5)、redis 十、…

字符串和字符串函数(1)

前言&#xff1a; 字符串在C语言中比较特别&#xff0c;没有单另的字符串类型&#xff0c;想要初始化字符串必须用字符变量的数组初始化&#xff0c;但是在C语言标准库函数中提供了大量能对字符串进行修改的函数&#xff0c;比如说可以实现字符串的的拷贝&#xff0c;字符串的追…

经常碰到的20个等待事件

经常碰到的20个等待事件 oracle等待事件简介 DBA团队维护的部分应用运行在oracle数据库平台&#xff0c;为及时了解数据库的运行情况&#xff0c;需要建立涵盖各个维度的监控体系&#xff0c;包括实例状态、空间使用率、ORA错误等数十项监控指标。这其中有一个有效判断数据库…

Nodejs+Websocket+uniapp完成聊天

前言 最近想做一个聊天&#xff0c;但是网上的很多都是不能实现的&#xff0c;要么就是缺少代码片段很难实现websocket的链接&#xff0c;更别说聊天了。自己研究了一番之后实现了这个功能。值得注意的是&#xff0c;我想在小程序中使用socket.io&#xff0c;不好使&#xff0…

从0.1nm到1mm:显微测量仪在抛光至粗糙表面测量中的技术突破

显微测量仪是纳米级精度的表面粗糙度测量技术。它利用光学、电子或机械原理对微小尺寸或表面特征进行测量&#xff0c;能够提供纳米级甚至更高级别的测量精度&#xff0c;这对于许多科学和工业应用至关重要。 在抛光至粗糙表面测量中&#xff0c;显微测量仪器具有从0.1nm到1mm…

java:程序包javax. servLet不存在

一.原因 1.项目Tomcat 服务器依赖未导入 2.项目的 SDK 版本选择错误 二.解决方法 方案一&#xff1a; 1.选择项目结构选项 2.导入Tomcat依赖 把tomcat里面的【jsp-api.jar】和【servlet-api.jar】这两个包导入 方案二&#xff1a; 1.选择项目结构选项 2.选择自己的jdk版本…

Golang | Leetcode Golang题解之第108题将有序数组转换为二叉搜索树

题目&#xff1a; 题解&#xff1a; func sortedArrayToBST(nums []int) *TreeNode {rand.Seed(time.Now().UnixNano())return helper(nums, 0, len(nums) - 1) }func helper(nums []int, left, right int) *TreeNode {if left > right {return nil}// 选择任意一个中间位置…

【Python性能优化】取最值的差异

取最值的差异 测试Windows 测试结果Linux 测试结果 测试 测试内容&#xff1a;从一组 x, y, z 坐标值中获得每个维度&#xff08;x、y、z&#xff09;的值域范围。此处不考虑将数据临时存放到内存&#xff0c;再整组获取值域的操作&#xff08;因为对单文件这么做问题不大&…

PS系统教学01

在前面几节内容基本介绍了PS的基本作用&#xff0c;简单的对PS中的某些基础功能进行介绍应用。 接下来我们进行系统的分享。 本次分享内容 基础的视图操作 接下来我们是对于PS工作区域的每个图标工具进行详细的分享 抓手工具缩放工具 这个图标是将工具栏由一列变成两列 一…