我的编程空间,编程开发者的网络收藏夹
学习永远不晚

【linux】进程信号——信号的保存和处理

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

【linux】进程信号——信号的保存和处理

在这里插入图片描述
上一章主要讲述了信号的产生:【linux】进程信号——信号的产生
这篇文章主要讲后面两个过程。

一、阻塞信号

1.1 信号的相关概念

  • 实际执行信号的处理动作称为信号递达(Delivery)。
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。

因为信号不是被立即处理的,所以在信号产生之后,递达之前的这个时间窗口称作信号未决,也就是把信号暂时保存起来。

  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作

注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
而且没有信号产生我们也可以选择阻塞某个信号。

1.2 在内核中的构成

我们知道发送信号的本质:修改PCB中的信号位图。 而阻塞和未决也是通过位图的方式来保存信号。它们的位图也存在于进程的PCB内。

在这里插入图片描述

位图的第几个比特位代表第几个信号。
对于block,比特位的内容代表是否阻塞信号
对于pending,比特位的内容代表是否收到信号
对于handler,他是一个函数指针数组,代表处理动作
信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
而只要阻塞位图对应比特位为1,那么信号永远不能递达
如果一个信号想要递达,那么pending位图对应的比特位为1,block位图对应的比特位为0。

总结:

1️⃣ 因为pending和block是两个位图,所以不会互相影响,一个信号没产生并不影响先被阻塞。
2️⃣ 进程能够识别信号是因为,内核当中有这三种结构,它们组合起来就能够识别信号。
3️⃣ 当有多个信号同时来的时候,因为位图只有一个比特位,所以只会处理一次,其他的信号都会被丢失。

二、捕捉信号概念

2.1 内核态和用户态

信号产生后不会立即进行处理,而是在合适的时候进行处理。那么什么时候是合适的时候呢?

从内核态返回用户态的时候进行处理。

如果用户态想要获得操作系统自身资源(getpid……)或者硬件资源(write……)的时候必须通过系统调用接口完成访问。
而我们无法以用户态的身份调用系统调用,必须让自己的状态变成内核态。

所以往往系统调用比较花费时间,我们应该避免频繁调用系统接口。

既然有内核态和用户态,那么我们怎么辨别我们当前是哪个身份呢?

CPU内存有寄存器,而寄存器又分为可见寄存器(EXP)和不可见寄存器(状态寄存器),而所有保存在寄存器跟当前进程强相关的数据叫做上下文数据
CPU里面有一个叫做CR3的寄存器,它表征的就是当前进程的运行级别:
0表示内核态
3表示用户态

那么一个进程是如何进入操作系统中执行方法呢?
在这里插入图片描述
因为操作系统会加载到内存且只有一份,所以内核级页表也只需要一份。在CPU里有一块寄存器指向这个内核级页表,进程切换时这个寄存器不变
所以进程可以在特定的区域内以内核级页表的方式访问操作系统的代码和数据。当进程想要访问OS的接口,直接在自己的进程地址空间跳转即可

而我们知道操作系统有自己的保护机制,用户凭什么能执行访问操作系统数据的接口呢?

当想要跳转到内核区会进行权限认证,如果CR寄存器显示的是内核态就可以访问,反之阻止访问。但是我们怎么把用户态切换成内核态呢?当我们调用系统接口的时候,起始的位置会帮忙改变,先把CR3中的用户态改成内核态,然后再跳转到内核区。

2.2 信号捕捉流程图

在这里插入图片描述
当执行代码要调用系统调用接口时,本来应该调用完成后返回用户态继续执行代码。但是我们知道用户态和内核态的转换消耗时间很大,所以这里不会直接返回。
在这里插入图片描述
它会去找task_struct中的三张表先遍历block,当发现为1就跳过,如果不是1就看pending表如果为1就进入handler完成动作。而默认动作直接杀死进程,忽略动作直接把pending位图中的1置为0。
但是如果时自定义动作,我们自己写的handler方法在用户态,因为内核态不能直接访问用户态(从技术上可以,但是不能,为了安全),所以又要把自己的身份变成用户态再进入用户态执行handler方法。

在这里插入图片描述
当我们执行完handler后能不能直接返回代码区继续执行呢?
答案是不能,因为上下文信息都还在操作系统里。所以要先回到内核,经过特殊的系统调用回到代码区继续执行代码。

在这里插入图片描述
我们可以把线路简化一下方便观察:
在这里插入图片描述

分析:

在这里插入图片描述
这里的绿色部位交点代表身份的切换,而箭头的指向:
向下表示从用户态切换到内核态
向上表示从内核态切换到用户态

三、信号操作

经过上面的的学习我们知道了内核中有block和pending位图,为了方便我们操作,操作系统定义了一个类型sigset_t。

#include int sigemptyset(sigset_t *set);// 清0int sigfillset(sigset_t *set);int sigaddset (sigset_t *set, int signo);// 比特位由0变为1int sigdelset(sigset_t *set, int signo);// 比特位由1变为0int sigismember(const sigset_t *set, int signo);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
  • 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
  • sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

3.1 sigset_t信号集

我们能看到阻塞和未决都是用一个比特位进行标记(非0即1),所以在用户层采用相同的类型sigset_t进行描述。这个类型表示每个信号有效无效的状态:在阻塞信号集就表示是否处于阻塞;在未决信号集就表示是否处于未决。
阻塞信号集有一个专业的名词叫做信号屏蔽字

3.2 信号集操作函数

sigset_t对每个信号用一个比特位表示有效或者无效的状态。它的底层操作对于我们用户层来说不必要知道,我们只能调用下面的接口函数来操作sigset_ t变量。

3.2.1 更改block表sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

#include int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);RETURN VALUEsigprocmask() returns 0 on success and -1 on error.In the event of an error, errno is set to indicate the cause.

参数介绍:

how:怎么修改。
在这里插入图片描述
set:主要是用来跟how一起使用,用来重置信号。
oldset:输出型参数,把老的信号屏蔽字保存,方便恢复

3.2.2 获取pending信号集sigpending

#include int sigpending(sigset_t *set);RETURN VALUEsigpending() returns 0 on success and -1 on error.In the event of an error, errno is set to indicate the cause.

读取当前进程的未决信号集,通过set参数传出。 set是输出型参数。

3.3 验证

首先要知道默认情况所有信号都不会被阻塞。获取pending表对应的比特位变成1。
而如果被阻塞了,信号永远不会被递达,获取pending表对应的比特位永远为1。

static void show_pending(const sigset_t &Pending){    // 信号只有1 ~ 31    for(int signo = 31; signo >= 1; signo--)    {        if(sigismember(&Pending, signo))        {            std::cout << "1";        }        else std::cout << "0";    }    std::cout << std::endl;}int main(){    sigset_t Block, oBlock, Pending;    // 初始化全0    sigemptyset(&Block);    sigemptyset(&oBlock);    sigemptyset(&Pending);    // 在Block集添加阻塞信号    sigaddset(&Block, 2);    // 修改block表    sigprocmask(SIG_SETMASK, &Block, &oBlock);    // 打印    while(true)    {        // 获取pending        sigpending(&Pending);        show_pending(Pending);        sleep(1);    }    return 0;}

在这里插入图片描述
前面我们使用signal函数捕捉信号不能自定义捕捉9号信号,这里也是一样不能屏蔽9号信号。

当然我们也可以解除阻塞,让信号递达,信号一旦递达,pending就会先由1置0,然后就会处理信号,进程退出

static void show_pending(const sigset_t &Pending){    // 信号只有1 ~ 31    for(int signo = 31; signo >= 1; signo--)    {        if(sigismember(&Pending, signo))        {            std::cout << "1";        }        else std::cout << "0";    }    std::cout << std::endl;}int main(){    sigset_t Block, oBlock, Pending;    // 初始化全0    sigemptyset(&Block);    sigemptyset(&oBlock);    sigemptyset(&Pending);    // 在Block集添加阻塞信号    sigaddset(&Block, 2);    // 修改block表    sigprocmask(SIG_SETMASK, &Block, &oBlock);    // 打印    int cnt = 8;    while(true)    {        // 获取pending        sigpending(&Pending);        show_pending(Pending);        sleep(1);        if(--cnt == 0)        {            // 恢复            sigprocmask(SIG_SETMASK, &oBlock, &Block);            std::cout << "恢复对信号的屏蔽" << std::endl;        }    }    return 0;}

在这里插入图片描述
而为什么没有打印后面那句话呢?

因为进程在内核态直接退出来,就不会返回到用户态执行代码。

四、捕捉信号操作

4.1 内核捕捉信号sigaction

在上一章【linux】进程信号——信号的产生中我们学习了捕捉信号自定义函数signal

sighandler_t signal(int signum, sighandler_t handler);

sigaction使用起来要比signal使用起来复杂。

#include int sigaction(int signum, const struct sigaction *act,              struct sigaction *oldact);

参数说明:

signum代表指定的信号。
act是一个跟函数名同名的结构体输入型参数

struct sigaction {               void     (*sa_handler)(int); //自己写的方法               void     (*sa_sigaction)(int, siginfo_t *, void *);// null               sigset_t   sa_mask;// 信号集               int        sa_flags;// 设置0               void     (*sa_restorer)(void);// null           };

oldact输出型参数,保存过去的数据,方便恢复。

话不多说,直接上代码:

#include #include #include void handler(int signo){    std::cout << "catch signo: " << signo << std::endl;}int main(){    struct sigaction act, oact;    // 初始化    act.sa_handler = handler;    act.sa_flags = 0;    sigemptyset(&act.sa_mask);    sigaction(SIGINT, &act, &oact);    while(1) sleep(1);    return 0;}

在这里插入图片描述
我们可以看到它可以实现跟signal函数一样的功能。
那么它跟signal有什么区别呢?

我们想象这样一个场景:

假设我们在handler设置等待15秒的倒计时函数,先发送一个SIGINT信号,在自定义处理等待15s的期间再次发送一个SIGINT信号,那么会不会递归似的调用handler呢?

运行中:
在这里插入图片描述
结束:
在这里插入图片描述
现象:

我们发了许多的二号信号,但是只处理了两个
当我们处理第一个信号的时候,后边的信号不会再次被提交,当处理完后,后续信号就会递达,但是一共就两个信号递达了,后续信号全部丢失了。

结论:

当我们正在处理一个递达的信号时,同类信号无法被递达,因为当前信号正在被捕捉时,系统会自动把该信号设置进信号屏蔽字中(block)
当信号完成捕捉动作系统又会自动解除对该信号的屏蔽。

所以为什么我们发送了一堆的二号信号,处理完第一次后会处理第二次?

当一个信号被递达时,pending位图的位置就由1置为0,后边再次发送多个,又由0置为1(只有一个比特位所以只收到一个),当一个信号被解除屏蔽的时候,OS会去检查pending位图,如果被置1,就再次递达。

4.1.1 act.sa_mask参数

上面我们是捕获了2号信号,如果我们想在处理某种信号的时候顺便屏蔽其他信号,就可以添加进sa_mask信号集中。
在这里插入图片描述
可以看到处理二号信号的时候3号信号被屏蔽了,那么为什么最后3号信号会起作用呢?

sa_mask :在执行捕捉函数时,设置阻塞其它信号,sa mask进程阻塞信号集,退出捕捉函数后,还原回原有的阻塞信号集

五、可重入函数

假设一种场景:一个信号的处理方法是给一个链表进行头插,现在我们在main函数调用头插,而在头插的过程触发了信号的捕捉动作,又要进行头插,这样就会导致失去了头节点的位置。
在这里插入图片描述
因为两个执行流重复进入insert函数导致出现错误,我们把insert函数叫做不可重入函数。
没出问题就叫做可重入函数

可不可重入是个特性(中义词),我们用的大部分接口都是不可重入的。

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

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

六、volatile关键字

#include #include #include #include  int quit = 0;void handler(int signo){    printf(" %d signo is being caught\n",signo);    printf("quit:%d\n",quit);    quit = 1;    printf("->%d\n",quit);}int main(){    signal(2,handler);    while(!quit);    printf("i am quit\n");    return 0;}

在这里插入图片描述

这样退出是正常情况。但是如果我们让编译器进行优化:

在这里插入图片描述
在这里插入图片描述
可以看到quit确实被改为1了,但是却没有终止循环。
这里是因为编译器把quit数据优化到了寄存器中。

如果不优化,每次判断quit都需要从物理内存获取quit的内容:
在这里插入图片描述
而如果要优化,编译器看到main中while(!quit)并没有被修改,所以直接把quit的值放进寄存器中,不用再从物理内存中获取。
在这里插入图片描述
而我们后边修改quit改的是内存中的quit,并不会印象到寄存器,所以不会退出循环。这是因为寄存器的存在遮盖了物理内存的quit的值。

而加上volatile关键字就可以避免这种情况。
在这里插入图片描述
在这里插入图片描述
volatile的作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作 。



来源地址:https://blog.csdn.net/qq_66314292/article/details/129364847

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

【linux】进程信号——信号的保存和处理

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

【linux】进程信号——信号的保存和处理

文章目录 一、阻塞信号1.1 信号的相关概念1.2 在内核中的构成 二、捕捉信号概念2.1 内核态和用户态2.2 信号捕捉流程图 三、信号操作3.1 sigset_t信号集3.2 信号集操作函数3.2.1 更改block表s
2023-08-20

Linux之信号的保存方式

Linux保存信号信息通过进程信号掩码(sigmask)和信号堆栈。sigmask标记进程对信号的屏蔽状态,而信号堆栈在进程收到信号时保存其寄存器上下文。当信号触发时,内核检查sigmask,未被屏蔽的信号将推送到信号堆栈,启动信号处理程序。信号处理程序处理信号后,内核恢复进程状态并清理信号堆栈。自定义信号处理程序允许进程自定义信号行为,提供控制、异常处理和异步执行能力。
Linux之信号的保存方式
2024-04-02

【Linux】进程信号(上)

文章目录 1.信号概念理解信号产生信号保存信号处理 2. 信号的产生证明输入 CTRL C 就是向前台进程发送信号signal 函数内部参数传递的理解对于信号自定义动作的捕捉问题 3.信号产生的方式1.从键盘输入2.使用
2023-08-20

linux信号解释(3)--信号处理机制

如果需要进程捕获某个信号,并作出相应的处理,就需要注册信号处理函数(其实就是内核里需要识别信号函数,类似C语言里的include某函数库)。    处理信号就类似软中断,内核为每个进程准备了一段信号向量表,记录信号的处理机制。当某个信号发生
2023-01-31

PHP进程信号如何处理

这篇文章主要介绍“PHP进程信号如何处理”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“PHP进程信号如何处理”文章能帮助大家解决问题。一、在Linux操作系统中有哪些信号1、简单介绍信号信号是事件发
2023-07-05

golang进程信号怎么处理

在Go语言中,可以使用`os/signal`包来处理进程信号。下面是一个处理 `SIGINT` 和 `SIGTERM` 信号的示例代码:package mainimport ("fmt""os""os/signal""syscall")
2023-10-26

Linux exit函数与进程退出时信号的处理

在Linux中,exit()函数和信号处理是两个不同的概念,但它们都与进程的退出有关exit()函数:exit()函数是一个C库函数,用于正常终止进程。当一个程序调用exit()函数时,它会执行以下操作:关闭所有打开的文件描述符释放进程
Linux exit函数与进程退出时信号的处理
2024-08-30

Linux协程与信号处理的兼容性

在Linux系统中,协程与信号处理有一定的兼容性问题。由于协程是一种用户态的轻量级线程,它们通常是由用户程序来实现,而信号处理是由操作系统来处理的。在使用协程时,需要注意以下几点与信号处理的兼容性问题:阻塞信号:在协程中,如果需要阻塞某些信
Linux协程与信号处理的兼容性
2024-08-06

python中如何对信号进行处理

这篇文章主要介绍了python中如何对信号进行处理,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。什么是信号信号(signal)-- 进程间通讯的一种方式,也可作为一种软件中断
2023-06-20

Android Init进程对信号的处理流程详细介绍

Android Init进程对信号的处理流程 在Android中,当一个进程退出(exit())时,会向它的父进程发送一个SIGCHLD信号。父进程收到该信号后,会释放分配给该子进程的系统资源;并且父进程需要调用wait()或waitpi
2022-06-06

Ruby信号处理的方法

这篇文章主要介绍“Ruby信号处理的方法”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Ruby信号处理的方法”文章能帮助大家解决问题。Ruby使用Process.kill发送信号Process.ki
2023-06-30

Linux环境下exit函数与进程退出时信号处理的顺序

在Linux环境下,当一个进程调用exit函数或者接收到退出信号时,会触发一系列操作来结束进程调用exit函数或者接收到退出信号。如果进程注册了信号处理函数(signal handler),那么首先会执行相应的信号处理函数。信号处理函数可
Linux环境下exit函数与进程退出时信号处理的顺序
2024-08-30

python 对信号 处理的 测试

python 对信号 处理的测试小结下:每次信号 会将当前执行的函数挂起,进入 信号处理函数如果信号处理函数还在处理,又来信号,当前函数仍然被挂起执行完毕回到刚才挂起点继续执行从下面输出 我们就可以看出来 ^Cget an signa
2023-01-31

编程热搜

目录