原文:http://www.blogcn.com/User8/flier_lu/index.html?id=3024651
我们知道 CLR 中 Assembly 是在名为 AppDomain 的逻辑空间中被载入运行的,而 AppDomain 是介于操作系统层面进程和线程概念之间,同时具有线程的轻便和进程的封闭性,使用者可以通过 AppDomain.CreateDomain 创建新的 AppDomain。这样一来就出现了一个鸡生单还是蛋生鸡的问题,这个 AppDomain.CreateDomain 方法肯定是要在一个载入了 AppDomain 类型的 AppDomain 里面被调用的,但这个 AppDomain 又是谁调用 AppDomain.CreateDomain 方法创建的呢?呵呵
我们可以使用 WinDbg + SOS 的 EEHeap 命令,通过列出 CLR 执行引擎的堆信息,获取当前运行的 AppDomain 情况。我们以下面这段代码为例
以下内容为程序代码:
//
// AppDomain.cs
//
using System;
public class EntryPoint
{
public static void Main(string[] args)
{
Console.Out.WriteLine("Hello AppDomain!"
;Console.In.ReadLine();
}
}
这个典型的 CLR 程序的输出如下:
以下为引用:
0:003> !EEHeap
succeeded
Loaded Son of Strike data table version 5 from "E:WINDOWSMicrosoft.NETFrameworkv1.1.4322mscorwks.dll"
Loader Heap:
--------------------------------------
System Domain: 793e6fc8
LowFrequencyHeap:00960000(2000:00001000)
Size: 0x00001000(4096) bytes.
HighFrequencyHeap:00962000(8000:00001000)
Size: 0x00001000(4096) bytes.
StubHeap:0096a000(2000:00001000)
Size: 0x00001000(4096) bytes.
Total size: 0x3000(12288)bytes
--------------------------------------
Shared Domain: 793e83f8
LowFrequencyHeap:00990000(2000) 06c40000(10000:00007000)
Size: 0x00009000(36864) bytes.
HighFrequencyHeap:00992000(8000:00001000)
Size: 0x00001000(4096) bytes.
StubHeap:0099a000(2000:00001000)
Size: 0x00001000(4096) bytes.
Total size: 0xb000(45056)bytes
--------------------------------------
Domain 0: 147330
LowFrequencyHeap:00970000(2000) 06c60000(10000:00004000)
Size: 0x00006000(24576) bytes.
HighFrequencyHeap:00972000(8000:00004000)
Size: 0x00004000(16384) bytes.
StubHeap:0097a000(2000:00001000)
Size: 0x00001000(4096) bytes.
Total size: 0xb000(45056)bytes
--------------------------------------
Jit code heap:
Normal Jit:06c80000(10000:00002000)
Size: 0x00002000(8192) bytes.
Total size 0x00002000(8192)bytes.
--------------------------------------
Total LoaderHeap size: 0x1b000(110592)bytes
=======================================
generation 0 starts at 0x04aa1040
generation 1 starts at 0x04aa1034
generation 2 starts at 0x04aa1028
segment begin allocated size
04aa0000 04aa1028 04aa4000 00002fd8(12248)
Large object heap starts at 0x05aa1028
segment begin allocated size
05aa0000 05aa1028 05aa6000 0x00004fd8(20440)
Total Size 0x7fb0(32688)
------------------------------
GC Heap Size 0x7fb0(32688)
我们可以看到,虽然这个程序非常简单,没有自己创建任何 AppDomain,但实际上 CLR 已经有了三个 AppDomain:"System Domain", "Shared Domain" 和 "Domain 0"。而进一步使用 DumpDomain 命令查看三个 AppDomain:
以下为引用:
0:003> !DumpDomain 793e6fc8
Domain: 793e6fc8
LowFrequencyHeap: 793e702c
HighFrequencyHeap: 793e7080
StubHeap: 793e70d4
Name:
Assembly: 00158e48 [mscorlib]
ClassLoader: 00158f20
Module Name
79b66000 e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll
0:003> !DumpDomain 793e83f8
Domain: 793e83f8
LowFrequencyHeap: 793e845c
HighFrequencyHeap: 793e84b0
StubHeap: 793e8504
Name:
0:003> !DumpDomain 147330
Domain: 00147330
LowFrequencyHeap: 00147394
HighFrequencyHeap: 001473e8
StubHeap: 0014743c
Name: appdomain.exe
Assembly: 0015c2c0 [appdomain]
ClassLoader: 00161008
Module Name
00161d50 d: empappdomain.exe
我们可以看到,System Domain 实际上是专门用于载入 mscorlib.dll 这个 BCL 基础库的;Shared Domain 暂时没有使用;而 Domain 0 则负责运行我们的目标 Assembly。我们可以猜测 System Domain 是 CLR 专门用来载入系统基础库的,而系统将进一步使用此 mscorlib 创建其他 AppDomain 以运行用户目标 Assembly。我们接下来看看 Rotor 的相关代码,是否能够予以印证。
在 CLR 启动时负责加载执行引擎的 EEStartup 函数(vmceemain.cpp:206)中,可以发现此函数首先在进行基础性初始化工作后,调用 SystemDomain::Attach 函数载入 SystemDomain,然后加载并初始化异常处理、JITer等等支持代码,最后会调用 SystemDomain::Init 函数完成初始化 SystemDomain 等等工作。
SystemDomain::Attach 函数(vmappdomain.cpp:912)主要完成四部分工作:初始化系统 stub 管理器和 SystemDomain 的静态成员变量;以全局静态数组 g_pSystemDomainMemory 的内存区,构造并初始化 SystemDomain 对象,并将指针保存到 m_pSystemDomain 静态变量中,用于以后判断 SystemDomain 是否被构造等功能使用;构造缺省的 AppDomain;构造 SharedDomain。函数的简要功能代码如下:
以下内容为程序代码:
SystemDomain* SystemDomain::m_pSystemDomain = NULL;
static BYTE g_pSystemDomainMemory[sizeof(SystemDomain)];
HRESULT SystemDomain::Attach()
{
// 判断 SystemDomain 是否已经构造
_ASSERTE(m_pSystemDomain == NULL);
if(m_pSystemDomain != NULL)
return COR_E_EXECUTIONENGINE;
// 初始化系统 stub 管理器和 SystemDomain 的静态成员变量
// ...
// 构造 SystemDomain 对象
m_pSystemDomain = new (&g_pSystemDomainMemory) SystemDomain();
if(m_pSystemDomain == NULL) return COR_E_OUTOFMEMORY;
// 初始化 SystemDomain 对象
HRESULT hr = m_pSystemDomain->BaseDomain::Init(); // Setup the memory heaps
if(FAILED(hr)) return hr;
m_pSystemDomain->GetInterfaceVTableMapMgr().SetShared();
// 构造缺省的 AppDomain
hr = m_pSystemDomain->CreateDefaultDomain();
if(FAILED(hr)) return hr;
// 构造 SharedDomain
hr = SharedDomain::Attach();
return hr;
}
值得注意的是,为了让 SystemDomain 的构造不会失败,SystemDomain 及其基类 BaseDomain 的构造函数都为空,而初始化代码放到 Init 方法中完成,CLR 中很多类型的代码都使用类似的模式将构造和初始化分离以保障构造成功。BaseDomain::Init 函数在 SystemDomain::Attach 中直接被调用以初始化 SystemDomain 的父类;SystemDomain::Init 函数则在上面提到的 EEStartup 函数末尾才被调用,待会再详细讨论。
BaseDomain::Init 函数(vmappdomain.cpp:310)除了要负责初始化 BaseDomain 对象的一大堆成员变量外,主要负担堆和缓存的初始化。CLR 中的堆,实际上是在每个 AppDomain 中存在的,这也是为什么我们刚刚可以使用 EEHeap 命令列举 AppDomain 的原因。在初始化 BaseDomain 之后,会将 SystemDomain 的接口 VTable 映射表设置为共享,这是因为 SystemDomain 负责载入的 mscorlib 中类型实际上是所以 AppDomain 中都需要使用到的。
接着 SystemDomain::Attach 会调用 SystemDomain::CreateDefaultDomain 函数(vmappdomain.cpp:2522)构造缺省的 AppDomain,也就是前面试验中的 "Domain 0",用作载入用户指定 Assembly 执行。此函数只是简单地调用 SystemDomain::NewDomain 函数以非 Managed 方式构造新的 AppDomain 实例;然后将此 AppDomain 设置为缺省的 AppDomain。
以下内容为程序代码:
HRESULT SystemDomain::CreateDefaultDomain()
{
HRESULT hr = S_OK;
// 防止多次初始化
if (m_pDefaultDomain != NULL)
return S_OK;
// 以非 Managed 方式构造新的 AppDomain 实例
AppDomain* pDomain = NULL;
if (FAILED(hr = NewDomain(&pDomain)))
return hr;
// 将此 AppDomain 设置为缺省的 AppDomain
pDomain->GetSecurityDescriptor()->SetDefaultAppDomainProperty();
m_pDefaultDomain = pDomain;
// ...
}
SystemDomain::NewDomain 函数(vmappdomain.cpp:2480)比较简单,构造 AppDomain 实例后,通知此 AppDomain 其载入的 Assembly;最后会调用 AppDomain::SetupSharedStatics 函数(vmappdomain.cpp:4583) 构造并初始化一个内部类 System.SharedStatics。这个类被用于生成全局唯一的 GUID,在诸如 System.Runtime.Remoting.Identity.ProcessIDGuid 以及安全相关类型中被用到。
在 SystemDomain::Attach 函数的末尾,会调用 SharedDomain::Attach 函数构造并初始化 SharedDomain。此 SharedDomain 负责载入 Appdomain-neutral 的共享 Assembly。我以前写的一篇文章《.Net平台下CLR程序载入原理分析》中讨论了载入 Assembly 进行共享的策略,有兴趣的朋友可以仔细看看,这儿摘抄一段:
以下为引用:
以下三个参数用于指定配件载入优化策略.我们等会详细讨论.
STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN = 0x1 << 1,
STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN = 0x2 << 1,
STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST = 0x3 << 1,
...
CLR在执行一个配件时,会新建一个应用域,将此配件放入新的应用域.如果多个应用域同时使用到一个配件,就要涉及到前面提到的配件载入优化策略了.最简单的方法是使用STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN标志,每个应用域拥有一份独立的配件的镜像,这样速度最快,管理最方便,但占用内存较多.相对的是所有应用域共享一份配件的镜像,(使用STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN标志)这样节约内存,但在此配件中存在静态变量等数据时,因为要保证每个应用域有独立的数据,所以会一定程度上影响效率.折中的方案是使用(使用STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST标志)此时,只有那些有Strong Name的配件才会被多个应用域共享.
SharedDomain::Attach 函数(vmappdomain.cpp:6440)的实现比较简单,与 SystemDomain::Attach 类似,其也是在 g_pSharedDomainMemory 分配的全局静态内存区构造 SharedDomain 对象,并调用 SharedDomain::Init 函数初始化之。而 SharedDomain::Init 函数(vmappdomain.cpp:6475)则首先调用基类的初始化函数 BaseDomain::Init,然后初始化 Assembly 映射表。
在完成 SystemDomain::Attach 函数调用和异常等初始化工作后,EEStartup 函数会调用 SystemDomain::Init 函数完成 SystemDomain 的初始化工作。
SystemDomain::Init 函数(vmappdomain.cpp:1074)首先初始化 fusion 系统关闭回调函数;然后获取 Windows 系统目录等配置信息;接着分别完成最重要的三项工作:载入 BCL 库所在 Assembly (mscorlib.dll);构造预分配异常对象;构造并初始化全局字符串常量表。
SystemDomain::LoadBaseSystemClasses 函数(vmappdomain.cpp:1263)首先调用 SystemDomain::LoadSystemAssembly 函数载入 mscorlib.dll;然后通过 Binder::StartupMscorlib 函数间接调用 g_Mscorlib.Init (Binder::Init) 完成 mscorlib 的初始化工作;最后从 mscorlib 中载入常用的一些类型,如g_pValueTypeClass、g_pArrayClass等等。
SystemDomain::CreatePreallocatedExceptions 函数(vmappdomain.cpp:1019)则使用刚刚获取的类型定义,构造预分配的三个异常对象:OutOfMemoryException、StackOverflowException 和 ExecutionEngineException。因为这三种异常被引发的时候,CLR 堆栈和堆可能已经被破坏或溢出,不能再通过传统的内存分配方式进行构造。而 .NET Framework 2.0 中对此类问题更是进一步提出了 CER(Constrained Execution Regions)等概念,确保局部构造的确定性等等。有兴趣的朋友可以参考我另外一篇文章《Finalization [2] Whidbey 中的改进》
对全局字符串常量表的初始化就比较简单,实际上是初始化了一个以字符串Hash值为键,以字符串为值的全局 HashMap。用于优化字符串性能,保障跨 AppDomain 字符串传递的高效率等等。有兴趣的朋友可以参考我另外一篇文章《CLR中字符串不变性的优化》。
至此,CLR 在运行用户程序之前,启动 System Domain、Shared Domain 和 Default Domain 的流程基本上已经介绍完毕,下一节将介绍这三者如何搭配使用,使 CLR 在运行时能够在空间和效率上达到最优。