目录

1.准备工作

2.演示浮点异常

结论

3.为什么是无限打印? 换句话说,信号为什么会一直触发? 而且为什么除0会给进程发送信号呢?

除以0异常

又返回到a/=0的原因

回答

补充: 异常的分类

操作系统如何分辨各种异常?

为什么操作系统不直接杀死出异常的进程?


异常也能产生信号

1.准备工作

新建如下文件:

test_signal/
├── makefile
└── test_signal.cpp

makefile写入:

test_signal.out:test_signal.cpp
	g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
	rm -f test_signal.out

2.演示浮点异常

test_signal.cpp写入:

int main()
{
    int a=1;
    a/=0;
    return 0;
}

运行结果: 浮点异常,退出码不是0

man手册中指出,浮点异常对应信号是SIGFPE

man 7 signal

尝试捕捉信号:

#include <unistd.h>
#include <iostream>
#include <signal.h>
void myhandler(int signo)
{
    std::cout<<"已执行自定义动作,信号编号为"<<signo<<"号"<<std::endl;
}
 
int main()
{
    signal(SIGFPE,myhandler);
    int a=1;
    a/=0;
    return 0;
}

运行结果: 进程确实收到了SIGFPE信号,但是无限打印,并没有退出

查看进程的运行状态:

ps ajx | head -1 && ps ajx | grep test_signal

不是僵尸状态:

结论

结论: 异常信号不会让进程退出,但是由于异常,建议还是捕获信号后,手动终止进程,如下:

#include <unistd.h>
#include <iostream>
#include <signal.h>
#include <stdlib.h>
void myhandler(int signo)
{
    std::cout<<"已执行自定义动作,信号编号为"<<signo<<"号"<<std::endl;
    exit(1);
}
 
int main()
{
    signal(SIGFPE,myhandler);
    int a=1;
    a/=0;
    return 0;
}

运行结果:

3.为什么是无限打印? 换句话说,信号为什么会一直触发? 而且为什么除0会给进程发送信号呢?

使用gdb调试这个代码:

#include <unistd.h>
#include <iostream>
#include <signal.h>
#include <stdlib.h>
void myhandler(int signo)
{
    std::cout<<"已执行自定义动作,信号编号为"<<signo<<"号"<<std::endl;
}
 
int main()
{
    signal(SIGFPE,myhandler);
    int a=1;
    a/=0;
    return 0;
}

这里使用gdb-dashboard调试(项目链接: https://github.com/cyrus-and/gdb-dashboard)

分别在a/=0和myhandler的打印语句下断点:

接着r命令执行:

接着c命令继续执行,发现进程收到了SIGFPE,停留在idiv %ecx:

接着c命令继续执行,进入自定义函数:

接着c命令继续执行,又回到了a/=0,仍然停留在idiv %ecx(这个是关键,下面会从CPU手册中寻找答案):

接着c命令继续执行,又进入自定义函数:

第一次执行到a/=0处,CPU产生除法异常,操作系统收到后发送SIGFPE信号给进程,执行myhandler函数,当myhandler返回时,又返回到a/=0,CPU又产生除法异常...... 就这样无限打印......

除以0异常

Intel® 64 and IA-32 Architectures Software Developer’s Manual中规定:除以0会产生#DE异常

Vol. 2A 3-5:

Vol. 2A 3-15:

又返回到a/=0的原因

Intel® 64 and IA-32 Architectures Software Developer’s Manual中规定:

Vol. 3A 7-5:

这里直接摘抄李忠老师的《x86汇编语言:从实模式到保护模式 第二版》第344页的说明,是对Intel手册的解释:

故障(Faults)。故障通常是可以纠正的,比如,当处理器执行一个访问内存的指令时,
发现那个段或者页不在内存中(P=0),此时,可以在异常处理程序中予以纠正(分
配内存,或者执行磁盘的换入换出操作),返回时,程序可以重新启动并不失连续性。
为了做到这一点,当故障发生时,处理器把机器状态恢复到引起故障的那条指令之前
的状态,在进入异常处理程序时,压入栈中的返回地址(CS 和EIP 的内容)是指向引
起故障的那条指令的,而不像通常那样指向下一条指令
。如此一来,当中断返回时,
将重新执行引起故障的那条指令,而且不再出错(如果引起异常的情况已经妥善处
置)。这意味着,异常并不总是意味着坏消息,相反,很多时候,它是有益的,就像
益虫。如果没有异常,虚拟内存管理将无从谈起。

得出结论: 异常不一定都是坏的

对于本代码,进程在进入异常处理程序时,压入栈中的返回地址(CS 和EIP 的内容)是指向引
起故障的那条idiv %ecx指令的,而不像通常那样指向下一条指令
[这个是无限打印的关键],由于没有纠正这个故障异常,那么每次从handler函数返回时,都会再次执行a/=0,从而再次引发异常

上面提到的"压入栈中的返回地址(CS 和EIP 的内容)"可以理解为进程的上下文

事实上,由于进程的独立性,进程出异常时只会影响自己,修改的是自己的状态寄存器,因为异常会保存在进程的上下文中,不会导致CPU无法执行其他进程

我对"在进入异常处理程序时,压入栈中的返回地址(CS 和EIP 的内容)是指向引
起故障的那条指令的,而不像通常那样指向下一条指令
"这句话的理解:

"压入栈中的返回地址是指向引起故障的那条指令的"是合情合理的,假设操作系统要向一个虚拟内存区域写入数据,如果这个虚拟内存区域没有分配物理内存,那么会产生缺页异常(page-fault exception,#PF),执行对于的异常处理程序,操作系统分配好对应的物理内存后,再次执行原来未成功执行的向一个虚拟内存区域写入数据的代码就没有问题了

缺页异常的处理图:

可以作答无限打印的原因了:

回答

答: 出异常的进程一直被调度,但故障异常一直没有修正,导致操作系统对该进程保存的上下文的中仍然有异常,所以操作系统调度该进程时一直报错,发送信号,该进程捕获到这个信号就一直执行自定义方法,因此无限打印

补充: 异常的分类

Intel® 64 and IA-32 Architectures Software Developer’s Manual中是这样分类异常的:

7-4 Vol. 3A:

这里直接摘抄李忠老师的《x86汇编语言:从实模式到保护模式 第二版》第344页的说明,是对Intel手册的解释:

        异常就是我们在介绍16 位汇编语言时所说的内部中断。它们是处理器内部产生的中断,
表示在指令执行的过程中遇到了错误的状况。当处理器执行一条非法指令,或者因条件不具备,指令不能正常执行时,将引发这种类型的中断。以上所列的情况都是异常情况,所以内部中断又叫异常或者异常中断。比如,在执行除法指令div/idiv 时,遇到了被0 除的情况(除数是0);再比如,使用jmp 指令发起任务切换时,指令的操作数不是一个有效的 TSS 描述符选择子。
        异常分为三种,第一种是指令执行异常,或者叫程序错误异常,指处理器在执行指令的过程中,检测到了程序中的错误,并由此而引发的异常。
        第二种是程序调试异常,它们是为调试程序而特意准备的礼物。这类异常通常由into、
int3 和bound 指令主动发起,实际上也是软件引发的异常。这些指令允许在指令流的当前点上检查实施异常处理的条件是否满足。举个例子来说,into 指令在执行时,将检查寄存器
EFLAGS 的OF 标志位,如果满足为“1”的条件,则引发异常。
        第三种是机器检查异常。这种异常是处理器型号相关的,也就是说,每种处理器都不太一样。比如奔腾4、至强和P6 处理器族就实现了机器检查架构,用于检测和报告与硬件有关的总线错误、奇偶校验错误、高速缓存错误,等等。当检测到有错误时,将引发此类异常。

显然: 异常不只有硬件产生,例如管道: 写端继续写,读端关闭,那么操作系统会杀死写端的进程(发送13号信号SIGPIPE)这是软件异常

操作系统如何分辨各种异常?

上面看到了异常有很多类,每类里面有很多种,那么操作系统是如何分辨各种异常的呢?

Intel手册给出了一般异常表:

Vol. 2A 3-15:

每个异常都有自己的编号(Vector列,向量号),操作系统靠这个编号区分不同的异常

为什么操作系统不直接杀死出异常的进程?

因为进程引发的绝大部分的异常无法修复,所以进程就应该退出

操作系统不会直接杀死进程,而是发送信号给进程,好让进程捕获异常告知用户情况、进程保存未做完的内容方便用户查看......

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐