
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》
《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬 艾莉丝的简介:

文章目录
- [3 ~> 信号的保存](#3 ~> 信号的保存)
-
- [3.1 为什么要进行信号保存](#3.1 为什么要进行信号保存)
- [3.2 信号其它常见概念](#3.2 信号其它常见概念)
-
- [3.2.1 概念](#3.2.1 概念)
- [3.2.2 结合示例理解](#3.2.2 结合示例理解)
- [3.3 内核结构中的表示](#3.3 内核结构中的表示)
-
- [3.3.1 进程PCB内部存在三张表:信号如何被递达、未决、阻塞](#3.3.1 进程PCB内部存在三张表:信号如何被递达、未决、阻塞)
- [3.3.2 结合内核源码,怎么证明有这三张表?](#3.3.2 结合内核源码,怎么证明有这三张表?)
- [3.3.3 什么叫阻塞信号?](#3.3.3 什么叫阻塞信号?)
-
- [3.3.3.1 以2号信号为例子](#3.3.3.1 以2号信号为例子)
- [3.4 sigset_t](#3.4 sigset_t)
- [3.5 信号集操作函数](#3.5 信号集操作函数)
-
- [3.5.1 sigprocmask](#3.5.1 sigprocmask)
- [3.5.2 sigpending](#3.5.2 sigpending)
- [3.5.3 做实验的准备工作](#3.5.3 做实验的准备工作)
-
- [3.5.3.1 定义并初始化信号集](#3.5.3.1 定义并初始化信号集)
- [3.5.3.2 向block信号集添加信号](#3.5.3.2 向block信号集添加信号)
- [3.5.3.3 屏蔽2号信号(以2号为例)](#3.5.3.3 屏蔽2号信号(以2号为例))
- [3.5.3.4 不断获取pending信号集并且打印](#3.5.3.4 不断获取pending信号集并且打印)
-
- [3.5.3.4.1 获取pending信号集](#3.5.3.4.1 获取pending信号集)
- [3.5.3.4.2 打印](#3.5.3.4.2 打印)
- [3.5.3.4.3 打印函数](#3.5.3.4.3 打印函数)
- [3.5.3.4.4 获取当前进程pid](#3.5.3.4.4 获取当前进程pid)
- [3.5.3.5 简易演示](#3.5.3.5 简易演示)
- [3.5.4 实验(继续前面的实验)](#3.5.4 实验(继续前面的实验))
-
- [3.5.4.1 打印](#3.5.4.1 打印)
- [3.5.4.2 尝试恢复](#3.5.4.2 尝试恢复)
- [3.5.4.3 运行](#3.5.4.3 运行)
- [3.5.4.4 自定义捕捉2号信号](#3.5.4.4 自定义捕捉2号信号)
- [3.5.4.5 测试](#3.5.4.5 测试)
- [3.5.4.6 另一个问题](#3.5.4.6 另一个问题)
- [3.4.5.7 尝试获取信号集并且打印](#3.4.5.7 尝试获取信号集并且打印)
- [3.4.5.8 初不初始化不影响](#3.4.5.8 初不初始化不影响)
- [3.4.5.9 所有信号都屏蔽了会怎么样?](#3.4.5.9 所有信号都屏蔽了会怎么样?)
- [3.4.5.10 结论](#3.4.5.10 结论)
- [3.5.5 信号屏蔽的注意事项](#3.5.5 信号屏蔽的注意事项)
- [3.5.6 信号保存小结及下阶段预告](#3.5.6 信号保存小结及下阶段预告)
- [4 ~> 信号的处理:OS如何运行,系统调用如何实现的](#4 ~> 信号的处理:OS如何运行,系统调用如何实现的)
-
- [4.1 快速地说明什么叫内核态、什么叫做用户态(以及地址空间重谈)](#4.1 快速地说明什么叫内核态、什么叫做用户态(以及地址空间重谈))
- [4.2 为什么要区分这两种状态?](#4.2 为什么要区分这两种状态?)
- [4.3 信号处理的流程](#4.3 信号处理的流程)
-
- [4.3.1 信号捕捉的流程](#4.3.1 信号捕捉的流程)
- [4.3.2 记忆信号捕捉流程](#4.3.2 记忆信号捕捉流程)
- [4.3.3 信号处理的思维导图](#4.3.3 信号处理的思维导图)
- [4.4 重谈内核态和用户态(引入硬件中断,OS如何运行,系统调用)](#4.4 重谈内核态和用户态(引入硬件中断,OS如何运行,系统调用))
-
- [4.4.1 sigaction](#4.4.1 sigaction)
- [4.4.2 穿插话题:OS是如何运行的?](#4.4.2 穿插话题:OS是如何运行的?)
-
- [4.4.2.1 硬件中断](#4.4.2.1 硬件中断)
- [4.4.2.2 时钟中断](#4.4.2.2 时钟中断)
-
- [4.4.2.2.1 时钟中断机制](#4.4.2.2.1 时钟中断机制)
- [4.4.2.2.2 时间戳](#4.4.2.2.2 时间戳)
- [4.4.2.2.3 时间片](#4.4.2.2.3 时间片)
- [4.4.2.2.4 操作系统本质是一个代码和数据的集合](#4.4.2.2.4 操作系统本质是一个代码和数据的集合)
- [4.4.2.2.5 时钟中断思维导图](#4.4.2.2.5 时钟中断思维导图)
- [4.4.2.3 软中断](#4.4.2.3 软中断)
-
- [4.4.2.3.1 软中断分为异常和陷阱](#4.4.2.3.1 软中断分为异常和陷阱)
- [4.4.2.3.2 软中断的概念](#4.4.2.3.2 软中断的概念)
- [4.2.2.3.3 软中断有什么用?系统调用:系统调用表、系统调用号!](#4.2.2.3.3 软中断有什么用?系统调用:系统调用表、系统调用号!)
- [4.4.2.4 中断分类](#4.4.2.4 中断分类)
- [4.4.3 理解内核态和用户态](#4.4.3 理解内核态和用户态)
- 代码演示
- 结尾
3 ~> 信号的保存

3.1 为什么要进行信号保存
为什么要进行信号保存?
信号处理有可能不是立即处理,当前可能在做更重要的事情。
- 信号保存本质是为了
延后处理。

3.2 信号其它常见概念
3.2.1 概念

- 实际执行信号的处理动作称为
信号递达(Delivery) - 信号从产生到递达之间的状态,称为
信号未决(Pending)。 - 进程可以选择
阻塞(Block)某个信号。 - 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

3.2.2 结合示例理解


3.3 内核结构中的表示
能递达,未决,阻塞信号,本质就是进程能识别信号 && 处理信号,这个工作肯定是有人提前做好了!
3.3.1 进程PCB内部存在三张表:信号如何被递达、未决、阻塞
进程PCB内部会存在三张表:

-
pending:位图结构,对应
信号未决; -
handler:函数指针数组,将来执行一个个方法(捕捉、忽略、......方法),可以;理解从下标就是信号编号,数组下标对应的内容就是执行方法,可以索引了------signal方法的本质:通过系统调用,修改handler表,对应
信号递达;
有了pending、handler这两张表,对于信号我们已经具有了保存信号的能力。
- block:和pending的位图结构一模一样,只不过在block位图里面:
1、比特位的位置,表示的是信号编号(同pending)-1(比特位索引 = 信号编号 − 1) ;
2、比特位的内容,表示是否阻塞对应的信号(pending:是否收到)。
这三张表共同赋予了一种能力:
进程是能够识别并且处理信号的!

3.3.2 结合内核源码,怎么证明有这三张表?
怎么证明有这三张表呢?我们看一看内核的源码就明白了!

保存信号的表结构:

阻塞信号(无符号长整型):

处理信号的结构:

信号处理的类型:
- __sighandler_t:用户设置的回调函数

类似于这个:


一共有62个信号,这里写的是64,实际上没有32、33号信号:

思维导图:

3.3.3 什么叫阻塞信号?
- pending写为1,能不能处理?可以,因为并没有被block;
- block变为0,就不能递达,直到block为0,才能够递达。


3.3.3.1 以2号信号为例子
以2号为例子:假设当前所有都是0。
- 我们先block2号信号

本质肯定都是OS给提供系统调用,对应系统调用肯定也会有对应的位图结构参数,就跟之前进程间通信ctr那个方法,我们里面也有个结构参数。
3.4 sigset_t
还是拿出这张图:

分析一下:

我们这个结构叫做信号集,阻塞信号集,pending信号集,handler信号递达。
阻塞信号集也叫做当前进程的信号屏蔽字(屏蔽你不需要递达,不需要处理的信号),有点像权限那里的umask。

3.5 信号集操作函数


用户不直接按位与按位或操作位图而是用这些函数。
3.5.1 sigprocmask

下面是how参数的可选值:

追加、覆盖、去除------

返回值问题:

输入型参数:

oset就是老的位图:

oset是在写之前把老的位图进行了一下保存。
3.5.2 sigpending
sigpending:本质是获取pending位图。

主要用来获取当前调用进程的pending信号集。
- 我们怎么不见对于pending信号集的写入 || 修改操作呢?只有获取操作,为什么?因为我们已经见过了!信号产生的五种方式!
各种各样的软件条件,就是在给pending做写入操作,还不够嘛!

3.5.3 做实验的准备工作
3.5.3.1 定义并初始化信号集
sigset_t就是数据类型,后面的两个就是在用户栈上的两个变量(定义变量而已,没多高级,在用户栈上)!
)
可能是随机值!我们要初始化!做清空:

3.5.3.2 向block信号集添加信号
向block信号集添加信号(还没设置到内核)。
怎么添加信号?
把特定信号的比特位由0改为1。
添加信号集的过程,有没有设置到内核中?有没有设置到当前进程内部?没有!要设置到内核必然要调用系统调用!
3.5.3.3 屏蔽2号信号(以2号为例)

3.5.3.4 不断获取pending信号集并且打印
必须得有一个sigset_t类型:

3.5.3.4.1 获取pending信号集

3.5.3.4.2 打印

3.5.3.4.3 打印函数

3.5.3.4.4 获取当前进程pid

3.5.3.5 简易演示

肉眼看到了2号新号被pending的过程:

2号信号是被block的,不能递达。

所有信号都block了,都不递达,那进程是不是金刚不坏了嘛!
3.5.4 实验(继续前面的实验)
我们要看到信号被递达之后,由1变成0。
3.5.4.1 打印

3.5.4.2 尝试恢复


3.5.4.3 运行
一旦恢复了对2号信号的屏蔽,再运行,就不会被递达应该先由0变成1,再由1变回0,看看结果:

3.5.4.4 自定义捕捉2号信号

不退出就会继续打印继续处理。


3.5.4.5 测试

3.5.4.6 另一个问题
pending信号被递达的时候,是跑sighandler之前 || 之后1 ~> 0:

怎么验证?
也是进程自己执行的,信号获取并且打印的pending信号集,再测试一次什么时候1 -> 0,是之前还是之后?
3.4.5.7 尝试获取信号集并且打印

运行一下:

所以是在递达之前就1 -> 0。
之前和之后处理都行,之前处理只不过信号正在处理2号信号的时候还能够收2号信号------表达这个意思。
3.4.5.8 初不初始化不影响


3.4.5.9 所有信号都屏蔽了会怎么样?
所有信号都添加到阻塞信号集里面了,后续先不考虑恢复:

再试一下:

9号呢?
bash
cnt=1; while [ $cnt -le 31 ]; do kill -${cnt} 1467737;let cnt++; sleep 1; done

9号不会被屏蔽!
9号信号叫做 管理员信号!

19号信号也不会被屏蔽!
18号也可以理解成没有被屏蔽。
- 我们下面会再提一嘴的!

命令行脚本:

每隔一秒发一次:

9号信号既可以不被捕获也可以不被屏蔽,"管理员信号"。

19号进程被暂停了,自动变成后台进程:

18号,continue,继续运行:

系统当中存在很多的特殊信号:

19号不能被block------后台进程不能被读。
18号是能够被block,18号信号发送了continue------特殊情况,继续这个操作还是做了但是可以被block。
3.4.5.10 结论
对所有信号设置屏蔽不是屏蔽所有信号。
9、19都没有被屏蔽,18号可以理解成被屏蔽。
3.5.5 信号屏蔽的注意事项


3.5.6 信号保存小结及下阶段预告
接下来的阶段:

4 ~> 信号的处理:OS如何运行,系统调用如何实现的

信号收到不一定会立即递达,而是在合适的时候。
在什么合适的时候呢?
1、核心工作做完;
2、进程从内核态返回用户态的时候,进行信号的检测和处理。

4.1 快速地说明什么叫内核态、什么叫做用户态(以及地址空间重谈)

- 用户执行自身代码------
用户态; - 访问操作系统内核结构的代码和数据------
内核态。
原理上还是不太理解什么是用户态什么是内核态。
4.2 为什么要区分这两种状态?

4.3 信号处理的流程
下图中,横线之上叫用户态 ,横线之下叫内核态。
4.3.1 信号捕捉的流程
信号捕捉的流程:


4.3.2 记忆信号捕捉流程

这个东西是什么?很像无穷大、极限的概念。
- 交点处:检测点,检查并且处理信号。
横线上四个交点,四次状态切换!(检测通过的话)
信号处理的流程就是倒着的8,加一条横线!
4.3.3 信号处理的思维导图
信号处理的流程如果画成思维导图就是下面这样:

4.4 重谈内核态和用户态(引入硬件中断,OS如何运行,系统调用)
我们的目标是:理解内核态和用户态。
内核态和用户态再谈下去就是,寄存器,有标志位。

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

- 为啥会多一个sa_mask?
- 如果进程收到了大量的重复信号会怎么办?
我们保存对应的普通信号是用的位图,很短一段时间收到连续多个相同信号,对应的OS只会在进程内部位图记录一次。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段。
处理之前pending会变为0,正在处理信号时会把block先设置为1,如果有一个相同的会阻塞住。OS处理同一个信号的时候是串行处理的。相同信号越来越多,可能会导致一直调用handler函数,递归调用,导致栈溢出
代码实验:

4.4.2 穿插话题:OS是如何运行的?
CPU是中断处理的核心!!!
4.4.2.1 硬件中断
键盘向CPU发送的内容就叫做"硬件中断"------凡是向CPU发送的都叫做"硬件中断"。

4.4.2.2 时钟中断

4.4.2.2.1 时钟中断机制

4.4.2.2.2 时间戳

4.4.2.2.3 时间片

4.4.2.2.4 操作系统本质是一个代码和数据的集合

4.4.2.2.5 时钟中断思维导图

4.4.2.3 软中断

4.4.2.3.1 软中断分为异常和陷阱
-
除0算软中断的,mmu(硬件)现在集成到了CPU中,所以野指针也算软中断。
-
凡是有CPU外部的外设触发的中断都叫硬件中断,由CPU内部触发的都叫做软中断。

上述这些通常就叫做 异常。

4.4.2.3.2 软中断的概念

4.2.2.3.3 软中断有什么用?系统调用:系统调用表、系统调用号!

直接上图:

4.4.2.4 中断分类

4.4.3 理解内核态和用户态
理解
在OS层面上进程所对应的0-3GB通常会由用户映射表映射到对应位置,我们3-4GB内核空间也会映射,所以有内核页表和用户页表。不过不同进程内核页表都是一样的,不过实际上其实两个页表是混在一起的。逻辑上我们分开来说。进程之间是具有独立性的。
无论我的进程怎么调度进程PCB怎么切换,我们都可以找到同一个操作系统,变的只是0~3GB的用户区。
Linux中,任何函数跳转都是在进程的地址空间内进行跳转的!

Linux中,任何函数跳转都是在进程的地址空间内进行跳转的。 从逻辑上可以直接跳转到内核区但是实际上是不行的我们设置了一个条件,CPU里面有个CS寄存器,用低两位的比特位来表示当前CPU本身的执行级别,其中Linux只使用这两种执行级别,00(0):表示内核态 11:(3) 表示用户态.所以我们就会判断执行级别,如果是11会禁止,是00系统调用才能被调用。我们当前进程是处于用户态还是内核态是由CPU里面来标识的。
那么现在的问题就是对应的用户能不能直接访问3-4GB呢,是不行的,因为会对执行级别进行处理,我要访问内核就得3变为0,内核态才行,我们使用汇编语言其实也是改不了这个的。要改只能 int80 && syscall,自动由3->0结束再变回3,和这个软中断强制绑定了。执行级别为0才可以访问OS。
系统调用号是必须要设置的,不然会报错的。

CPL为0,内核态就可以直接以系统调用的方式访问OS,CPL如果是3就只能访问0-3GB的用户空间。

结论

理解sigaction

用户态和内存态的内存边界划分细节

思维导图

代码演示
test.cc
cpp
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <signal.h>
void PrintPending(sigset_t &pending)
{
// 0000 0000 . . . 0000 -> 0000 0000 . . . 0010
for(int signum = 31;signum >= 1;signum--)
{
if(sigismember(&pending,signum))
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
}
void handler(int signum)
{
std::cout << "get a signal: " << signum << std::endl;
// 在处理2号信号期间,2号信号被自动block了!
while(true)
{
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
sleep(1);
}
// exit(0);
}
int main()
{
struct sigaction act,old_act;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&(act.sa_mask)); // 如果进程收到了大量的重复信号?
sigaddset(&(act.sa_mask),1);
sigaddset(&(act.sa_mask),3);
sigaddset(&(act.sa_mask),4);
sigaddset(&(act.sa_mask),5);
// // 高可读性版本
// sigaddset(&(act.sa_mask), SIGHUP); // 1
// sigaddset(&(act.sa_mask), SIGQUIT); // 3
// sigaddset(&(act.sa_mask), SIGILL); // 4
// sigaddset(&(act.sa_mask), SIGTRAP); // 5
sigaction(2,&act,&old_act); // 使用数字 2
// sigaction(SIGINT, &act, &old_act); // 使用常量
while(true)
{
pause();
}
}
// // 自定义捕捉,屏蔽(block)2号信号
// void PrintPending(sigset_t &pending)
// {
// // 0000 0000 . . . 0000 -> 0000 0000 . . . 0010
// for(int signum = 31;signum >= 1;signum--)
// {
// if(sigismember(&pending,signum))
// {
// printf("1");
// }
// else
// {
// printf("0");
// }
// }
// printf("\n");
// }
// void handler(int signo)
// {
// std::cout << "处理了: " << signo << " 信号" << std::endl;
// std::cout << " ######################" << std::endl;
// sigset_t pending;
// sigfillset(&pending);
// int m = sigpending(&pending);
// (void)m;
// // 打印
// PrintPending(pending);
// std::cout << " ######################" << std::endl;
// }
// int main()
// {
// // 0.自定义捕捉2号信号
// signal(SIGINT,handler);
// // 获取当前进程的pid
// std::cout << "pid: " << getpid() << std::endl;
// // 1、定义并且初始化信号集
// sigset_t block,old_block;
// sigemptyset(&block);
// sigemptyset(&old_block);
// for(int signum;signum <= 31;signum++)
// sigaddset(&block,signum);
// // 2、向block信号集添加信号
// sigaddset(&block,SIGINT); // 有没有设置到当前进程内部?没有!
// // 3、屏蔽2号信号
// int n = sigprocmask(SIG_SETMASK,&block,&old_block);
// // 4、不断获取pending信号集 && 打印
// int cnt = 0;
// sigset_t pending;
// while(true)
// {
// sigemptyset(&pending);
// // 获取pending信号集
// int m = sigpending(&pending);
// (void)m;
// // 打印
// PrintPending(pending);
// sleep(1);
// cnt++;
// // 尝试恢复
// if(cnt == 20)
// {
// std::cout << "恢复2号信号,解除屏蔽" << std::endl;
// sigprocmask(SIG_SETMASK,&old_block,nullptr);
// }
// }
// return 0;
// }
// void PrintPending(sigset_t &pending)
// {
// // 0000 0000 . . . 0000 -> 0000 0000 . . . 0010
// for(int signum = 31;signum >= 1;signum--)
// {
// if(sigismember(&pending,signum))
// {
// printf("1");
// }
// else
// {
// printf("0");
// }
// }
// printf("\n");
// }
// int main()
// {
// // 获取当前进程的pid
// std::cout << "pid: " << getpid() << std::endl;
// // 1、定义并且初始化信号集
// sigset_t block,old_block;
// sigemptyset(&block);
// sigemptyset(&old_block);
// // 2、向block信号集添加信号
// sigaddset(&block,SIGINT); // 有没有设置到当前进程内部?没有!
// // 3、屏蔽2号信号
// int n = sigprocmask(SIG_SETMASK,&block,&old_block);
// // // 声明
// // sigset_t pending;
// // sigemptyset(&pending);
// // // 获取pending信号集
// // int m = sigpending(&pending);
// // (void)m;
// //
// // // 打印
// // PrintPending(pending);
// return 0;
// }
结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!
往期回顾:
【Linux信号】Linux进程信号(上):信号产生方式和闹钟
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
