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,你可以查看這本書,最好看英文版的。