文章目录
- 概要&序論
- 一、 硬件异常产生信号
- 1.1什么是硬件异常
- 1.2硬件异常举例
- 1.2.1浮点数错误
- 1.2.2段错误
- 1.3 CPU是怎么知道硬件错误
- 1.3.1算术溢出与除0错误
- 1.3.2 内存访问段错误
- 1.4 MMU 查表时的权限与状态判定
- 1.4.1 根本没有映射(空指针引发的未分配错误)
- 1.4.2 有映射但越界或越权(非法访问限制区域)
- 1.5 操作系统的接管与进程上下文读取
- 1.6 合法与非法的甄别,产生信号
- 二、软件条件产生信号
- 2.1管道产生信号
- 2.2闹钟产生信号
- 2.2.1先见一见闹钟怎么用
- 2.2.2闹钟的用法详细介绍
- 2.2.3递归闹钟与操作系统的基本运行原理
- 2.2.4时间戳与时间片
- 2.2.5操作系统对闹钟的先描述再组织
概要&序論
Hello,大家好我是此方,今天继续介绍信号是如何产生的问题。上一篇我们介绍了信号的两种产生机制:键盘产生和系统调用/命令产生。本文将讲解剩下两类信号产生机制——“硬件异常”与“软件条件”。同时带大家初步接触“时钟中断”等内容。
一、 硬件异常产生信号
1.1什么是硬件异常
在 Linux 操作系统中,程序的崩溃或异常终止(如除0错误、野指针访问)会触发硬件异常,CPU 首先捕捉到硬件异常,并立刻通过中断机制将其移交给操作系统内核。
信号由操作系统发送。
1.2硬件异常举例
1.2.1浮点数错误
8号信号:SIGFPE (Floating-point exception)浮点数异常。
1.2.2段错误
11号信号:SIGSEGV (Segmentation violation / Segmentation fault)段错误。
1.3 CPU是怎么知道硬件错误
操作系统作为软硬件资源的管理者,之所以能知道进程的犯错,得益于 CPU 内部各种寄存器和硬件单元的实时监控:
1.3.1算术溢出与除0错误
当程序执行a /= 0时,CPU 的算术逻辑单元(ALU)在计算时会触发异常,并自动将状态寄存器(如 x86 的EFLAGS寄存器)中的特定标志位(例如溢出或错误标志)置位。
1.3.2 内存访问段错误
当程序尝试访问int *p = nullptr; *p = 100;,负责地址翻译的集成硬件MMU(内存管理单元)会拿着该虚拟地址和CR3 寄存器中保存的当前进程页表物理地址去进行转换。MMU 在将虚拟地址转换为真正的物理地址时,主要根据页表项中的“权限和状态”来判断访问是否合法。
1.4 MMU 查表时的权限与状态判定
1.4.1 根本没有映射(空指针引发的未分配错误)
页表项中有一个最关键的位叫Present(存在位,通常是 Bit 0):
- Present = 1:代表该虚拟地址已经映射了真实的物理内存。
- Present = 0:代表这块虚拟地址在物理内存中根本不存在。
当 MMU 发现对应行的Present位为 0 时,硬件逻辑电路就会自动触发中断信号(硬件报错)。
1.4.2 有映射但越界或越权(非法访问限制区域)
即使地址存在映射,页表项中还包含R/W(读写权限位)和U/S(用户/内核权限位):
- 如果代码企图去修改(写)一个只读的常量字符串(R/W位为 0),MMU 就会报错。
- 如果普通应用程序(用户态)企图去访问内核才能看的地址(U/S位为 0),MMU 同样会报错。
1.5 操作系统的接管与进程上下文读取
当 MMU 或 CPU 触发硬件报错后,CPU 会瞬间暂停当前正在执行的应用程序,强行切换到操作系统的缺页异常处理程序。此时,操作系统必须立刻、无条件地读取当前进程的上下文(即内核中的current->task_struct以及 CPU 寄存器),以此弄清楚两件事:是谁在哪个地方犯了错?
在 Linux 内核源码中,current 是一个极其常用的宏或者指针。它永远指向当前正在 CPU 上执行的那个进程的 task_struct。
操作系统会读取以下一组特殊的寄存器:
- CR2 寄存器:这个寄存器极其重要!MMU 报错时,会自动把导致报错的那个虚拟地址塞进 CR2 里。操作系统读取它,才能知道程序刚才是在访问哪块内存时翻车的。
- 程序计数器(CS:RIP / EIP):操作系统要看看底是哪一行代码(哪条指令)触发了这次内存访问。
- 通用寄存器和错误码(Error Code):搞清楚程序当时是在执行“读”还是“写”,以及当时是在用户态还是内核态。
1.6 合法与非法的甄别,产生信号
拿到上下文并读取 CR2 里的虚拟地址后,操作系统会去对比当前进程的VMA(虚拟内存区域,即 vm_area_struct 链表)来甄别这次报错是否合法:
- 合法错误(真正的缺页):比如使用
mmap分配了虚拟内存,但在第一次读写时物理内存还没落实(Present = 0)。操作系统会执行补齐页表的操作,分配物理内存并建立映射,随后恢复上下文,重新执行那条代码,程序得以“复活”。 - 非法错误(真正的越界/野指针):如果操作系统发现该虚拟地址根本不在进程合法的虚拟内存区域内,或者违反了读写权限。操作系统就会将其判定为非法,并向目标进程发送SIGSEGV(段错误)信号;若是除0错误,则发送SIGFPE(浮点异常)信号。最终强行杀死进程,造成程序崩溃。
二、软件条件产生信号
2.1管道产生信号
软件条件产生信号的方式有很多种,我们曾经学过管道产生信号的方式。Re:Linux系统篇(三十七)通信篇·二:深入理解匿名管道:父子进程是如何“看到同一个文件”
操作系统不会做任何浪费资源的事情。当读端关闭的时候,写段任在写入就毫无意义。此时操作系统必须发送信号SIGPIPE终止进程。
2.2闹钟产生信号
2.2.1先见一见闹钟怎么用
这一块本来应该放在时钟中断讲,但是放在这里也可以。
补充一下pause系统调用,让这个进程处于暂停状态。
#include<iostream>#include<unistd.h>#include<signal.h>voidHander(inti){std::cout<<"收到一个信号"<<i<<std::endl;}intmain(){for(inti=0;i<32;i++)signal(i,Hander);alarm(10);std::cout<<"定一个闹钟"<<std::endl;std::cout<<"开始睡觉"<<std::endl;pause();std::cout<<"睡醒"<<std::endl;return0;}如上。我们让进程设置一个闹钟。时间到了就会发送14号信号。终止进程。
2.2.2闹钟的用法详细介绍
调用alarm(seconds)后,系统开始倒计时。
- 时间到了会怎样?系统会向你的程序发送一个SIGALRM 信号。
- 默认后果:如果你的程序没有提前做好准备,收到这个信号后会直接退出(终止运行)。
- 一次只能设一个闹钟:如果先调用了alarm(10),紧接着又调用了alarm(3),前一个闹钟会被直接覆盖。程序只会在 3 秒后收到闹钟信号。
- 如何取消闹钟:调用alarm(0)即可直接关闭倒计时,不会再触发信号。
2.2.3递归闹钟与操作系统的基本运行原理
我们让闹钟执行的自定义捕捉函数调用新的闹钟。让每一个闹钟响起后执行随机的工作
#include<iostream>#include<unistd.h>#include<signal.h>#include<time.h>#include<vector>#include<functional>std::vector<std::function<void(int)>>work;voidfunc_1(intx){std::cout<<"我是一个网络任务,参数为: "<<x<<std::endl;}voidfunc_2(intx){std::cout<<"我是一个磁盘任务,参数为: "<<x<<std::endl;}voidfunc_3(intx){std::cout<<"我是一个日志任务,参数为: "<<x<<std::endl;}voidfunc_4(intx){std::cout<<"我是一个计算任务,参数为: "<<x<<std::endl;}voidfunc_5(intx){std::cout<<"我是一个数据库任务,参数为: "<<x<<std::endl;}voidHander(inti){std::cout<<"###########################"<<std::endl;work[rand()%5](rand()%100);alarm(1);std::cout<<"###########################"<<std::endl;}intmain(){work.push_back(func_1);work.push_back(func_2);work.push_back(func_3);work.push_back(func_4);work.push_back(func_5);srand((unsignedint)time(NULL));for(inti=0;i<32;i++)signal(i,Hander);alarm(1);//std::cout<<"定一个闹钟"<<std::endl;//std::cout<<"开始睡觉"<<std::endl;while(true)pause();//std::cout<<"睡醒"<<std::endl;return0;}进程可以处于暂停状态,通过外部信号传达实现不同功能的调用。——这就是操作系统,以及操作系统去做进程管理和内存管理等工作的基本原理。
比如拿进程管理为例子,进程被创建好之后PCB就被push_back到一个管理结构中,然后操作系统在运行的时候会去遍历这个管理结构,找到时间片的下一个进程,然后执行他。
操作系统的本质就像是我们这个进程,登录以后就处于一种死循环。操作系统不会去主动执行这些任务。alarm () 间隔时间为0…0几秒,这样的时间间隔,我们的操作系统就会以非常高的频率来执行我们的任务!
操作系统居然不是主动干活的
后面会讲这就是时钟中断
2.2.4时间戳与时间片
时间戳的本质就是一个计数器,外部每一秒刺激操作系统一次,操作系统调用一次就++,实时记录操作系统一共开机了多少时间(操作系统自启动后的时间间隔)
2.2.5操作系统对闹钟的先描述再组织
OS内会不会同时存在很多的闹钟!OS要不要对闹钟进行管理?先描述,在组织!
闹钟结构体的伪代码:
structtimer_list{structlist_headentry;unsignedlongexpires;void(*function)(unsignedlong);unsignedlongdata;structtvec_t_base_s*base;};我们的很多闹钟会被按照这个时间间隔值(unsigned long expires;(操作系统自启动后的时间间隔))进行建小堆,操作系统对比自己的时间间隔值和闹钟的时间间隔值:1000<1005 不响。1006>1005响。然后调用闹钟的函数,传递信号SIGALRM。
新建一个闹钟就是在堆里面插入一个闹钟结构体。但是实际上在内核中,并没有使用这个堆。
闹钟产生信号的这个过程没有硬件参与,这就是软件条件触发信号的另一种方式。扯的有点远了
信号的产生。我们就讲完了。