CLR是如何工作的

原文连接:https://www.cnblogs.com/awpatp/archive/2009/11/11/1601219.html

MetData和引擎初始化

托管程序集本身只包含 CLR 可识别的 MetaData(元数据),不包含机器指令。托管程序集都依赖于 mscoree.dll 。mscoree.dll 在system32目录下,全称是Microsoft Core Execution Engine。它的功能是选择合适的CLR Execution Engine来加载。

2009-11-11 18-18-06

多个版本的CLR可以共存。CLR的目录在 C:\Windows\Microsoft.NET\Framework。当前系统中最新版本的CLR对应的 mscoree.dll文件被拷贝到system32目录下。

当mscoree.dll加载后,它根据托管代码的 metadata 和 app.config,选择恰当版本的引擎加载。同时 mscoree 还负责判断应该用何种 GC Flavor。GC Flavor 包括 Workstation GC 和 Server GC。在CLR1中, Workstation GC 对应到 mscorwks.dll,而 Server GC 对应到 mscorsvr.dll 文件。在 CLR2 中虽然保留了 mscorsvr.dll 文件,但是 mscorwks.dll 已经包含了两种 GC Flavor 的实现, 只需要加载 mscorwks 就可以了。

CLR加载后,先初始化CLR需要的各种功能,比如必要的全局变量,引擎需要的模块(ClassLoader, assembly Loader, JitEngine, Copntext等),启动 Finalizer thread 和 GC thread,创建 System AppDomain 和 Shared AppDomain,创建 RCDebugger Thread,加载CLR基础类 (比如mscorlib.dll, system.dll)。

当CLR引擎初始化完成后,CLR会找到当前exe的元数据,然后找到Main函数,编译Main函数,,执行Main函数。

JIT动态编译

1. 全托管代码

假设 C# 函数 foo1 要调用 foo2。当CLR编译 foo1 的时候,无论 foo2 是否已经编译成机器代码,call 指令都是把执行指向到跟foo2 相关的一个内存地址(stub)。当执行这个 call 指令的时候,如果 foo2 没有被CLR编译,stub 中的代码就会把执行定向到 CLR JitEngine,这样对foo2的调用便导致了CLR JitEngine 的启动来编译 foo2 函数。Jit Engine 编译完成之后,CLR 把编译好的机器代码拷贝到进程中由 CLR 管理的某一块内存(loader heap)上,然后 Jit Engine 把编译好的 foo2 函数入口地址填回到 stub 中。

通过这样的技术,第二次对 foo2 调用的时候,foo2 的 stub 指向的已经是编译好的地址了,于是不需要再次编译。当然第一次编译完成之后,JitEngine 同时需要负责执行刚刚编译好的函数。

2. 托管代码调用非托管代码

在CLR的执行过程中,如果使用到的都是托管代码,编译和执行就按照上面的逻辑进行。但是不可避免的,托管代码需要调用非托管代码。这里分两种情况。

第一种是调用系统 API 和 DLLImport。比如CLR中使用 FileStream 打开一个文件,最终还是要调用到 CreateFileW。通过DLLImport 调用自定义的非托管函数,以及 COM Interop 也属于这种情况。

第二种是调用 CLR Runtime 的功能,比如内存分配,异常派发。

两种情况都使用 stub 技术。对于第一种情况,不管是 PInvoke 还是 COM Interop 发生的时候,托管代码调用的都是由CLR创建的stub。在这个 stub 中CLR会做一些必要的工作,然后把控制权交给对应的非托管代码。必要的工作包括把必要的函数参数拷贝到非托管的内存上,marshal 必要的类型,锁住需要跟非托管代码交互的托管内存区域,防止GC移动这块内存。如果是COM Interop,还包括对非托管接口指针进行必要的 QueryInterface 等等。当非托管调用结束后,执行权返回 stub,再次进行必要的工作后,回到托管代码。

第二种情况中,对CLR功能的调用往往是隐式发生的。

一类是编译器直接生成对 CLR stub 的调用。比如 new / throw 关键字。动态编译引擎对这些关键词的处理是生成函数调用到特殊的 stub 上,stub 再把执行定位到CLR引擎中的关键函数。就分配内存来讲,比如 new 一个 StringBuilder object,动态编译生成的指令把执行权定向到特殊的 stub,该 stub 包含了指令来调用 CLR 中的内存分配函数,同时传入类型信息。

另一类是通过把托管代码标示为 internal call 来编译。 Internal call 表示该托管函数其实是某些 unmanaged 函数的映像,编译引擎在编译 internal call 的时候,会直接把标记的 internal call 属性的CLR方法,直接跟 unmanaged 的函数实现对应起来。该对应关系是在CLR的实现中通过 C++ 的一张静态表定义的。

GC内存管理

CLR 引擎初始化的时候会向操作系统申请连续内存作为 managed heap。所有的 managed object 都分配在 managed heap 中。对于任何一种托管类型,由于类型信息保存在 metadata 中,所以CLR清楚如何生成正确的内存格式。

当托管类型分配请求定向到CLR中后,CLR首先检查 managed heap 是否足够。如果足够,CLR 直接使用鲜有内存,根据类型信息填入必要的格式资料,然后把地址传递给托管代码使用。如果托管堆不够,CLR 执行 GC 试图请扫除一部分内存。如果GC无法清扫出内存,CLR 向 OS 请求更多的内存作为 managed heap。如果OS拒绝内存请求,OutOfMemory 就发生了。CLR 内存分配的特点是:

1. 大多数情况下比非托管代码内存分配速度快。CLR保持内部指针指向当前托管堆中的 free point。只要内存足够,CLR直接把当前指针所在地址作为内存分配出去,然后用指针加/减分配出去的内存的长度。对于非托管代码的内存分配,不管是 Heap Manager,还是 Virtual Memory allocation,都需要做相应计算才能找到合适的内存进行分配。

2. 由于托管对象受到CLR的管理,GC发生的时候 CLR 可以对托管 object 进行随意移动,然后修正保存 object 的 stub 信息,保证托管代码不会受此影响。移动 object 可以防止内存碎片的产生,提高内存使用效率。对于非托管代码来说,由于程序可以直接使用指针,所以无法进行内存碎片整理。

3. GC可以在任何时候触发,但是GC不能在任何时候发生。比如某一个线程正在做 regex 的匹配,访问到大量的托管object,很多object 的地址保存到CPU寄存器上进行优化。如果GC发生,导致 object 地址变化,恢复运行后CPU寄存器上的指针可能就会无效。所以GC必须在所有线程的执行状态都不会受到 GC 影响的时候发生。当线程的执行状态不受影响时,该线程的PreEmptive GC 属性是1, 否则是0。这个开关受到CLR的控制,很多 stub 中的代码会操作这个开关。比如托管代码调用了MessageBox.Show,该方法最后会调用到MessageBox API。在 stub 调用 API 从托管代码变化到非托管代码前,stub 会通过 CLR 内部方法把 PreEmptive 设定为1,表示 GC 可以发生了。大致的情况是,当线程 idle 的时候 (线程 idle 的时候肯定是在等某一个系统API,比如 sleep 护着 WaitForSingleObject ),PreEmptive 为1。当线程在托管代码中干活的时候,PreEmptive为 0。当GC触发的时候,GC必须等到所有的线程都进入了 PreEmptive 模式后,才可以发生。

Exception Handling 异常处理

异常处理在CLR中也非常有特色。比如,NullReferenceException 和 Access Violation 其实是密切相关的。当编译的托管代码执行的时候,对于 NULL object 的访问,首先触发的是 Access Violation.。但是聪明的 CLR 已经设定好了对应的 FS:[0] 寄存器来截获可能的异常。CLR截获异常后,首先检查异常的类型,对于Access Violation,CLR先检查当前的代码是否是托管代码,对应的类型信息是什么。发现是 NULLobject 访问后,CLR再把这个 Access Violaiton 异常标记为已处理,然后生成对应的NullReferenceException 抛出来。当 NullReferenceException 被CLR设定的 FS:[0] 截获后,CLR 发现异常是 CLR Exception,于是找对应的 catch 语句执行。

CLR异常发生之后可以打印出 call stack,原因在于CLR可以通过原数据采集所有的类型信息,同时CLR在 thread 中通过多种机制记录运行状态。保存在 Stack 中的 Frame 就是其中的一种重要的数据结构。Frame 是 CLR 保存在 stack 中的小块数据结构。当 therad 的执行状态发生改变的时候,比如在托管代码和非托管代码中切换,异常产生,remoting 调用等等的时候,CLR会恰当的插入Frame 来标示状态的改变。thread 中所有的 frame 是通过指针链接在一起的,所以CLR可以方便的获取一个 thread 的各种状态情况。

总结:

1. 运行托管程序集的时候,先会加载mscoree.dll。

2. 系统中最新的 mscoree.dll 被加载,然后 mscoree.dll 根据托管程序集的 metadata 决定该加载那个版本的CLR。同时加载GC Flavor。

3. CLR执行初始化

4. CLR找到当前 exe 的 metadata,找到,编译,执行main函数。

5. 过程中后可能遇到另外的函数,第一次运行的时候都要先编译,然后用 stub 技术让调用者拿到编译后的函数入口,完成调用。

6. 运行过程中,如果请求内存会用到GC的一些特性。

7. 出了异常,会用到CLR的一些特性。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值