Linux初阶——信号

一、预备

1、信号的处理方式

1.1. 默认动作

当收到一个信号时,就执行这个信号的默认动作。

1.2. 忽略

当收到一个信号时,就忽略执行这个信号的默认动作。

1.3. 自定义动作

当收到一个信号时,就执行信号的自定义动作。

2、硬件中断

你有没有想过一个问题:电脑是怎么把键盘上敲的字读进来的呢?

我们知道,我们的电脑是当键盘有数据的时候才会读的,而当键盘没数据时,电脑是不会读的。所以,我们首先需要解决的问题是:电脑怎么知道键盘有数据呢?就像上图一样,其实键盘这种硬件和 CPU 虽然没有直接的导线相连,但会通过其他组件与 CPU 的某个针脚间接通过导线相连。而当键盘有数据时,键盘会通过导线向 CPU 发送高电平,然后 CPU 会用内部的电容来储存这个电平,等到空闲时才会再处理这个信号。然后当 CPU 处理这个信号时,因为这个信号是 5 号针脚传来的,因此 CPU 会去内存的中断向量表调取中断向量表的 5 号方法,即读键盘的方法。然后会把键盘的内容读到键盘的文件缓冲区,最后操作系统会把文件缓冲区的内容刷到内核缓冲区,再到用户缓冲区。

3、常用信号

红框框住的才是常用的信号

4、相关函数

4.1. signal 函数

参数介绍

  • 返回类型:void
  • sig:信号编号。
  • func: 一个返回类型是 void ,参数是 int 类型的函数指针。其中 int 类型的参数是用来传信号编号的。

这个函数就是负责给某个信号定义一个自定义动作。当该进程捕捉到 sig 信号时,进程就会执行 signal 函数里的自定义动作。 

4.2. kill 函数

参数介绍

  • 返回类型:成功发送返回 0,失败返回 -1.
  • pid:进程的 pid。
  • sig:给某个进程发送 sig 号信号。 

4.3. raise 函数

 参数介绍

  • sig:信号编号。

这个函数就是给调用该函数的进程自己发送 sig 号信号。 

4.4. abort 函数 

这个函数就是给调用这个函数的进程自己发送 abort 信号。但值得注意的是,就算我们给 abort 设置了 signal 函数,当进程执行到 abort 函数时,不仅会执行 abort 的自定义动作,还会 abort 掉自己。

5、前台进程 & 后台进程

我们的键盘输入只能输给前台进程,不会输给后台进程。而且在操作系统中只能有一个前台进程,但可以有多个后台进程。

二、信号的产生 

1、键盘组合键(如 control + C / control \)

2、kill 命令(如 kill -signo <pid>)

3、系统函数(kill, raise, abort...)

4、异常

4.1. 硬件产生的异常

什么是硬件产生的异常呢?这里我分别用除零错误和空指针解引用两个例子来说明。

4.1.1. 除零错误

CPU 通过 PC 指针得知正在执行的语句,而当 CPU 执行到上图的语句时,根据冯诺依曼结构, CPU 内部是由计算器和控制器组成的,因此 CPU 在执行此语句时会发现这是除零错误;然后 CPU 会把内部的状态寄存器的某个比特位(用来表示是否除零错误的那一位)从 0 置 1;然后操作系统发现 CPU 的状态寄存器的这个比特位变成 1 后,就会向进程发送除零错误的信号;然后进程就会调用相对应的处理这个信号的方法。


但是,为什么会在这种情况下陷入死循环呢?其实原理很简单。因为 CPU 运算时遇到了除零错误, 而 CPU 认为一旦运算出错,那后面的代码跑了也没意义,因此 CPU 在向操作系统发送信号后,就不会继续向下继续执行代码了。然而,由于我们用 signal 函数重置了对除零错误的处理方法,而这个方法是不会让进程退出的;但是 CPU 因为除零错误,停在了第 17 行,不会执行到第 18 行(return 0)让进程退出;因此进程就一直运行,CPU 的状态寄存器就一直向 OS 报除零错误,然后 OS 就一直向进程发信号,进程就一直调 signal 方法,所以就死循环了。

4.1.2. 空指针解引用

当 CPU 执行到野指针解引用的语句时,由于它是野指针,所以必定会在页表映射时地址转换失败; 当页表地址转换失败时,位于 CPU 内部的 MMU 就会检测到错误,同时 CPU 内部会有一个寄存器专门存报错的虚地址;然后 CPU 就会发信号给操作系统;然后操作系统就会发信号给进程;最后进程就会调用对应的信号处理方法。

4.1.3. OS 如何向进程发送信号
struct task_struct
{int signal; // 0000 0000 0000 0000 0000 0000 0000 0010 此时 OS 向进程发送了 1 号信号
};

其实就是 OS 会向进程的 PCB 里的 signal 位图的某一位置 1,表示某号信号。举个例子,当 OS 向进程 PCB 的 signal 的 1 号(最低位是 0 号位)置 1 时,就表示向该进程发送了 1 号信号,然后进程就会执行处理方法了。因此,OS 向进程发送信号的本质其实就是 OS 向进程 PCB 的 signal 位图写东西。所以,我们可以发现,OS 向进程发送信号的过程是不是和硬件中断很像呢?

4.1.4. 进程如何给 OS 发送信号

在这之前,我们得先介绍一下 CPU 内部的状态寄存器。状态寄存器其实是一个有若干位的寄存器,且位于 CPU 内部。而且状态寄存器里的每一位都是有不同的含义的,比如不同位就代表由不同原因产生的报错。

我们再回来谈谈操作系统。操作系统会查看 CPU 的状态寄存器,当看到寄存器的某一位为 1 时,就知道 CPU 报的是什么错了,就相当于 CPU 把报错信号发给操作系统了。

4.2. 软件产生的异常

举个例子,管道关闭读端,写端还在写的话,直接报错。

5、软件产生的信号(以 alarm 为例)

5.1. 关于 alarm 函数

这个函数就是输入一个时间,然后当时间耗尽之后会向进程发送 14 号信号(SIGALRM)。而返回上一个 alarm 调用中剩余的秒数。如果没有活动的定时器,则返回 0。

#include <signal.h>
#include <iostream>
#include <unistd.h>using namespace std;void handler(int signo)
{cout << "...get a sig, number: " << signo <<endl; //我什么都没干,我只是打印了消息int n = alarm(5);cout << "剩余时间:" << n << endl;
}int main()
{signal(SIGALRM, handler);alarm(50);while (true){cout << "proc is running... pid:" << getpid() << endl;sleep(1);}return 0;
}

5.2. 软件如何产生信号 

三、信号的保存

1、递达(handler 表)

信号的递达其实就是进程在收到信号后,对信号的处理方法。

2、未决(未处理的信号)(pending 表)

信号的未决就是进程在收到信号后,如果该信号并没有被处理,那么 pending 位图的对应位就会置为 1,如果该信号已经被处理了,那 pending 表的对应位就置为 0 了。

3、屏蔽(屏蔽信号)(block 表)

信号的屏蔽就是如果 block 位图里的 2 号位为 1,那么就代表 2 号信号被屏蔽了;即使之后收到了 2 号信号,pending 位图的 2 号位被置为 1,进程也不会调用 2 号信号的处理方法。

4、相关系统函数

4.0. 关于 sigset_t 类型

typedef struct {unsigned long __val[_NSIG_WORDS];
} sigset_t;

4.1. sigemptyset

这个函数传的是输出型参数,因此这个函数的作用就是把信号集清空。 

4.2. sigfillset

和上个函数一样,传的也是输出型参数,因此这个函数的作用就是直接把信号集置满,即每一个比特位都为 1. 

4.3. sigaddset

这个函数的作用就是把 signo 号信号加入到 *set 信号集中。 

4.4. sigdelset

这个函数的作用就是从 *set 信号集中删掉 signo 号信号。  

4.5. sigismember

因为这个函数的 set 是输入型参数,因此这个函数的作用就是检查 signo 号信号是不是在该信号集当中。 

4.6. sigprocmask

参数介绍

  • restrict_set:输入型参数,用 restrict_set 修改进程中已有的 block 信号集。
  • restrict_oset:输出型参数,带出原来的 block 信号集。
  • how: 如何用 restrict_set 修改进程中已有的 block 信号集。

how 的宏

  • SIG_BLOCK:屏蔽信号,将新信号添加到当前的 block 信号集中,原来的屏蔽信号保持屏蔽。
  • SIG_SETMASK:直接把 restrict_set 的信号集赋给当前的 block 信号集。
  • SIG_UNBLOCK:对进程的 block 信号集删掉其中的 restrict_set 信号集里的信号。

4.7. sigpending

这个函数的 set 参数是输出型参数,因此该函数的功能是把进程的 pending 位图带出来。如果成功返回 0,失败返回 -1. 

四、信号的处理

1、用户态 & 内核态

简单来说,如果进程要访问库函数或者是我们自己写的函数,那么进程就是处于用户态;而如果进程要访问系统函数,或者是系统的数据结构(比如前面的 block 和 pending 位图,或进程的 task_struct 等),那么进程就是处于内核态的。

但如果想逻辑自洽一点的话,那肯定是没有那么简单的。而要谈用户态和内核态,那么我们就又要谈到进程地址空间了。

1.1. 进程地址空间

我们知道,进程地址空间里的虚拟内存是这样划分的:

但是内核空间里的地址并不会通过普通的页表实现虚地址和物理地址的映射,而是通过内核页表来实现虚地址和物理地址之间的映射的;而且这个内核页表也非常特别,每个操作系统只有 1 个内核页表。换句话说,如果有 500 个进程,那么普通页表就有 500 张,但内核页表就只有 1 张。

1.2. 内核态 & 用户态

从进程的角度来说,当进程要调系统函数时,执行流就会从进程地址空间的代码区跳到内核空间中去调系统函数。然而,操作系统为了防止用户恶意修改其内核数据,于是便给进程设置了权限,当进程符合进入内核的权限时,才能访问内核的数据和代码;如果没有进入内核的权限,那么就无法访问内核的数据和代码。而这个权限就是 ecs 寄存器。当进程要进入内核时,ecs 寄存器的低 2 位为 00,表示进程可以访问内核的代码和数据,但无法访问用户的代码和数据;而当进程想访问用户的代码和数据时,ecs 寄存器的低 2 位为 11,表示进程可以访问用户的代码和数据,但无法访问内核的代码和数据。

用户态与内核态之间的转换

因此,到底什么叫用户态,什么叫内核态呢?当 ecs 寄存器的低 2 位为 00 时,进程就处于内核态,当 ecs 寄存器的低 2 位为 11 时,进程就处于用户态。

2、信号何时被处理

进程从内核态变成用户态的时候,就会做信号的检测和处理。意思就是说,当进程由内核态变成用户态时,就会访问这 3 张表(block、pending、handler),寻找在同一行下,block 和 pending 的值分别为 0,1 的下标(以下图为例的话,下标就是 4,即 4 号信号),然后通过函数数组 handler 调用这个信号的处理方法。

3、信号产生到处理过程(抽象版) 

  • A:在用户态执行用户代码。
  • B:在内核态执行系统函数(如 fork 等)和向进程发信号(如果有信号的话)。
  • C:执行信号检测与处理。如果 handler 表对该信号的处理方法是默认的,那么就调默认的方法(比如退出,就不会去到 D 状态了);而如果处理方法是忽略,那么就直接回到 A 状态,并继续执行下一句代码。
  • D:如果 handler 表对该信号的处理方法被自定义了,即用户用 signal 重置了信号处理方法,就去用户态调自定义的信号处理方法
  • E:调用系统函数 sigreturn,通过 sigreturn 获取原来调系统函数的地方。比如在第 17 行调了 pipe 函数,那么就可以通过 sigreturn 函数让 pc 指针指回第 17 行代码;然后再接着执行第 18 行,第 19 行……

五、信号的捕捉

1、sigaction 函数

1.1. struct sigaction 结构体

因为除了红框框住的,其他都是和实时信号有关的,因此只关注红框的参数就行了。

参数介绍

  • sa_mask:可以理解成这是一个信号集,而里面存的是需要屏蔽的信号。
  • __sa_handler:和 signal 函数的 handler 一样,就是一个处理方法。

 1.2. sigaction 函数

参数介绍

  • sig:要捕捉的 sig 号信号。
  • act:捕捉到 sig 号信号后,对 sig 号信号的处理方法。
  • oact: 该信号原来的处理方法。
  • 返回:如果成功返回 0,失败返回 -1.

1.3. 一些细节 

当 sigaction 或 signal 函数捕捉到信号时,操作系统会自动把这个信号屏蔽掉,等到  sigaction 或 signal 函数运行完后(当 sigaction 或 signal 函数运行完后,就表示已经把 sigreturn 函数也运行完了),才会取消对该信号的屏蔽。否则如果在调用 signal 或 sigaction 函数时又收到了信号,那么进程的状态就会这样变化:E -> C -> D(参考前面的“信号产生到处理”的图),就构成死循环了。

2、可重入函数

什么是重入函数呢?我们以头插链表为例子。假设 insert 函数每调一次就会头插一个节点。

通过上图,我们可以看出,当进程刚执行完 1,但还没执行 4 时,此时进程收到了信号,就会去执行处理方法,然后在处理方法里再做一次头插链表,然后再返回到第一次的 insert 函数。 

 

通过上图,我们可以看出,node2 并没有被指针维护,因此是无法被释放的,就会造成内存泄漏错误。而像这种被重复进入后就会出错的函数,就是不可重入函数;而即使重复进入后也不会出错的函数就叫可重入函数。

3、volatile 关键字

3.1. g++ 优化级别

g++ 有很多优化选项。其中 -O0 是没有优化的,而 -O1,-O2 是优化程度逐步变大。但这都不重要,重要的是我们可以看看下面的代码:

int flag = 0;void handler(int signo)
{cout << "catch a signal: " << signo << endl;flag = 1;
}int main()
{signal(2, handler);while(!flag); // 因为后续的代码都不会改变 flag 的值,因此 g++ 会优化 flag 变量,// 把 flag 的值拷到寄存器里,以后要访问 flag 的值的话可以直接访问// 寄存器,这样可以减少访问时间cout << "process quit normal" << endl;return 0;
}

正常来说 flag 是在栈上的,但是因为编译器对 flag 的优化,在程序运行时 CPU 如果要访问 flag 的值,就直接访问存 flag 的值的寄存器,而不会访问栈上的 flag。因此,如果我们对进程发 control + c 信号,由于 CPU 只访问寄存器上的 flag ,而不访问栈上的 flag,因此只是栈上的 flag 被改成了 1,而寄存器上的 flag 还是 0;所以除了打印 “catch a signal…” 以外,程序还是会死循环的。 

上面代码的运行结果:control + c 失效

3.2. 再谈 volatile

而 volatile 就是用来告诉编译器:不要把 flag 优化掉,就是说不要把 flag 的值拷到寄存器里,以至于不要让 CPU 访问寄存器里的 flag;取而代之的是,CPU 访问栈上的 flag。

加了 volatile 的运行结果

4、SIGCHLD 信号

4.1. 介绍 SIGCHLD 信号

当子进程结束时,会向父进程发送 SIGCHLD (17) 信号。因此其实我们可以利用这个信号来回收子进程。

4.2. 基于信号捕捉的子进程回收

void handler(int signo)
{sleep(5);pid_t rid;while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0)cout << "I am process: " << getpid() << " catch a signo: " << signo<< "child process quit: " << rid << endl;
}int main()
{signal(SIGCHLD, handler);for (int i = 0; i < 10; i++){pid_t id = fork();if (id == 0){while (true){cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;sleep(5);break;}cout << "child quit!!!" << endl;exit(0);}sleep(1);}// fatherwhile (true){cout << "I am father process: " << getpid() << endl;sleep(1);}return 0;
}

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

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

相关文章

跨设备使用的便签软件哪款好?

在快节奏的现代生活中&#xff0c;便签软件已成为我们不可或缺的数字助手&#xff0c;它们帮助我们记录灵感、安排日程、设置提醒&#xff0c;极大地提升了我们的工作与生活效率。然而&#xff0c;面对市场上琳琅满目的便签应用&#xff0c;选择一款既实用又适合手机使用的便签…

【万兴科技-注册_登录安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…

51单片机应用开发(进阶)---外部中断(按键+数码管显示0-F)

实现目标 1、巩固数码管、外部中断知识 2、具体实现&#xff1a;按键K4&#xff08;INT1&#xff09;每按一次&#xff0c;数码管从0依次递增显示至F&#xff0c;再按则循环显示。 一、共阳数码管 1.1 共阳数码管结构 1.2 共阳数码管码表 共阳不带小数点0-F段码为&#xff…

《链表篇》---两数相加(中等)

题目传送门 方法一&#xff1a;迭代 文字描述看代母注释 class Solution {public ListNode addTwoNumbers(ListNode l1, ListNode l2) {//定义头结点和当前节点ListNode head null,cur null;//carry记录进位情况。int carry 0; while(l1 ! null || l2 ! null){//判断节点是…

Date工具类详细汇总-Date日期相关方法

# 1024程序员节 | 征文 # 目录 简介 Date工具类单元测试 Date工具类 简介 本文章是个人总结实际工作中常用到的Date工具类&#xff0c;主要包含Java-jdk8以下版本的Date相关使用方法&#xff0c;可以方便的在工作中灵活的应用&#xff0c;在个人工作期间频繁使用这些时间的格…

uniapp iOS打包证书过期——重新下载证书及更新文件

证书过期&#xff0c;重新生成步骤 关于所需的证书、标识符、描述文件等请查看iOS打包证书申请流程证书有效期为1年 过期提示 生成证书 点击 钥匙串访问 → 证书助理 → 从证书颁发机构请求证书 &#xff1b; 我这里选择 存储到磁盘 &#xff0c;邮件、名称 可自定义&#…

uniapp圆形波浪进度效果

uniapp圆形波浪进度效果 背景实现思路代码实现尾巴 背景 最近项目中有些统计的地方需要用到圆形的波浪进度效果&#xff0c;要求是根据百分比值然后在一个圆形内动态的展示一个波浪形的进度&#xff0c;看参考一下效果。 实现思路 这个效果看着挺复杂的&#xff0c;那么我们…

宠物空气净化器有用吗?有哪几款吸毛效果好且低噪的推荐

伴随着天气越来越凉&#xff0c;照常来说&#xff0c;猫咪掉毛的频率应该会变少&#xff0c;但是为什么我家的猫咪还在掉很多毛。 现在就连南方地区都要加外套了&#xff0c;但是猫咪掉毛太多&#xff0c;都不敢穿纯棉面料的衣服&#xff0c;还有本来想着顺应天气的变化&#…

深入理解C++ Lambda表达式:语法、用法与原理及其包装器的使用

深入理解C Lambda表达式&#xff1a;语法、用法与原理及其包装器的使用 lambda表达式C98中的一个例子lambda表达式语法lambda表达式各部分说明捕获列表说明 函数对象与lambda表达式 包装器function包装器 bind &#x1f30f;个人博客主页&#xff1a; 个人主页 本文深入介绍了…

2024最新Instagram养号攻略!海外社媒起号码住了

Instagram至今仍然是全球顶级的流量平合&#xff0c;不仅在国外是各大网红明星必备app&#xff0c;国内下载量也居高不下&#xff0c;但从2018年下半年开始加大了对新账号的监控和权限限制。新注册的账号会受到诸多限制&#xff0c;稍不慎就会进入安全模式或者被封&#xff0c;…

【vue】10.组件的生命周期-从Vue 2到Vue 3的演变

自Vue 2发布以来&#xff0c;它以其简单易用、灵活高效的特点赢得了众多开发者的喜爱。随着时间的推移&#xff0c;Vue 3在Vue 2的基础上进行了全面升级&#xff0c;其中包括对组件生命周期的调整。本文将首先回顾Vue 2的组件生命周期&#xff0c;然后重点介绍Vue 3组件生命周期…

STM32 HAL 点灯

首先从点灯开始 完整函数如下&#xff1a; #include "led.h" #include "sys.h"//包含了stm32f1xx.h&#xff08;包含各种寄存器定义、中断向量定义、常量定义等&#xff09;//初始化GPIO口 void led_init(void) {GPIO_InitTypeDef gpio_initstruct;//打开…

计算合约方法的签名

计算合约方法的签名 通过智能合约实现 // SPDX-License-Identifier: MIT pragma solidity ^0.8.26;contract FunctionSelector {/*"transfer(address,uint256)"0xa9059cbb"transferFrom(address,address,uint256)"0x23b872dd*/function getSelector(stri…

【Java】方法的使用 —— 语法要求、方法的重载和签名、方法递归

目录 1. 方法基础知识 1.1 方法的概念 1.2 语法格式 * 注意事项【与C不同】 1.3 return —— 返回值的严格检查【比C语言严格】 2. 形参与实参的关系 3. 方法重载 3.1 什么是方法重载&#xff1f;为什么要方法重载&#xff1f; 3.2 方法重载的规则 4. 方法签名 5. 递…

鸿蒙开发:arkts Refresh 组件

基本概念 在 ArkTS 5.0 中&#xff0c;Refresh组件是用于实现下拉刷新功能的重要元素。它为用户提供了一种直观的方式来获取最新的数据。当用户在界面上对Refresh组件所在的区域进行下拉操作时&#xff0c;如果满足一定的触发条件&#xff0c;就会触发刷新事件&#xff0c;从而…

ClickHouse 神助攻:纽约城市公共交通管理(MTA)数据应用挑战赛

本文字数&#xff1a;13198&#xff1b;估计阅读时间&#xff1a;33 分钟 作者&#xff1a;The PME Team 本文在公众号【ClickHouseInc】首发 我们一向对开放数据挑战充满热情&#xff0c;所以当发现 MTA&#xff08;城市交通管理局&#xff09;在其官网发起了这样的挑战时&…

HarmonyOS开发5.0 net 启动界面设置

第一步、创建我们界面 第二步&#xff0c; 在EntryAbility中配置启动页面&#xff0c;在entry/src/main/ets/entryability/EntryAbility.ets中配置启动页面 配置如下 至此大功告成

算法笔记day10

目录 1.牛牛冲钻五 2.最长无重复子数组_牛客题霸_牛客网 3.重排字符串 1.牛牛冲钻五 算法思路&#xff1a; 特别简单的模拟题&#xff0c;没什么说的。 #include <iostream> #include <vector> #include <string> using namespace std; int main() { …

ETF申购赎回指南:详解注意事项与低费率券商推荐!

​ETF 申购&赎回 ETF申购赎回是个啥业务&#xff1f; 01 ETF申购、赎回是一种交易委托方式&#xff0c;指投资者通过申购方式(买入方向)获得ETF份额&#xff0c;通过赎回的方式&#xff08;卖出方向&#xff09;换掉/卖出ETF份额。ETF申购&#xff0c;通常是通过一篮子成…

LinkedList和链表之刷题课(下)

1. 给定x根据x把链表分割,大的结点放在x后面,小的结点放在x前面 题目解析: 注意此时的pHead就是head(头节点的意思) 基本上就是给定一个链表,我们根据x的值来把这个链表分成俩部分,大的那部分放在x后面,小的那部分放在x前面,并且我们不能改变链表本来的顺序,比如下面的链表,我…