天天看點

深入了解CLR類加載機制

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 使用顯式的CODEBASE裝載一個程式集

1

2

3

4

5

6

7

8

9

<code>using</code> <code>System;</code>

<code>using</code> <code>System.Reflection;</code>

<code>public</code> <code>class</code> <code>Utilities {</code>

<code>  </code><code>public</code> <code>static</code> <code>Object LoadCustomerType() {</code>

<code>    </code><code>Assembly a = Assembly.LoadFrom(</code>

<code>                    </code><code>"file://C:/usr/bin/xyzzy.dll"</code><code>);</code>

<code>    </code><code>return</code> <code>a.CreateInstance(</code><code>"AcmeCorp.LOB.Customer"</code><code>);</code>

<code>  </code><code>}</code>

<code>}</code>

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

圖2.9程式集解析和裝載

深入了解CLR類加載機制

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

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

10

11

<code>    </code><code>Assembly a = Assembly.Load(</code>

<code>      </code><code>"xyzzy, Version=1.2.3.4, "</code> <code>+</code>

<code>      </code><code>"Culture=neutral, PublicKeyToken=9a33f27632997fcc"</code><code>);</code>

程式集解析器由應用任何有效的版本政策開始解析。版本政策用來使程式集解析器将請求的程式集重新指向另一個版本。一個版本政策可以映射給定程式集的一個或多個版本到另一個版本。然而,一個版本政策不能将解析器重定向到一個名字不同的程式集。注意到版本政策僅用于那些完全由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 程式集解析器配置節點

深入了解CLR類加載機制

表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 設定版本政策

12

13

14

15

16

17

18

19

<code>&lt;?</code><code>xml</code> <code>version="1.0" ?&gt;</code>

<code>&lt;</code><code>configuration</code> <code>xmlns:asm="urn:schemas-microsoft-com:asm.v1"&gt;</code>

<code>  </code><code>&lt;</code><code>runtime</code><code>&gt;</code>

<code>    </code><code>&lt;</code><code>asm:assemblyBinding</code><code>&gt;</code>

<code>&lt;!-- one dependentAssembly per unique assembly name --&gt;</code>

<code>      </code><code>&lt;</code><code>asm:dependentAssembly</code><code>&gt;</code>

<code>        </code><code>&lt;</code><code>asm:assemblyIdentity</code>

<code>           </code><code>name="Acme.HealthCare"</code>

<code>           </code><code>publicKeyToken="38218fe715288aac" /&gt;</code>

<code>&lt;!-- one bindingRedirect per redirection --&gt;</code>

<code>        </code><code>&lt;</code><code>asm:bindingRedirect</code> <code>oldVersion="1.2.3.4"</code>

<code>                      </code><code>newVersion="1.3.0.0" /&gt;</code>

<code>        </code><code>&lt;</code><code>asm:bindingRedirect</code> <code>oldVersion="1-1.2.3.399"</code>

<code>                      </code><code>newVersion="1.2.3.7" /&gt;</code>

<code>      </code><code>&lt;/</code><code>asm:dependentAssembly</code><code>&gt;</code>

<code>    </code><code>&lt;/</code><code>asm:assemblyBinding</code><code>&gt;</code>

<code>  </code><code>&lt;/</code><code>runtime</code><code>&gt;</code>

<code>&lt;/</code><code>configuration</code><code>&gt;</code>

版本政策可以從三個級别來指定:每一個應用,每一個組建和每一台機器。每一個基本都有機會來處理版本編号,它使用一個級别的結果作為相鄰基本的輸入進行處理。如圖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 版本政策

深入了解CLR類加載機制

除了應用程式相關和機器端的配置設定外,一個給定的程式集還有一個釋出商政策。一個釋出商政策是元件開發者用于指定元件的哪一版本與另一相容的描述。

釋出商政策作為配置檔案存儲在機器端的全局程式集緩存。這些檔案的結構與應用程式和機器端配置檔案的結構完全相同。然而,為了在用于的機器安裝,釋出商政策配置檔案必須作為一個自定義資源包裝成一個程式集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 設定應用程式為安全模式

<code>&lt;</code><code>configuration</code> <code>xmlns:rt="urn:schemas-microsoft-com:asm.v1"&gt;</code>

<code>    </code><code>&lt;</code><code>rt:assemblyBinding</code><code>&gt;</code>

<code>      </code><code>&lt;</code><code>rt:publisherPolicy</code> <code>apply="no" /&gt;</code>

<code>    </code><code>&lt;/</code><code>rt:assemblyBinding</code><code>&gt;</code>

2 将名稱解析為位置

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

<code>&lt;</code><code>configuration</code><code>&gt;</code>

<code>    </code><code>&lt;</code><code>developmentMode</code> <code>developerInstallation="true" /&gt;</code>

因為DEVPATH環境變量并不用于部署,以下小節将忽略其存在。

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

圖2.12 程式集解析

深入了解CLR類加載機制

為了避免歧義,程式集解析器僅當請求的程式集包含公鑰時查詢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

en

a1x\bb\yourcode.dll

1.0.1.8

vv\a0\yourcode.dll

libzero

1.1.0.0

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指令,程式集解析器必須啟動為查找一個比對請求的程式集的潛在的昂貴的處理過程。

<code>&lt;!-- one codeBase per version --&gt;</code>

<code>        </code><code>&lt;</code><code>asm:codeBase</code>

<code>           </code><code>version="1.2.3.4"</code>

<code>           </code><code>href="file://C:/acmestuff/Acme.HealthCare.DLL"/&gt;</code>

<code>           </code><code>version="1.3.0.0"</code>

<code>           </code><code>href="http://www.acme.com/Acme.HealthCare.DLL"/&gt;</code>

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

圖2.13 APPBASE和相對搜尋路徑

深入了解CLR類加載機制

表2.7 設定相對搜尋路徑

<code>      </code><code>&lt;</code><code>asm:probing</code> <code>privatePath="shared;common"  /&gt;</code>

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

圖2.14 文化(Culture)中立探測

深入了解CLR類加載機制

如果程式集引用包含一個文化辨別,那麼探測将稍微複雜。如圖2.15,前面的算法将通過查找與請求的文化比對的子目錄進行擴充。一般來講,應用程式應該是搜尋路徑盡可能小以避免過多的加載時間的延遲。

圖2.15 依賴文化的探測

深入了解CLR類加載機制

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

本文轉自道法自然部落格園部落格,原文連結:http://www.cnblogs.com/baihmpgy/archive/2013/02/27/CLR_Loader_And_OSGi.html,如需轉載請自行聯系原作者