进程信号(2)

news2024/9/20 14:33:45

一、信号的处理

        进程对应信号的处理的一般步骤就是:先去遍历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。

NAME
       sigaction - examine and change a signal action
 
SYNOPSIS
       #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.coloradmin.cn/o/1702824.html

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

相关文章

ESP32C3驱动SPI NAND

最近收到了一片国产工业级SD NAND&#xff0c;可以替代SD卡&#xff0c;容量大&#xff0c;贴片封装&#xff0c;非常适合做飞控"黑匣子"。 不用写驱动程序自带坏块管理的NAND Flash&#xff08;贴片式TF卡&#xff09;&#xff0c;尺寸小巧&#xff0c;简单易用&…

深入理解Kubernetes的调度核心思想

一、引言 Kubernetes&#xff08;简称K8s&#xff09;是一个开源的容器编排系统&#xff0c;用于自动化部署、扩展和管理容器化应用程序。在Kubernetes集群中&#xff0c;调度器是一个核心组件&#xff0c;它负责将Pod&#xff08;Kubernetes中的最小部署单元&#xff09;分配…

网络协议——FTP(简介、搭建FTP服务端)

一、简介 1、什么是FTP&#xff1f; FTP&#xff08;File Transfer Protocol&#xff0c;文件传输协议&#xff09; TCP/IP 协议组的协议之一。常用20&#xff08;数据&#xff09;、21&#xff08;命令&#xff09;端口作为通讯端口。&#xff08;22为SSH端口&#xff09;F…

一口气看完es(上)

此系列博客分为上中下3篇&#xff1a;上篇是关于es的概念和对数据的增删改操作&#xff0c;中篇是对数据的查询、对搜索结果进行处理操作&#xff0c;下篇是介绍怎么在Java代码中调用和操作es。 基本概念 1、es是什么&#xff1f;有什么作用&#xff1f; es全名是elasticsea…

诚心分享!主食冻干横向对比:希喂、爱立方、K9等谁最值得入手?

主食冻干到底有必要喂吗&#xff1f;七年铲龄铲屎官告诉你&#xff0c;是真的很有必要喂&#xff01; 这些年随着宠物经济的发展、科学养宠的普及&#xff0c;现在养猫不仅局限在让猫吃饱就行&#xff0c;更多人开始关注到猫的饮食健康。大量的实际喂养案例证明了&#xff0c;传…

【Linux】centos7下载安装Python3.10,下载安装openssl1.1.1

目录 centos7下载安装Python&#xff08;版本3.10.14&#xff09; &#xff08;1&#xff09;网页下载python压缩包&#xff0c;并解压缩 &#xff08;2&#xff09;编译安装 Python在make altinstall时&#xff0c;报错及解决 &#xff08;3&#xff09;将安装目录和可执…

Python脚本启动应用并输入账号或密码

一、简介 如果每天要启动某个软件还要输入账号密码登录的需求的话&#xff0c;可以参考本文章&#xff1b; 二、Python环境 环境&#xff1a;Python3.11 已经在Windows电脑中配置Python环境变量&#xff0c;且配置了pipd的环境变量&#xff1b; 三、安装模块 安装所需要的…

【Linux学习】进程间通信 (2) —— 信号

下面是有关进程通信中信号的相关介绍&#xff0c;希望对你有所帮助&#xff01; 小海编程心语录-CSDN博客 目录 1. 信号 1.1 概念 1.2 信号的产生 1.3 信号的处理方式 2. 函数 2.1 kill() 函数 2.2 signal()函数 2.3 sigaction()函数 2.4 sigprocmask()函数 …

自定义函数python:深入解析与实操

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言&#xff1a;函数的命名与规范 二、函数命名&#xff1a;遵循规范&#xff0c;易于…

Python计算回归拟合各项指标

0、各项回归指标简介 Relative Root Mean Squared Error&#xff08;RRMSE&#xff09;&#xff1a;The RRMSE normalizes the Root Mean Squared Error (RMSE) by the mean of observations. It goes from 0 to infinity. The lower the better the prediction performance.T…

“AI黏土人”一夜爆火,图像生成类应用应该如何长期留住用户?

文章目录 最近大火的“AI黏土人”&#xff0c;一股浓浓的《小羊肖恩》风。 凭借这这种搞怪的风格&#xff0c;“AI黏土人”等图像生成类应用凭借其创新技术和市场需求迅速崛起并获得巨大关注。然而&#xff0c;要保持用户黏性并确保长期发展&#xff0c;这些应用需要采取一系列…

Spring常用注解(超全面)

官网&#xff1a;核心技术SPRINGDOC.CN 提供 Spring 官方文档的翻译服务&#xff0c;可以方便您快速阅读中文版官方文档。https://springdoc.cn/spring/core.html#beans-standard-annotations 1&#xff0c;包扫描组件标注注解 Component&#xff1a;泛指各种组件 Controller、…

第53期|GPTSecurity周报

GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区&#xff0c;集成了生成预训练Transformer&#xff08;GPT&#xff09;、人工智能生成内容&#xff08;AIGC&#xff09;以及大语言模型&#xff08;LLM&#xff09;等安全领域应用的知识。在这里&#xff0c;您可以找…

Leecode热题100---46:全排列(递归)

题目&#xff1a; 给定一个不含重复数字的数组 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 思路&#xff1a; 元素交换函数递归&#xff1a; 通过交换元素来实现全排列。即对于[x, nums.size()]中的元素&#xff0c;for循环遍历每个元素分别成…

streamlit 学习

表情网站 https://getemoji.com/ 官网&#xff1a; https://streamlit.io/ 文档 https://docs.streamlit.io/develop/api-reference/chat/st.chat_message 安装&#xff1a; pip install streamlit启动 以下的python 文件指写streamlit 程序的脚步。 1、先切换目录到Pyth…

回溯算法之简单组合

哦吼&#xff01;今天结束了二叉树&#xff0c;开始回溯算法 其实也需要用到迭代&#xff0c;哈哈哈哈&#xff0c;但是这个暴力穷举真的好爽。 先记一下回溯算法的基本框架吧 老规矩&#xff1a; 还是有结束条件 但是后面就不太一样了 这里就是for循环&#xff0c;循环n…

2024年中国金融行业网络安全案例集

随着科技的飞速发展,金融行业与信息技术的融合日益加深,网络安全已成为金融行业发展的生命线。金融行业作为国家经济的核心支柱&#xff0c;正在面临着日益复杂严峻的网络安全挑战。因此&#xff0c;深入研究和探讨金融行业的网络安全问题&#xff0c;不仅关乎金融行业的稳健运…

聚数力 以数兴 | 与“闽”同行,共话数字未来

闽江之畔&#xff0c;数智腾飞。5月24日&#xff0c;第七届数字中国建设峰会在海峡国际会展中心盛大举办。本届展会的主题是“释放数据要素价值&#xff0c;发展新质生产力”&#xff0c;由国家发展改革委、国家数据局、福建省人民政府等单位共同主办&#xff0c;福州市人民政府…

【电路笔记】-状态可变滤波器

状态可变滤波器 文章目录 状态可变滤波器1、概述2、**状态可变滤波器电路**3、状态可变滤波器示例4、陷波滤波器设计5、总结状态可变滤波器是一种多反馈滤波器电路,可以从同一单个有源滤波器设计中同时产生所有三种滤波器响应:低通、高通和带通。 1、概述 状态可变滤波器使用…

你也许不知道,自己可能是一个热人

今天想跟大家分享的&#xff0c;是一种很少有人了解的人格特质。它非常普遍&#xff0c;许多人都或多或少有一些倾向&#xff0c;但却很少有人意识到它。 不妨看一看&#xff0c;你有没有下面这些特征&#xff1a; 有着极其旺盛的求知欲&#xff0c;对许多奇奇怪怪的问题都有着…