深入理解CLR類載入機制

來源:互聯網
上載者:User

1 CLR載入器

CLR載入器負責裝載和初始化程式集、模組、資源和類型。CLR載入器載入儘可能少的這些資源。不像Win32載入器,CLR載入器不會解析和自動載入子模組或程式集。相反,子模組只有當它們真正需要的時候,才進行載入。這不僅縮短了程式初始化時間,而且減少了運行程式消耗的資源。

在CLR,載入一般是基於類型且由JIT觸發。當JIT編譯器嘗試將一個方法從通用中間語言編譯成機器碼,它需要使用聲明的類型的類型定義和該類型的欄位定義。此外,JIT編譯器還需要使用由任何被JIT正在編譯的方法的本地變數或參數使用的類型定義。裝載一個類型,意味著裝載包含類型定義的程式集和模組。

按需裝載類型的策略,意味著程式中那些沒有被使用的部分代碼將從不被裝載到記憶體。它也意味著一個運行中的應用程式將經常搜尋新的被載入的新的程式集和模組,這些程式集和模組是在執行過程中隨時間的推移包含了需要的類型的那些檔案。如果這不是你需要的功能,你有兩個選擇。一個選擇是簡單的聲明那些類型的隱藏欄位,這些類型是你想要確保當你的類型被載入時需要一塊載入的。另一個選擇是顯式的使用載入器。

載入器通常是根據你的行為隱式的執行功能。開發人員可以通過程式集載入器顯式的使用載入器。程式集載入器通過在System.Reflection.Assembly類的LoadFrom靜態方法暴露給開發人員的。這個方法接受一個CODEBASE字串,它可以是一個檔案系統路徑或者一個標識在資訊清單包含的模組的URL。如果指定的檔案不存在,則裝載器將拋出一個System.FileNotFoundException一場。如果指定的檔案存在但不是一個包含在資訊清單的CLR模組,裝載器將拋出一個System.BadImageFormatException一場。最後,如果CODEBASE是一個使用一個非“file:”Scheme的URL,那麼調用者必須具有WebPermission的存取權限,否則一個System.SecurityException異常將被拋出。此外,使用不是“file:”協議的URLs的程式集將在載入之前被載入到本地download緩衝。

表2.2顯示一個簡單的C#程式,這個程式從file://C:/user/bin/xyzzy.dll載入一個程式集,然後建立一個包含AcmeCorp.LOB.Customer的類型的執行個體。調用者提供的只是程式集的實體路徑。當一個程式以這種方式使用程式集載入器,那麼CLR將忽略程式集由4部分組成的名字,包括版本編號。

表2.2 使用顯式的CODEBASE裝載一個程式集

using System;using System.Reflection;public class Utilities {  public static Object LoadCustomerType() {    Assembly a = Assembly.LoadFrom(                    "file://C:/usr/bin/xyzzy.dll");    return a.CreateInstance("AcmeCorp.LOB.Customer");  }}

雖然使用路徑裝載程式集有點意思,不過大多數程式集是利用程式集解析器使用名稱載入。程式集解析器使用由4部分組成的名稱來確定哪個檔案被程式集載入器載入到記憶體。2.9所示,這個名字到路徑的解析過程考慮了一序列的因素,包括宿主的應用程式的路徑,版本原則和其它詳細的配置。

圖2.9程式集解析和裝載

程式集解析器通過System.Reflection.Assembly類的靜態方法Load來暴露給開發人員。如表2.3所示,這個方法接受一個由4部分組成的名字(可以是一個字串,或者是一個AssemblyName引用),且它從表面上看和LoadFrom方法相似,他們都由程式集載入器暴露的。實際上,二者的相似是膚淺的,因為Load方法將首先使用程式集解析器使用一序列相當複雜的操作尋找一個合適的檔案。這些操作的第一個是使用一個版本原則來精確的確定期待被裝載的程式集的版本。

表2.3 使用程式集解析器裝載一個程式集

using System;using System.Reflection;public class Utilities {  public static Object LoadCustomerType() {    Assembly a = Assembly.Load(      "xyzzy, Version=1.2.3.4, " +      "Culture=neutral, PublicKeyToken=9a33f27632997fcc");    return a.CreateInstance("AcmeCorp.LOB.Customer");  }}

程式集解析器由應用任何有效版本原則開始解析。版本原則用來使程式集解析器將請求的程式集重新指向另一個版本。一個版本原則可以映射給定程式集的一個或多個版本到另一個版本。然而,一個版本原則不能將解析器重新導向到一個名字不同的程式集。注意到版本原則僅用於那些完全由4個部分指定的程式集是很重要的。如果程式集名稱僅指定一部分(如公開金鑰、版本或文化丟失),那麼將不應用版本原則。同時,如果直接調用Assembly.LoadFrom來繞開程式集解析器,那麼也不會應用版本原則,因為你只是指定一個實體路徑而不是一個程式集名稱。

版本原則通過設定檔指定。這包括一個機器端設定檔和一個應用程式相關的設定檔。機器端設定檔名字總是為machine.config,它的位置在%SystemRoot%\Microsoft .NET \Framework\V1.0.nnnn\CONFIG檔案夾。應用程式集相關的設定檔總是在程式的APPBASE檔案夾。對於基於CLR的.EXE程式,APPBASE是裝載的主執行程式的路徑的URI。對於ASP.NET引用,APPBASE是Web應用程式的虛擬路徑的跟路徑。基於CLR的.EXE程式的設定檔的名字總是為可執行檔名稱加上“.config”尾碼。比如,如果啟動並執行CLR程式是C:\myapp\app.exe,其對應的設定檔將是C:\myapp\app.exe.config。對於ASP.NET應用程式,設定檔總是為web.config。

設定檔是基於XML格式,且總是有一個configuration根節點。設定檔由程式集解析器、遠程調用基礎設施和ASP.NET使用。圖2.10顯示了用於配置程式集解析器的節點的基本結構。所有相關的節點都是在基於urn:schemas-microsoft-com:asm.v1名稱空間的assemblyBinding節點。它還有控制探測路徑和發布商版本模式的設定。此外,dependentAssembly節點用於指定每一個依賴的程式集的版本和位置。

圖2.10 程式集解析器配置節點

表2.4顯示了一個簡單的設定檔,它包含了一個程式集的兩個版本原則。第一個策略將版本1.2.3.4的程式集Acme.HealthCare重新導向到1.3.0.0。第二個策略將1.0.0.0到1.2.3.399版本重新導向到1.2.3.7。

表2.4 設定版本原則

<?xml version="1.0" ?><configuration xmlns:asm="urn:schemas-microsoft-com:asm.v1">  <runtime>    <asm:assemblyBinding><!-- one dependentAssembly per unique assembly name -->      <asm:dependentAssembly>        <asm:assemblyIdentity           name="Acme.HealthCare"           publicKeyToken="38218fe715288aac" /><!-- one bindingRedirect per redirection -->        <asm:bindingRedirect oldVersion="1.2.3.4"                      newVersion="1.3.0.0" />        <asm:bindingRedirect oldVersion="1-1.2.3.399"                      newVersion="1.2.3.7" />      </asm:dependentAssembly>    </asm:assemblyBinding>  </runtime></configuration>

版本原則可以從三個層級來指定:每一個應用,每一個組建和每一台機器。每一個基本都有機會來處理版本編號,它使用一個層級的結果作為相鄰基本的輸入進行處理。2.11所示。需要注意的是如果應用程式和機器的設定檔都有指定程式集的一個版本原則,那麼應用程式的策略將先執行,然後產生的版本編號將在程式端的策略執行,最終產生實際的版本編號用於定位程式集。在這個例子,如果機器端設定檔重新導向Acme.HealthCare的1.3.0.0版本到2.0.0.0版本,那麼當請求1.2.3.4版本時程式集解析器將使用2.0.0.0版本,因為應用程式的版本原則映射1.2.3.4版本到1.3.0.0版本。

圖2.11 版本原則

除了應用程式相關和機器端的配置設定外,一個給定的程式集還有一個發布商策略。一個發布商策略是組件開發人員用於指定組件的哪一版本與另一相容的描述。

發布商策略作為設定檔儲存在機器端的全域組件快取。這些檔案的結構與應用程式和機器端設定檔的結構完全相同。然而,為了在用於的機器安裝,發布商策略設定檔必須作為一個自訂資源套件裝成一個程式集DLL。假設foo.config檔案包含發布商配置策略,以下命令將調用程式集連機器AL.exe並為AcmeCorp.Code 2.0版本建立一個合適的發布商策略程式集。

al.exe /link:foo.config

/out:policy.2.0.AcmeCorp.Code.dll

/keyf:pubpriv.snk

/v:2.0.0.0

發布商策略檔案遵循policy.major.minor.assmname.dll格式。由於該命名規範,一個給定的任一major.minor版本的程式集僅可以有一個發布商策略檔案。在這個例子,所有對主要版本2.0的AcmeCorp.Code的請求將通過策略檔案路由連結到policy.2.0.AcmeCorp.Code.dll。如果在GAC不存在該程式集,那麼就沒有發布商策略。2.11所示,發布商策略在應用程式相關版本原則之後使用,但比機器端版本原則之前。

考慮到版本化的組件固有的脆弱性,CLR允許開發人員在基於應用程式端配置關閉發布商版本原則。為了達到這個目的,開發人員必須使用設定檔的publisherPolicy節點。表2.5顯示了在簡單設定檔的這樣的節點。當這個節點有apply=”no”屬性時,應用程式的發布商策略將被忽略。當這個屬性被設定為apply=”yes”,或者根本沒有指定時,發布商策略將如描述的被使用。正2.10所示,publisherPolicy節點可以在應用程式端或一個基於程式集的程式集來啟動或禁止發布商策略。

表2.5 設定應用程式為安全模式

<?xml version="1.0" ?><configuration xmlns:rt="urn:schemas-microsoft-com:asm.v1">  <runtime>    <rt:assemblyBinding>      <rt:publisherPolicy apply="no" />    </rt:assemblyBinding>  </runtime></configuration>

2 將名稱解析為位置

當程式集解析器覺得了裝載哪一個版本的程式集之後,它必須定位一個合適的檔案來傳遞給底層的程式集載入器。CLR首先從DEVPATH作業系統環境變數指定的檔案夾尋找。這個環境變數一般在開發機器中沒有被設定。相反的,它僅給程式員使用,並用於允許從共用檔案目錄載入延遲簽名的程式集。此外,DEVPATH環境變數僅在以下XML設定檔節點存在machine.config時才被考慮。

<configuration>  <runtime>    <developmentMode developerInstallation="true" />  </runtime></configuration>

因為DEVPATH環境變數並不用於部署,以下小節將忽略其存在。

圖2.12顯示了程式集解析器為了尋找合適組件檔的整個過程。在正常的部署情境中,程式集解析器用於尋找一個程式集的第一位置是GAC。GAC是一個機器端的代碼緩衝,該緩衝包含了機器端使用的已經被安裝的程式集。GAC允許管理員來為所有應用程式安裝在每個機器一次程式集。為了避免系統崩潰,GAC僅接受那些具有有效簽名和公開金鑰的程式集。此外,GAC的項目僅能被管理員刪除,這阻止了非管理使用者來刪除和移動關鍵系統層級組件。

圖2.12 程式集解析

為了避免歧義,程式集解析器僅當請求的程式集包含公開金鑰時查詢GAC。這阻止了普通名字如utilities的請求來被錯誤的實現滿足。公開金鑰可以作為程式集引用來顯式的提供,或者Assembly.Load參數提供,或者通過設定檔qualifyAssembly配置節點隱式提供。

GAC由系統級組件(FUSION.DLL)控制,它在%WINNT%\Assembly檔案夾中提供緩衝。FUSION.DLL為你管理了這個目錄的層次並提供了基於由4部分組成的名字訪問隱藏檔的公用,如表2.4。雖然我們可以遍曆隱含的檔案夾,但是FUSION用於緩衝DLL的結構是確保隨著CLR演變進行變更的實現。相反,你必須使用GACUTIL.exe工具或一些其它基於FUSION API的工具與GAC互動。一個這樣的工具是SHFUSION.DLL,一個Window瀏覽器Shell擴充,它提供了與GAC互動的友好介面。

表2.4 全域組件快取

Name

Version

Culture

Public Key Token

Mangled Path

yourcode

1.0.1.3

de

89abcde...

t3s\e4\yourcode.dll

yourcode

1.0.1.3

en

89abcde...

a1x\bb\yourcode.dll

yourcode

1.0.1.8

en

89abcde...

vv\a0\yourcode.dll

libzero

1.1.0.0

en

89abcde...

ig\u\libzero.dll

如果程式集解析器在GAC不能找到請求的程式集,那麼程式集解析器將嘗試使用一個CODEBASE指令來訪問程式集。一個CODEBASE指令簡單的映射一個程式集名稱到一個檔案名稱或指定了包含在程式集的模組位置的URL。與版本原則相似,CODEBASE指令在應用程式和機器端設定檔中。表2.6顯示2個CODEBASE指令的設定檔。第一個指令映射版本為1.2.3.4的Acme.HealthCare程式集到C:\acmestuff\Acme.HealthCare.dll。第二個指令映射了版本為1.3.0.0的該程式集到http://www.acme.com/bin/Acme.HealthCare.dll。

假設一個CODEBASE指令提供了,程式集解析器將簡單的載入對應的組件檔,且程式集的載入處理就如一個程式集用一個顯式的CODEBASE使用Assembly.LoadFrom載入一樣。然而,如果沒有提供CODEBASE指令,程式集解析器必須啟動為尋找一個匹配請求的程式集的潛在的昂貴的處理過程。

<?xml version="1.0" ?><configuration xmlns:asm="urn:schemas-microsoft-com:asm.v1">  <runtime>    <asm:assemblyBinding><!-- one dependentAssembly per unique assembly name -->      <asm:dependentAssembly>        <asm:assemblyIdentity           name="Acme.HealthCare"           publicKeyToken="38218fe715288aac" /><!-- one codeBase per version -->        <asm:codeBase           version="1.2.3.4"           href="file://C:/acmestuff/Acme.HealthCare.DLL"/>        <asm:codeBase           version="1.3.0.0"           href="http://www.acme.com/Acme.HealthCare.DLL"/>      </asm:dependentAssembly>    </asm:assemblyBinding>  </runtime></configuration>

如果程式集解析器無法使用GAC或一個CODEBASE指令搜尋一個程式集,它通過相對與應用程式根路徑相對的一序列路徑執行搜尋。這個搜尋被稱為探測。探測僅在APPBASE目錄或其子目錄進行搜尋(APPBASE目錄是包含應用程式設定檔的目錄)。比如,給定2.13的目錄結構,只有m,common,shared和q有資格被探測。它意味著,程式集解析器僅探測顯式指定在設定檔的目錄。表2.7顯示了一個設定檔例子,它設定了相對目錄shared和common。所有APPBASE子目錄中沒有在設定檔配置將被探測過程排除。

圖2.13 APPBASE和相對搜尋路徑

表2.7 設定相對搜尋路徑

<?xml version="1.0" ?><configuration xmlns:asm="urn:schemas-microsoft-com:asm.v1">  <runtime>    <asm:assemblyBinding>      <asm:probing privatePath="shared;common"  />    </asm:assemblyBinding>  </runtime></configuration>

當探測一個程式集時,程式集解析器基於程式集的簡單名稱、將按照剛才所述的相對搜尋路徑和請求的程式集的Culture構建CODEBASE URLs。圖2.14示範了用於解析一個沒有指定Culture程式集引用的CODEBASE URLs的例子。在這個例子,程式集的簡單名稱是yourcode且相對搜尋路徑是shared和common目錄。程式集解析器首先在APPBASE目錄搜尋yourcode.dll檔案。如果沒有這個檔案,程式集解析器然後假設程式集是在一個相同名稱的目錄且在yourcode檔案夾尋找相同名稱的檔案。如果檔案還未找到,則探測過程將在相對路徑的每一個項目重複,直到yourcode.dll檔案找到。如果檔案找到,則探測停止。否則,探測過程繼續重複,不過這次會在相同路徑尋找yourcode.exe檔案。假設一個檔案找到,程式集解析器會驗證檔案匹配程式集引用指定的程式集名稱的所有屬性,然後裝載程式集。如果程式集名稱的一個屬性沒有與程式集引用屬性全部匹配,那麼Assembly.Load調用失敗。否則,程式集被載入並被使用。

圖2.14 文化(Culture)中立探測

如果程式集引用包含一個文化標識,那麼探測將稍微複雜。2.15,前面的演算法將通過尋找與請求的文化匹配的子目錄進行擴充。一般來講,應用程式應該是搜尋路徑儘可能小以避免過多的載入時間的延遲。

圖2.15 依賴文化的探測

3 版本危害

前面關於程式集解析器如何確定裝載哪一個版本的程式集主要是集中在CLR使用的機制。那沒有討論的地方是一個開發人員應該使用什麼策略來確定什麼時候、如何和為什麼將程式集版本化。考慮到在本次寫作描述的平台沒有上架,因此有點困難來描述基於難得的經驗所獲得的有效最佳實務。然而,通過洞悉CLR的知識並推斷一序列指導也是合理的。

注意到程式集是版本化的單元是很重要的。嘗試改變程式集的檔案而沒有更改版本編號很可能導致不可預料的問題。為此,該節剩下的部分將研究一下版本化,版本化僅考慮程式集作為一個整體而不是程式集的每個檔案。

什麼時候改變版本編號是一個有意思的問題。顯然,如果一個類型的公開契約發生更改,類型的程式集必須更改一個新的版本編號。否則,依賴一個版本的類型簽名的程式,當裝載了一個不同簽名的類型將,產生一個運行時一場。這意味著如果你添加一個public或protected的公開類型的成員,你必須更改這個類型程式集的版本。如果你更改了公用類型一個public或protected成員(比如添加一個方法參數、更改欄位的類型),你也需要一個新的程式集版本。這是絕對的原則。違背這些原則將導致不可預料的後果。

需要回答的更難的問題是與不會影響程式集類型的公開簽名的修飾有關的。比如,更改一個標記為private或internal的成員在只關心簽名匹配情況下被考慮為不會產生破壞行為的更改。因為在你程式集外,沒有代碼可以依賴private或internal成員,簽名不匹配在運行時不是問題因為它不會發生。不過,類型不符僅是冰山一角。

在每一個程式集的構建時更改版本編號是一個合理的理由,即使沒有公開可視的簽名被改變。一下事實將支援這種方法,那就是即使是一個看起來對一個方法無害的改變也可能對使用程式集的程式有難以琢磨但具有漣漪效應的影響。如果開發人員為程式集的每一個構建,使用一個唯一的版本編號,使用一個指定的構建的版本測試的代碼在部署時不會有異常。

針對程式集的每次構建有一個唯一的版本編號的爭論是,那些沒有針對新版本程式集重新編譯的程式不會具有“安全”的修複。這個論點並不合理,如果不考慮發布商策略檔案。為每次編譯使用唯一版本編號的開發人員擅長於提供發布商策略檔案,這些檔案描述了程式集向後相容的哪些版本的程式集。預設的,這給了低版本的使用者自動更新到新版本程式集。當程式集開發人員以為是錯誤時,每一個應用程式可以使用在設定檔中的publisherPolicy節點來禁用自動升級,從而大體上應用程式處於安全模式。

如前討論,CLR程式集解析器支援通過CODEBASE指令、私人探測路徑和GAC支援一個程式集多個版本並行安裝。這允許一個程式集的多個版本在檔案系統共存。然而,如果有不止一個版本的這些程式集被獨立的一些程式或單一程式在任一時刻載入到記憶體,事情會變得稍微無法預料。並存執行比並行安裝更加難以處理。

在記憶體中同時有多個版本的主要問題是,對於運行時,那些程式集包含的類型是截然不同的。也就是說,如果一個程式集包含一個名為Customer的類型,那麼當一個程式的兩個版本被載入,在記憶體中有兩個不同的類型,每一個有自己的唯一標識。這有有些很嚴重的副作用。其中之一,每一個類型有任意靜態欄位的拷貝。如果一個需要跟蹤一些共用狀態的類型與已經被載入的多個版本的類型互相獨立,它顯然不可以使用利用一個靜態欄位的解決方案。相反,開發人員需要時刻記住版本來重寫代碼且將狀態儲存在與版本無關的一個位置。一種方法是儲存共用狀態到運行時提供的位置,如ASP.NET Application對象。另一種方法是定義一個分開的類,這個類僅包含一個共用狀態的靜態欄位。開發人員可以把這種類型部署到單獨的程式集,這個程式集與版本無關,這樣可以確保對於一個應用程式僅有一份靜態欄位拷貝。

當版本化的類型作為方法的參數傳遞時,與並存執行有關的另一個問題將產生。如果方法的調用者和被調用者在載入哪一個程式集有不同的觀點時,調用者掉傳遞一個被調用者不認識的類型的參數。開發人員可以通過總是為所有公用方法定義無版本化的類型類解決這個問題。更重要的是,這些共用類型必須被部署到單獨的程式集,這些程式集沒有進行多版本化。

附:程式集的中繼資料有3個不同的屬性,以允許開發人員來指定在同一時刻是否允許程式集的多個版本被載入。如果這些屬性不存在,那麼程式集被假設為在所有情境可以並存執行(多版本並行)。Nonsidebysideappdomain屬性指定了每一個應用域只能載入這個程式集的一個版本。Nonsidebysideprocess屬性指定了每一個進程只能載入這個程式集的一個版本。Nonsidebysidemachine屬性指定了在每個機器只能一次性載入這個程式集的一個版本。

4 更加深入CLR

OSGi.NET外掛程式架構(:http://www.iopenworks.com/Products/SDKDownload)的構建要求對CLR理解較為深入,對CLR的深入理解有助於我們更好的理解OSGi.NET的外掛程式載入機制。關於CLR的知識,我們是從《Essential.NET,Volume I》這本書學習到的,並將重要的部分翻譯成中文供架構設計人員閱讀。如果你想更加深入理解CLR,你可以查看這本書,最好看英文版的。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.