用WinDbg探索CLR世界 [6] AppDomain 的創建過程

原文: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!"[img]/images/wink.gif[/img];
    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 在運行時能夠在空間和效率上達到最優。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章