中断与异常模型图

  1. 内中断

    内中断是由 CPU 内部事件引起的中断,通常是在程序执行过程中由于 CPU 自身检测到某些异常情况而产生的。例如,当执行除法运算时除数为零,或者访问了不存在的内存地址,CPU 就会产生内中断。

    1. 硬件异常

      CPU内部产生的异常事件

      1. 故障Fault

        故障是在指令执行过程中检测到的错误情况导致的内中断,比如空指针,除0异常,缺页中断等
      2. 自陷Trap

        这是一种有意的内中断,是由软件预先设定的特殊指令或操作引起的。比如syscall,int 3这种故意设定的陷阱
      3. 终止abort

        终止是一种比较严重的内中断,通常是由于不可恢复的硬件错误或者软件严重错误导致的,比如内存硬件损坏、Cache 错误等
    2. 用户异常

      软件模拟出的异常,比如操作系统的SEH,.NET的OutOfMemoryException
  2. 外中断

    外中断是由 CPU 外部的设备或事件引起的中断。比如键盘,鼠标,主板定时器。这些外部设备通过向 CPU 发送中断请求信号来通知 CPU 需要处理某个事件。外中断是计算机系统与外部设备进行交互的重要方式,使得 CPU 能够及时响应外部设备的请求,提高系统的整体性能和响应能力。

    1. NMI(Non - Maskable Interrupt,非屏蔽中断)

      NMI 是一种特殊类型的中断,它不能被 CPU 屏蔽。与普通中断(可以通过设置中断屏蔽位来阻止 CPU 响应)不同,NMI 一旦被触发,CPU 必须立即响应并处理。这种特性使得 NMI 通常用于处理非常紧急且至关重要的事件,这些事件的优先级高于任何其他可屏蔽中断。
    2. INTR(Interrupt Request,中断请求)

      INTR 是 CPU 用于接收外部中断请求的引脚(在硬件层面)或者信号机制(在软件层面)。外部设备(如磁盘驱动器、键盘、鼠标等)通过向 CPU 的 INTR 引脚发送信号来请求 CPU 中断当前任务,为其提供服务。这是计算机系统实现设备交互和多任务处理的关键机制之一。

用户异常

C#的异常,在Windows平台下是完全围绕SEH处理框架来展开。其开销并不低,内部走了很多流程。

        static void Main(string[] args)
{
try
{
var num = Convert.ToInt32("a");
}
catch (Exception ex)
{
Debugger.Break();
Console.WriteLine(ex.Message);
} Console.ReadLine();
}

眼见为实:用户Execption的调用栈

硬件异常

硬件异常指CPU执行机器码出现异常后,由CPU通知操作系统,操作系统再通知进程触发的异常。

比如:

  1. 内核模式切换:syscall
  2. 访问违例:AccessViolationException
  3. visual studio中F9中断:int 3
        static void Main(string[] args)
{
try
{
string str = null;
var len = str.Length; Console.WriteLine(len);
}
catch (Exception ex)
{
Debugger.Break();
Console.WriteLine(ex.ToString());
} Console.ReadLine();
}

与用户异常不同的是,异常的发起点在CPU上,并且CLR为了统一处理。会先将硬件异常转换成用户异常。以此来复用后续逻辑。所以相比用户异常,硬件异常的开销更大

眼见为实:硬件Execption的调用栈

硬件异常如何与用户异常绑定?

上面说到,CLR会先将硬件异常转换成用户异常。那么在抛出异常的时候,如何正确抛出一个托管堆认识的异常呢?

以空指针异常为例

核心逻辑在ProcessCLRException中,它会判断 Thread 是否挂了异常?没有的话就会通过MapWin32FaultToCOMPlusException来转换,然后通过 pThread.SafeSetThrowables 塞入到线程里。从而实现了硬件异常在托管堆上的映射。

眼见为实

上源码

https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/excep.cpp

.NET 异常处理流程

对.NET Runtime来说,主要实现以下四个操作

  1. 捕获异常并抛出异常的位置

  2. 通过线程栈空间获取异常调用栈

    线程的栈空间维护了整个调用栈,扫描整个栈空间即可获取。

windbg的k系列命令就是参考此原理。

  1. 获取元数据的异常处理表

    一旦方法中有try-catch语句块时,JIT会将try-catch的适用范围记录下来,并整理成异常处理表(Execption Handling Table , EH Table)
C# 代码
    public class ExceptionEmample
{
public static void Example()
{
try
{
Console.WriteLine("Try outer");
try
{
Console.WriteLine("Try inner");
}
catch (Exception)
{
Console.WriteLine("Catch Expception inner");
}
}
catch (ArgumentException)
{
Console.WriteLine("Catch ArgumentException outer");
}
catch (Exception)
{
Console.WriteLine("Catch Exception outer");
}
finally
{
Console.WriteLine("Finally outer");
}
}
}
IL代码
.method public hidebysig static void  Example() cil managed
{
// Code size 96 (0x60)
.maxstack 1
IL_0000: nop
IL_0001: nop
IL_0002: ldstr "Try outer"
IL_0007: call void [System.Console]System.Console::WriteLine(string)
IL_000c: nop
IL_000d: nop
IL_000e: ldstr "Try inner"
IL_0013: call void [System.Console]System.Console::WriteLine(string)
IL_0018: nop
IL_0019: nop
IL_001a: leave.s IL_002c
IL_001c: pop
IL_001d: nop
IL_001e: ldstr "Catch Expception inner"
IL_0023: call void [System.Console]System.Console::WriteLine(string)
IL_0028: nop
IL_0029: nop
IL_002a: leave.s IL_002c
IL_002c: nop
IL_002d: leave.s IL_004f
IL_002f: pop
IL_0030: nop
IL_0031: ldstr "Catch ArgumentException outer"
IL_0036: call void [System.Console]System.Console::WriteLine(string)
IL_003b: nop
IL_003c: nop
IL_003d: leave.s IL_004f
IL_003f: pop
IL_0040: nop
IL_0041: ldstr "Catch Exception outer"
IL_0046: call void [System.Console]System.Console::WriteLine(string)
IL_004b: nop
IL_004c: nop
IL_004d: leave.s IL_004f
IL_004f: leave.s IL_005f
IL_0051: nop
IL_0052: ldstr "Finally outer"
IL_0057: call void [System.Console]System.Console::WriteLine(string)
IL_005c: nop
IL_005d: nop
IL_005e: endfinally
IL_005f: ret
IL_0060:
// Exception count 4
.try IL_000d to IL_001c catch [System.Runtime]System.Exception handler IL_001c to IL_002c
.try IL_0001 to IL_002f catch [System.Runtime]System.ArgumentException handler IL_002f to IL_003f
.try IL_0001 to IL_002f catch [System.Runtime]System.Exception handler IL_003f to IL_004f
.try IL_0001 to IL_0051 finally handler IL_0051 to IL_005f
} // end of method ExceptionEmample::Example

IL代码中最后4行就代表了方法的异常处理表。

1. IL_000d to IL_001c 之间代码发生的Exception异常由IL_001c to IL_002c 之间的代码处理
2. IL_0001 to IL_002f 之间发生的ArgumentException异常由IL_002f to IL_003f之间的代码处理
3. IL_0001 to IL_002f 之间发生的Exception异常由IL_003f to IL_004f之间的代码处理
4. IL_0001 to IL_0051 之间无论发生什么,结束后都要执行IL_0051 to IL_005f之间的代码
  1. 枚举异常处理表,调用对应的catch块与finally块

    当异常发生时,Runtime会枚举EH Table,找出并调用对应的catch块与finally块。

    核心方法为ProcessManagedCallFrame:

https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/exceptionhandling.cpp

需要注意的是,一旦CLR找到catch块,就会先执行内层所有finally块中的代码,再等到当前catch块中的代码执行完毕finally才会执行

  1. 重新抛出异常

    在执行catch,finally的过程中,如果又抛出了异常。程序会再次进入ProcessCLRException中走重复流程。

    但是调用链会消失,如果想要防止调用链丢失,需要特殊处理。
        static void Main(string[] args)
{
try
{
Test();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
} private static void Test()
{
try
{
throw new Exception("test");
}
catch (Exception ex)
{
//throw ex; //会丢失调用链,找不到真正的异常所在
//throw; //调用链完整
//ExceptionDispatchInfo.Capture(ex).Throw();//调用链更完整,显示了重新抛出异常所在的位置。
}
}

我在这里踩过大坑,使用throw ex重新抛出异常,结果丢失了异常真正的触发点,日志跟没记一样。

finally一定会执行吗?

常规情况下,finally是保证会执行的代码,但如果直接用win32函数TerminateThread杀死线程,或使用System.Environment的Failfast杀死进程,finally块不会执行。

先执行return还是先执行finally

C#代码
~~~
public static int Example2()
{
try
{
return 100+100;
}
finally
{
Console.WriteLine("finally");
}
}
~~~
IL代码
.method public hidebysig static int32  Example2() cil managed
{
// Code size 22 (0x16)
.maxstack 1
.locals init (int32 V_0)
IL_0000: nop
IL_0001: nop
IL_0002: ldc.i4.1 //将100+100的值,压入Evaluation Stack
IL_0003: stloc.0 //从Evaluation Stack出栈,保存到序号为0的本地变量
IL_0004: leave.s IL_0014 //退出代码保护区域,并跳转到指定内存区域IL_0014, 指令 leave.s 清空计算堆栈并确保执行相应的周围 finally 块。
IL_0006: nop
IL_0007: ldstr "finally"
IL_000c: call void [System.Console]System.Console::WriteLine(string)
IL_0011: nop
IL_0012: nop
IL_0013: endfinally
IL_0014: ldloc.0 //读取序号0的本地变量并存入Evaluation Stack
IL_0015: ret //从方法返回,返回值从Evaluation Stack中获取
IL_0016:
// Exception count 1
.try IL_0001 to IL_0006 finally handler IL_0006 to IL_0014
} // end of method ExceptionEmample::Example2

从IL中可以看到,当try中包含return语句时,编译器会生成一个临时变量将返回值保存起来。然后再执行finally块。最后再return 临时变量。这个过程称为局部展开(local unwind)

再举一个例子

C#代码
        public static int Test()
{
int result = 1;
try
{
return result;
}
finally
{
result = 3;
}
}
IL代码
.method public hidebysig static int32  Test() cil managed
{
// 代码大小 15 (0xf)
.maxstack 1
.locals init (int32 V_0,
int32 V_1)
IL_0000: nop
IL_0001: ldc.i4.1 //将常量1压栈
IL_0002: stloc.0 //将序号0出栈,赋值给result
IL_0003: nop
IL_0004: ldloc.0 //将当前方法序号0的变量,也就是result,压入栈中。
IL_0005: stloc.1 //将序号1的值出栈,保存到一个临时变量中。也就是return的值
IL_0006: leave.s IL_000d //跳转到对应行, 指令 leave.s 清空计算堆栈并确保执行相应的周围 finally 块。
IL_0008: nop
IL_0009: ldc.i4.3
IL_000a: stloc.0
IL_000b: nop
IL_000c: endfinally
IL_000d: ldloc.1 //将return的值 入栈
IL_000e: ret //执行return
IL_000f:
// Exception count 1
.try IL_0003 to IL_0008 finally handler IL_0008 to IL_000d
} // end of method Class1::Test

虽然在finally块中修改了result的值,但是return语句已经确定了要返回的值,finally块中的修改不会改变这个返回值。不过,如果返回的是引用类型),在finally块中修改引用类型对象的内容是会生效的

异常对性能的影响

引用别人的数据,自己就不班门弄斧了

  1. 大佬的研究

    https://www.cnblogs.com/huangxincheng/p/12866824.html
  2. <.NET Core底层入门>

总体来说,只要进入内核态。就没有开销低的。

CLS与非CLS异常(历史包袱)

在CLR的2.0版本之前,CLR只能捕捉CLS相容的异常。如果一个C#方法调用了其他编程语言写的方法,且抛出一个非CLS相容的异常。那么C#无法捕获到该异常。

在后续版本中,CLR引入了RuntimeWrappedException类。当非CLS相容的异常被抛出时,CLR会自动构造RuntimeWrappedException实例。使之与与CLS兼容

        public static void Example2()
{
try
{ }
catch(Exception)
{
//c# 2.0之前这个块只能捕捉CLS相容的异常
}
catch
{
//这个块可以捕获所有异常
}
}

.NET Core 异常(Exception)底层原理浅谈的更多相关文章

  1. Java线上问题排查神器Arthas快速上手与原理浅谈

    前言 当你兴冲冲地开始运行自己的Java项目时,你是否遇到过如下问题: 程序在稳定运行了,可是实现的功能点了没反应. 为了修复Bug而上线的新版本,上线后发现Bug依然在,却想不通哪里有问题? 想到可 ...

  2. CSRF漏洞原理浅谈

    CSRF漏洞原理浅谈 By : Mirror王宇阳 E-mail : mirrorwangyuyang@gmail.com 笔者并未深挖过CSRF,内容居多是参考<Web安全深度剖析>.& ...

  3. 如何把Java代码玩出花?JVM Sandbox入门教程与原理浅谈

    在日常业务代码开发中,我们经常接触到AOP,比如熟知的Spring AOP.我们用它来做业务切面,比如登录校验,日志记录,性能监控,全局过滤器等.但Spring AOP有一个局限性,并不是所有的类都托 ...

  4. JAVA CAS原理浅谈

    java.util.concurrent包完全建立在CAS之上的,没有CAS就不会有此包.可见CAS的重要性. CAS CAS:Compare and Swap, 翻译成比较并交换. java.uti ...

  5. CAS+SSO原理浅谈

    http://www.cnblogs.com/yonsin/archive/2009/08/29/1556423.htmlSSO 是一个非常大的主题,我对这个主题有着深深的感受,自从广州 UserGr ...

  6. php模板原理PHP模板引擎smarty模板原理浅谈

    mvc是开发中的一个伟大的思想,使得开发代码有了更加清晰的层次,让代码分为了三层各施其职.无论是对代码的编写以及后期的阅读和维护,都提供了很大的便利. 我们在php开发中,视图层view是不允许有ph ...

  7. PHP的模板引擎smarty原理浅谈

    mvc是开发中的一个伟大的思想,使得开发代码有了更加清晰的层次,让代码分为了三层各施其职.无论是对代码的编写以及后期的阅读和维护,都提供了很大的便利. 我们在php开发中,视图层view是不允许有ph ...

  8. Docker 基础底层架构浅谈

    docker学习过程中,免不了需要学习下docker的底层技术,今天我们来记录下docker的底层架构吧! 从上图我们可以看到,docker依赖于linux内核的三个基本技术:namespaces.C ...

  9. Java中的SPI原理浅谈

    在面向对象的程序设计中,模块之间交互采用接口编程,通常情况下调用方不需要知道被调用方的内部实现细节,因为一旦涉及到了具体实现,如果需要换一种实现就需要修改代码,这违反了程序设计的"开闭原则& ...

  10. JDK source 之 LinkedHashMap原理浅谈

    注:本文参考JDK1.7.0_45源码. LinkedHashMap是基于HashMap实现的数据结构,与HashMap主要的不同为每个Entry是使用双向链表实现的,并且提供了根据访问顺序进行排序的 ...

随机推荐

  1. 生成系统中的maven依赖信息

    在项目终端直接执行命令 mvn project-info-reports:dependencies 等待文件生成... 生成信息如下...

  2. callable类型 是什么?

    在 C++ 中,callable 类型(可调用类型)是指可以像函数一样被调用的对象 C++ 中有多种不同的可调用对象类型,它们可以通过函数调用运算符 () 被调用. 常见的 callable 类型包括 ...

  3. 深入理解HDFS 错误恢复

    我们从动态的角度来看 hdfs 先从场景出发,我们知道 hdfs 的写文件的流程是这样的: 数据以 pipeline 的方式写入 hdfs ,然后对于读取操作,客户端选择其中一个保存块副本的 Data ...

  4. 数据库周刊33丨腾讯Tbase新版本发布;“2020数据技术嘉年华”有奖话题遴选;阿里云技术面试题;APEX 实现数据库自动巡检;MYSQL OCP题库……

    摘要:墨天轮数据库周刊第33期发布啦,每周1次推送本周数据库相关热门资讯.精选文章.干货文档. 热门资讯 1.中国移动国产OLTP数据库中标公告:南大金仓阿里,万里开源中兴 分获大单[摘要]近日,中国 ...

  5. 墨天轮访谈 | 阿里云捷熙:AnalyticDB,人人可用的数据分析服务

    分享嘉宾:李婧玮(捷熙) 阿里云数据库资深产品经理 整理:墨天轮社区 导读 大家好,我是来自阿里云的捷熙.AnalyticDB是融合数据库.大数据技术于一体的云原生企业级数据仓库平台,今天我为大家带来 ...

  6. 混合云下的 Kubernetes 多集群管理与应用部署

    本文是上海站 Meetup 中讲师李宇根据其分享内容梳理成的文章 大家好,很高兴来到今天下午的 Meetup.我先简单做个自我介绍,我叫李宇,目前是 KubeSphere 的一名研发,主要负责多集群方 ...

  7. Ubuntu 22.04 全局快捷键失效问题

    安装完 Ubuntu 22.04 后,你有可能会发现系统的快捷键失效了.侧栏用 Win + x 选中程序不可用了.为各种应用程序设置的快捷键也不起作用了. 出现此现象的原因,是因为 Ubuntu 22 ...

  8. Redhat 7.6安装11G RAC GI时遇到此类报错

    环境:Redhat Linux 7.6 + Oracle 11.2.0.4 RAC 现象:图像化安装过程中,按照提示执行root.sh脚本,报错中断. 1. 具体现象 2. 定位问题 3. 解决问题 ...

  9. 买了个mini主机当服务器

    虽然有苹果的电脑,但是在装一些软件的时候,从想着能不能有一个小型的服务器,免得各种设置什么帮我强各种别的导致Mac出现各种的异常,整体上的话去看了一些小的主机,看过苹果的MV迷你Mac,但是发现是太贵 ...

  10. 《用广义CNOT门产生质数幂维的图态》

    参考文献:Graph states of prime-power dimension from generalized CNOT quantum circuit 主机文件:<2016质数图态.p ...