寄宿(hosting)使任何應用程序都能利用clr的功能。特別要指出的是,它使現有應用程序至少能部分使用託管代碼編寫。另外,寄宿還爲應用程序提供了經過編程來進行自定義和擴展的能力。c++
容許可擴展性意味着第三方代碼可在你的進程中運行。在windows中將第三方dll加載到進程中意味着冒險。dll中的代碼很容易破壞應用程序的數據結構和代碼。dll還可能企圖利用應用程序的安全上下文來訪問它原本無權訪問的資源。clr的appDomain功能解決了全部這些問題。AppDomain容許第三方、不受信任的代碼在現有的進程中運行,而CLR保證數據結構、代碼和安全上下文不被濫用或破壞。程序員
程序員常常講寄宿和AppDomain與程序集的加載和反射一塊兒使用。這4種技術一塊兒使用,使clr稱爲一個功能及其豐富和強大的平臺。本章重點在於寄宿和AppDomain。web
.net在windows平臺的頂部運行。這意味着.net必須用windows能理解的技術來構建。首先,全部託管模塊和程序集文件都必須使用windows PE文件格式,並且要麼是windows EXE文件,要麼是Dll文件。sql
開發clr時,Microsoft實際是把它實現成包含在一個dll中的com服務區。也就是說,Microsoft爲clr定義了一個標準的com接口,併爲該接口和com服務器分配了guid。安裝.net時,表明clr的com服務器和其餘com服務器同樣在windows註冊表中註冊。數據庫
任何windows應用程序都能寄宿clr。但不要經過調用CoCreateInstance來建立clr com服務器的實例。相反,你的非託管宿主應該調用MetaHost.h文件中聲明的CLRCreateInstance函數。CLRCreateInstance函數在MSCorEE.dll文件中實現,該文件通常在system32目錄中。這個dll被人們親切地稱爲墊片(shim),它的工做是決定建立哪一個版本的clr:墊片dll自己不包含clr com服務器。編程
一臺機器能夠安裝多個版本的clr,但只有一個版本的MSCorEE.dll文件。機器上安裝的MSCorEE.dll是與機器上安裝的最新版本的clr一塊兒發佈的那個版本。windows
CLRCreateInstance函數可返回一個ICLRMetaHost接口。宿主應用程序可調用這個接口的GetRuntime函數,指定數組要建立的clr的版本。而後,墊片將所需版本的clr加載到宿主的進程中。跨域
默認狀況下,當一個託管的可執行文件啓動時,墊片會檢查可執行文件,提取當初生成和測試應用程序時使用的lcr的版本信息。但應用程序能夠在它的xml配置文件中設置requiredRuntime和supportedRuntime來覆蓋默認行爲。數組
GetRuntime函數返回指定非託管ICLRRuntimeInfo接口的指針。有個這個指針後,就能夠利用GetInterface方法獲取ICLRRuntimeHost接口。宿主應用程序可調用該接口定義的方法作以下事情安全
1 設置宿主管理器
2 獲取clr管理器
3 初始化並啓動clr
4 加載程序集並執行其中的代碼
5 中止clr,阻止任何更多的託管代碼在windows進程中運行
注意:一個clr加載到winows進程以後,變永遠不能卸載;clr從進程卸載的惟一途徑就是終止進程,這會形成windows清理進程使用的全部資源。
CLR COM服務器服務器初始化時會建立一個AppDomain。AppDomain是一組程序集的邏輯容器
CLR初始化時建立的第一個AppDomain稱爲默認AppDomain,這個默認的AppDomain還有在windows進程終止時纔會被註銷。
除了默認AppDomain,正在使用非託管com接口方法或託管類型方法的宿主還可要求clr建立額外的AppDomain。AppDomain是爲了提供隔離而設計的。下面總結了AppDomain的具體功能。
1 一個AppDomain的代碼不能直接訪問另外一個AppDomain的代碼建立的對象
一個AppDomain中的代碼建立了一個對象後,該對象便被該AppDomain擁有。換言之,它的生存期不能超過建立它的代碼所在的AppDomain。一個AppDomain中的代碼要訪問另外一個AppDomain中的對象,只能使用按引用封送或者按值封送的語義。這就強制創建了清晰的分隔和邊界,由於一個AppDomain中的代碼不能直接引用另外一個AppDomain中的代碼建立的對象。這種隔離使得AppDomain能很容易地從進程中卸載,不會影響其餘AppDomain正在運行的代碼。
2 AppDomain能夠卸載
CLR不支持從AppDomain中卸載特定的程序集。但能夠告訴clr卸載一個AppDomain,從而卸載該AppDomain當前包含的全部程序集。
3 AppDomain能夠單獨保護
AppDomain建立後會應用一個權限集,它決定了向這個AppDomain中運行的程序集授予的最大權限。正式因爲存在這些權限,因此當宿主加載一些代碼後,能夠保證這些代碼不會破壞(或讀取)宿主自己使用的一些重要數據結構。
4 AppDomain能夠單獨配置
AppDomain建立後會管理一組配置設置,這些設置主要影響clr在AppDomain中加載程序集的方式。涉及搜索路勁、版本綁定重定向、劵影賦值以及加載器優化。
提示:windows的一個重要特點就是讓每一個應用程序都在本身的進程地址空間中運行。這就保證了一個應用程序的代碼不能訪問另外一個應用程序使用的代碼或數據。進程隔離可防範安全漏洞、數據破壞和其餘不可預測的行爲,確保了windows系統以及在它上面運行的應用程序的健壯性。遺憾的是,在windows中建立進程的開銷很大。win32 createProcess函數的速度很慢,並且windows須要大量內存來虛擬化進程的地址空間。可是,若是應用程序徹底由託管代碼構成,同時這些代碼沒有調用非託管代碼,那麼在一個windows進程中運行多個託管應用程序是沒有問題的。AppDomain提供了保護、配置和終止其中每個應用程序所需的隔離。
圖22-1演示了一個windows進程,其中運行着一個CLR COM服務器。該CLR當前管理着兩個AppDomain(雖然在一個windows進程中能夠運行的AppDomain數量沒有硬性限制)。每一個AppDomain都有本身的loader堆,每一個loader堆都記錄了自AppDomain建立以來訪問過哪些類型。Loader堆中的每一個類型對象都有一個方法表,方法表中的每一個記錄項都指向jit編譯的本機代碼(前提是方法至少執行過一次)。
除此以外,每一個AppDomain都加載了一些程序集。AppDomain #1(默認AppDomain)有三個程序集:myApp.exe,TypeLib.dll和System.dll。AppDomain#2有兩個程序集Wintellect.dll和system.dll。
兩個AppDomain都加載了system.dll程序集。若是這兩個AppDomain都使用來自system.dll的一個類型,那麼兩個AppDomain的loader堆會爲相同的類型分別分配一個類型對象:類型對象的內存不會由兩個AppDomain共享。另外,一個AppDomain中的代碼調用一個類型定義的方法時,方法il代碼會進行jit編譯,生成的本機代碼單獨與每一個AppDomain關聯,而不是由調用它的全部AppDomain共享。
不共享類型對象的內存或本機代碼顯得有些浪費。但AppDomain的設計宗旨就是提供隔離:clr要求在卸載某個AppDomain並釋放其全部資源時不會影響到其餘任何AppDomain。複製clr的數據結構才能保證這一點。另外,還保證多個AppDomain使用的類型在每一個AppDomain中都有一組靜態字段。
有的程序集原本就要有多個AppDomain使用。最典型的例子就是MSCorLib.dll。該程序集包含了system.object,system.int32以及其餘全部.net密不可分的類型。clr初始化時,該程序集會自動加載,並且全部AppDomain都共享該程序集中的類型。爲了減小資源消耗,MSCorLib程序集以一種AppDomain中立的方式加載。也就是說,針對以AppDomain中立方式加載的程序集,clr會爲他們維護一個特殊的loader堆。該loader對中的全部類型對象,以及爲這些類型定義的方法jit編譯生成的全部本機代碼,都會由進程中全部AppDomain共享。遺憾的是,共享這些資源所得到的收益並非沒有代價,這個代價就是,以AppDomain中立方式加載的全部程序集永遠不能卸載。要回收他們佔用的資源,惟一的辦法就是終止windows進程,讓windows去回收資源。
一個線程能執行一個AppDomain中的代碼,再執行另外一個AppDomain的代碼。Thread.GetDomain()方法向CLR詢問它正在執行哪一個AppDomain。AppDomain的FriendlyName屬性獲取AppDomain的友好名稱(默認AppDomain使用可執行文件的名稱做爲友好名稱)
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Remoting; using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ConsoleApplication7 { class Program { static void Main(string[] args) { Marshalling(); } private static void Marshalling() { //獲取AppDomain引用(「調用線程」當前正在該AppDomain中執行) AppDomain adCallingThreadDomain = Thread.GetDomain(); //每一個AppDomain都分配了友好字符串名稱(以便調試) //獲取這個AppDomain的友好名稱並顯示它 String CallingDomainName = adCallingThreadDomain.FriendlyName; Console.WriteLine("默認AppDomain友好的名稱={0}",adCallingThreadDomain); //獲取並顯示咱們的AppDomain中包含了「Main」方法的程序集 String exeAssembly = Assembly.GetEntryAssembly().FullName; Console.WriteLine("包含「Main」方法的程序集={0}", exeAssembly); //定義局部變量來引用一個AppDomain AppDomain ad2 = null; //************************************************************************************************************ //************************************************************ DEMO 1:使用「按引用封送」進行跨AppDomain通訊 *** //************************************************************************************************************ Console.WriteLine("{0} Demo1 按引用封送", Environment.NewLine); //新建一個AppDomain(從當前AppDomain繼承安全性和配置) ad2 = AppDomain.CreateDomain("AD #2", null, null); MarshalByRefType mbrt = null; //將咱們的程序集加載到新AppDomain,構造一個對象,把它封送回咱們的AppDomain(實際獲得對一個代理的引用) mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApplication7.MarshalByRefType"); Console.WriteLine("Type={0}", mbrt.GetType());//CLR在類型上撒謊了 //證實獲得的是對一個代理對象的引用 Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbrt)); //看起來像是在MarshalByRefType上調用了一個方法,實則否則。 //咱們是在代理類型上調用了一個方法,代理是線程切換到擁有對象的那個 //AppDomain,並在真實的對象上調用這個方法 mbrt.SomeMethod(); //卸載新的AppDomain AppDomain.Unload(ad2); //此時,mbrt引用了一個有效的代理對象;代理對象引用一個無效的AppDomain try { mbrt.SomeMethod(); Console.WriteLine("調用成功"); } catch (AppDomainUnloadedException) { Console.WriteLine("調用失敗,AppDomain被卸載了"); } //************************************************************************************************************ //************************************************************ DEMO 2:使用「按值封送」進行跨AppDomain通訊 *** //************************************************************************************************************ Console.WriteLine("{0} Demo2 按值封送", Environment.NewLine); ad2 = AppDomain.CreateDomain("AD #2", null, null); mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApplication7.MarshalByRefType"); //對象的方法返回所返回對象的副本 //對象按值(而非按引用)封送 MarshalByValType mbvt= mbrt.MethodWithReturn(); //證實獲得的是對一個代理對象的引用 Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbvt)); //看起來在MarshalByValType上調用一個方法,實際也是如此 Console.WriteLine("Return object created " + mbvt.ToString()); //卸載新的AppDomain AppDomain.Unload(ad2); //此時,mbrt引用了一個有效的x代理對象;代理對象引用一個無效的AppDomain try { //卸載AppDomain以後調用mbvt方法不會拋出異常 Console.WriteLine("Return object created " + mbvt.ToString()); Console.WriteLine("調用成功"); } catch (AppDomainUnloadedException) { Console.WriteLine("調用失敗,AppDomain被卸載了"); } //************************************************************************************************************ //************************************************************ DEMO 3:使用不可封送的類型進行跨AppDomain通訊 *** //************************************************************************************************************ ad2 = AppDomain.CreateDomain("AD #2", null, null); mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApplication7.MarshalByRefType"); try { NonMarshalableType nmt = mbrt.MethodArgAndReturn(CallingDomainName);//拋出異常:未標記爲可序列化 } catch (SerializationException) { Console.WriteLine("拋出異常:未標記爲可序列化"); } Console.ReadKey(); } } //該類型的實例可跨越AppDomain的邊界「按引用封送」 public sealed class MarshalByRefType : MarshalByRefObject { public MarshalByRefType() { Console.WriteLine("{0} ctor running in {1}", GetType(), Thread.GetDomain().FriendlyName); } public void SomeMethod() { Console.WriteLine("Executing in " + Thread.GetDomain().FriendlyName); } public MarshalByValType MethodWithReturn() { Console.WriteLine("Execute in " + Thread.GetDomain().FriendlyName); MarshalByValType t = new MarshalByValType(); return t; } public NonMarshalableType MethodArgAndReturn(string callingDomainName) { //注意:callingDomainName是可序列化的 Console.WriteLine("Calling from '{0}' to '{1}'.", callingDomainName, Thread.GetDomain().FriendlyName); NonMarshalableType t=new NonMarshalableType(); return t; } } //該類的實例可跨越AppDomain的邊界「按值封送」 [Serializable] public sealed class MarshalByValType : Object { private DateTime m_creationTime = DateTime.Now;//注意:DateTime是可序列化的 public MarshalByValType() { Console.WriteLine("{0} ctor running in {1}, Created no {2:D}", GetType(), Thread.GetDomain().FriendlyName, m_creationTime); } public override string ToString() { return m_creationTime.ToLongDateString(); } } //該類的實例不能跨AppDomain邊界進行封送 //[Serializable] public sealed class NonMarshalableType : Object { public NonMarshalableType() { Console.WriteLine("Execute in " + Thread.GetDomain().FriendlyName); } } }
1,AppDomain.CreateDomain三個參數:
friendlyName:表明新AppDomain的友好名稱的一個String,
securityInfo:一個System.Security.Polict.Evidence,是CLR用於計算AppDomain權限集的證據。本例爲null,形成新的AppDomain從建立它的AppDomain繼承權限集。一般,若是但願圍繞AppDomain中的代碼建立安全邊界,可構造一個System.Security.PermssionSet對象,在其中添加但願的權限對象(實現了IPermission接口的類型實例),將獲得的PermssionSet對象引用傳給接受一個PermssionSet的CreateDomain方法
info:一個System.AppDomainSetup,表明CLR爲新AppDomain使用的配置設置。一樣,本例爲該參數傳遞爲null,是新的AppDomain從建立它的AppDomain繼承配置設置。若是但願對新AppDomain進行特殊配置,可構造一個AppDomainSetup對象,將它的各類屬性(例如置文件的名稱)設爲你但願的值,而後將獲得的AppDomainSetup對象引用傳給CreateDomain方法
2,AppDomain的CreateInstanceAndUnwrap內部實現
①CreateInstanceAndUnwrap方法致使調用線程從當前AppDomain切換到新的AppDomain
②線程將指定的程序集加載到新AppDomain中,並掃描程序集的類型定義元數據表,查找指定類型
③找到類型後,線程調用該類型的無參構造器(CreateInstanceAndUnwrap方法一些重載方法容許在調用類型的構造器時傳遞實參)
④如今線程又切換回默認的AppDomain,時CreateInstanceAndUnwrap能返回對新類型對象的引用
3,「按引用封送」的具體含義
當CreateInstanceAndUnwrapA線它封送的一個對象的類型派生自MarshalByRefObject時,CLR就會跨AppDomain邊界按引用封送
①源AppDomain想向目標AppDomain發送或返回對象引用時,CLR會在目標AppDomain的Loader堆中定義一個代理類型,代理類型是用原始類型的元數據定義的,因此它看起來和原始類型徹底同樣,有徹底同樣的實例成員(屬性、事件和方法)。實例字段不會成爲(代理)類型的一部分。代理類型肯定定義了幾個(本身的)實例字段,但這些字段和原始類型的不一致。相反,這些字段只是指出哪一個AppDomain「擁有」真實的對象,以及如何在擁有(對象的)AppDomain中找到真實的對象
②在目標AppDomain中定義好這個代理類型以後,CreateInstanceAndUnwrapA方法就會建立代理類型的實例,初始化它的字段來標識源AppDomain和真實對象,而後將這個代理對象的引用返回給目標AppDomain(調用該對象的GetType方法,他會向你撒謊)
③應用程序使用代理調用SomeMethod方法。因爲mbrt變量用代理對象,因此會調用由代理實現的SomeMethod。代理的實現利用代理對象中的信息字段,將調用線程從默認AppDomain切換至新AppDomain。如今,該線程的任何行動都在新AppDomain的安全策略和配置設置下運行。線程接着使用代理對象的GCHandle字段查找新AppDomain中的真實對象,並用真實對象調用真實的SomeMethod方法
④使用「按引用封送」的語義進行跨AppDomain邊界的對象訪問,會產生一些性能上的開銷。因此通常應該儘可能少用這個功能
CLR在目標AppDomain中精確的賦值了源對象。而後MethodWithReturn方法返回對這個副本的引用。源AppDomain中的對象和目標AppDomain中的對象有了獨立的生存期,它們的狀態也能夠獨立地更改
演示2與演示1很類似。和演示1同樣,演示2也建立了新AppDomain。而後調用CreateInstanceAndUnwrap方法將同一個程序集加載到新建AppDomain中,並在這個新AppDomain中建立MarshalByRefType類型的實例。CLR爲這個對象建立代理,mbrt變量(在默認AppDomain中)被初始化成引用這個代理。接着用代理調用MethodWithReturn方法。這個方法是無參的,將在新AppDomain種執行以建立MarshalByValType類型的實例,並將一個對象引用返回給默認AppDomain。
MarshalByValType不從system. MarshalByRefObject派生,因此clr不能定義一個代理類型並建立代理類型的實例:對象不能按引用跨AppDomain邊界進行封送。
但因爲MarshalByValType標記了自定義特性[Serializable],因此MethodWithReturn方法能按值封送對象。下面具體描述了將一個對象按值從一個AppDomain封送到另外一個AppDomain的含義。
源AppDomain想向目標AppDomain發送或返回一個對象引用時,clr將對象的實例字段序列化哼一個字節數組。字節數組從源AppDomain複製到目標AppDomain。而後,clr在模板AppDomain中發序列化字節數組,這會強制clr將定義了「被反序列化的類型」的程序集加載到目標AppDomain中。接着,clr建立類型的實例,並利用字節數組中的值初始化對象的字段,使之與源對象中的值相同。換言之,clr在目標AppDomain中精確複製了源對象。而後MethodWithReturn方法返回對這個副本的引用;這樣一來,對象就跨AppDomain的邊界按值封送了。
因爲NonMarshalableType不是從System. MarshalByRefObject中派生的,也沒有用[Serializable]定製特性進行標記,因此不容許MethodArgAndReturn按引用或按值封送對象--對象徹底不能跨越AppDomain邊界進行封
演示3與演示1和2類似。都是建立了新AppDomain。而後調用CreateInstanceAndUnwrap方法將同一個程序集加載到新建AppDomain中,並在這個新AppDomain中建立MarshalByRefType對象,並讓mbrt引用該對象的代理。
而後,我用代理調用接受一個實參的MethodArgAndReturn方法。一樣地,clr必須保持AppDomain的隔離,因此不能直接將對實參的引用傳給新AppDomain。若是對象的類型派生自MarshalByRefObject,clr會爲他建立代理並按引用封送。若是對象的類型用[Serializable]進行了標記,clr會將對象(及其子)序列化成一個字節數組,將字節數組封送到新AppDomain中,再將字節數組反序列化成對象圖,將對象圖的根傳給MethodArgAndReturn方法。
在這個特定的例子中,我跨越AppDomain邊界傳遞一個system.string對象。system.string類型不上從MarshalByRefObject派生的,因此clr不能建立代理。幸虧,system.string被標記爲[Serializable],因此clr能按值封送它,使代碼能正常工做。注意,對於string對象,clr會採起特殊的優化措施。跨越AppDomain邊界封送一個string對象時,clr只是跨越邊界傳遞對string對象的引用;不會真的生成string對象的副本。之因此能提供這個優化,是由於string對象是不可變的;因此,一個AppDomain中的代碼不可能破壞string對象的字段。
在MethodArgAndReturn內部,我顯示傳給它的字符串,證實字符串跨越了AppDomain邊界。而後,我建立NonMarshalableType類型的實例,並將對這個對象的引用返回至默認AppDomain。因爲NonMarshalableType不是從system.MarshalByRefObject派生的,也沒有用[Serializable]定製特性進行標記,因此不容許MethodArgAndReturn按引用或按值封送對象--—對象徹底不能跨域AppDomain邊界進行封送!爲了報告這個問題,MethodArgAndReturn在默認AppDomain中拋出一個SerializableException異常。因爲個人程序沒有捕捉這個異常,因此程序終止。
AppDomain很強大的一個地方就是能夠卸載它。卸載AppDomain會致使clr卸載AppDomain中的全部程序及,還會釋放AppDomain的loader堆。卸載AppDomain的辦法是調用AppDomain的靜態Unload方法。這致使clr執行一系列操做來得體地卸載指定AppDomain。
1 clr掛起進程中執行過託管代碼的全部線程。
2 CLR檢查全部線程棧,查看哪些線程正在執行要卸載的AppDomain中的代碼,或者哪些線性會在某個時候返回至要卸載的AppDomain。任何棧上有要卸載的AppDomain,CLR都會強制對應的線程拋出一個ThreadAbortException(同時恢復線程的執行)。這將致使線程展開,並執行遇到的全部finally塊以清理資源。若是沒有代碼捕捉ThreadAbortException,它最終會成爲未處理的異常,CLR會「吞噬」這個異常,線程會終止,但進程可繼續執行。這是很特別的一點,由於對於其餘全部未處理異常,clr都會終止進程。
3 當第2步發現的全部線程都離開AppDomain後,CLR遍歷堆,爲引用了「因爲卸載的AppDomain建立的對象」的每一個代理對象都設置一個標誌(flag)、這些代理對象如今知道他們引用的真實對象已經不存在了。如今,任何代碼在無效的代理對象上調用方法都會拋出AppDomainUnloadedException異常
4 CLR強制垃圾回收,回收由已卸載的AppDomain建立的任何對象的內存。這些對象的Finalze方法被調用,是對象由機會正確清理他們佔用的資源
5 CLR恢復剩餘全部線程的執行。調用AppDomain.Unload方法的線程將繼續運行;對於AppDomain.Unload的調用是同步進行的
AppDomain的幾條MonitoringEnabled屬性設置爲true顯式打開監控。打開監控後,代碼可查詢AppDomain類提供的如下4個屬性
①MonitoringSurvivedProcessMemorySize:這個Int64靜態屬性返回由當前CLR實例控制的全部AppDomain使用的字節數。這個數字值保證在上一次垃圾回收時時準確的
②MonitoringTotalAllocatedMemorySize:這個Int64實例屬性返回特定AppDomain已分配的字節數。這個數字只保證在上一次垃圾回收時是準確的
③MonitoringSuvivedMemorySize:這個Int64實例屬性返回特定AppDomain當前正在使用的字節數。這個數字只保證在上一次垃圾回收時是準確的
④MonitoringTotalProcessorTime:這個TimeSpan實例返回特定AppDomain的CPU佔用率
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApplication8 { class Program { static void Main(string[] args) { AppDomainResourceMonitoring(); Console.WriteLine(Environment.TickCount); Console.ReadLine(); } public static void AppDomainResourceMonitoring() { using (new AppDomainMonitorDalte(null)) { //分配在回收時能存活的約10MB var list = new List<object>(); for (int x = 0; x < 1000; x++) { list.Add(new byte[1000]); } //分配在回收時存活不了的約20MB for (int x = 0; x < 2000; x++) { new byte[10000].GetType(); } //保持CPU工做約5秒 var stop = Environment.TickCount + 5000; while (Environment.TickCount < stop) ; } } } public class AppDomainMonitorDalte : IDisposable { private AppDomain m_appdomain; private TimeSpan m_thisADCpu; private Int64 m_thisAdMemoryInUse; private Int64 m_thisADMemoryAllocated; static AppDomainMonitorDalte() { //肯定已打開AppDomain監視 AppDomain.MonitoringIsEnabled = true; } public AppDomainMonitorDalte(AppDomain ad) { m_appdomain = ad ?? AppDomain.CurrentDomain; m_thisADCpu = m_appdomain.MonitoringTotalProcessorTime; m_thisAdMemoryInUse = m_appdomain.MonitoringSurvivedMemorySize; m_thisADMemoryAllocated = m_appdomain.MonitoringTotalAllocatedMemorySize; } public void Dispose() { GC.Collect(); Console.WriteLine("AppDomain友好名稱={0},CPU={1}ms", m_appdomain.FriendlyName, (m_appdomain.MonitoringTotalProcessorTime - m_thisADCpu).TotalMilliseconds); Console.WriteLine("Allocated {0:N0} bytes of which {1:N0} survied GCs", m_appdomain.MonitoringTotalAllocatedMemorySize - m_thisADMemoryAllocated, m_appdomain.MonitoringSurvivedMemorySize - m_thisAdMemoryInUse); } } }
①異常首次拋出時,CLR調用向拋出異常的AppDomain登記的全部FirstChanceException回調方法。
②而後。CLR查找棧上同一個AppDomain中的任何catch塊,有一個catch塊能處理異常,則異常處理完成,將繼續執行
③若是AppDomain中沒有一個catch塊能處理異常,則CLR沿着棧向上來到調用AppDomain,再次拋出同一異常對象(序列化和反序列化以後)
④這時感受就像是拋出一個全新新的異常,CLR調用當前AppDomain登記的全部FirstChanceException回調方法
⑤這個過程會一直執行,直到抵達線程棧頂部。若是異常還未處理,則進程終止
前面已討論了宿主以及宿主加載clr的方式。同時還討論了宿主如何告訴clr建立和卸載AppDomain。下面將描述不一樣應用程序類型如何寄宿clr,以及如何管理AppDomain。
控制檯ui應用程序、nt service應用程序、windows窗體應用程序和windows presentation foundation(wpf)應用程序都是自寄宿(self-hosted,即本身容納clr)的應用程序,它們都有託管exe文件。windows用託管exe文件初始化進程時,會加載墊片。墊片檢查應用程序的程序集(exe文件)中的clr頭信息。頭信息指明瞭生成和測試應用程序時使用的clr版本。墊片根據這些信息決定將哪一個版本的clr加載到進程中,clr加載並初始化好以後,會再次檢查程序集clr頭,判斷哪一個方法是應用程序的入口方法(main)。clr調用該方法,此時應用程序才真正啓動並運行起來。
代碼運行時會訪問其餘類型。引用另外一個程序集中的類型時,clr會定位所需的程序集,並將其加載到同一個AppDomain中。應用程序的main方法返回後,windows進程終止(銷燬默認AppDomain和其餘全部AppDomain)
注意:要關閉windows進程(包括它全部AppDomain),可調用system.Environment的靜態方法Exit。Exit是終止進程最得體的方式,由於它首先調用託管堆上的全部對象的fimalize方法,再釋放clr容納的全部非託管com對象。最後,exit調用win32 ExitProcess函數。
ASP.NET做爲一個ISAPI DLL實現,客戶端首次請求有這個dll處理的url時,asp.net會加載clr。客戶端請求一個web應用程序時,ASP.NET判斷這是否是第一次請求。若是是,ASP.NET要求clr爲該web應用程序建立新AppDomain;每一個web應用程序都根據虛擬根目錄標識。而後,ASP.NET要求clr將包含應用程序全部公共類型的程序集加載到新AppDomain中,建立該類型的實例,並調用其中的方法相應客戶端的web請求。若是代碼引用了更多的類型,clr將所需的程序集加載到web應用程序的AppDomain中。
之後,若是客戶端請求已開始運行web應用程序,就再也不新建AppDomain了,而是使用現有的AppDomain,建立web應用程序的類型的新實例並開始調用方法。這些方法已jit編譯成本機代碼,因此後續客戶端請求的性能會比較出衆。
若是客戶端請求不一樣的web應用程序,ASP.NET會告訴clr建立新AppDomain。新AppDomain一般在和其餘AppDomain同樣的工做進程中建立。這意味着將有大量web應用程序在同一個windows進程中運行,這提高了系統的整體效率。一樣地,每一個web應用程序須要的程序集都加載到一個單獨的AppDomain中,以隔離不一樣web應用程序的代碼和對象。
ASP.NET的一個亮點是容許在不關閉web服務器的前提下動態更改網站代碼。網站的文件在硬盤上發生改動時,ASP.NET會檢測到這個狀況,並卸載包含舊版本文件的AppDomain(在當前運行的最後一個請求完成以後),並建立一個新AppDomain,向其中加載新版本文件。爲確保這個過程順利,ASP.NET使用了AppDomain的一個名爲影像複製功能
Microsoft sql server是非託管應用程序,它的大部分代碼還是用非託管c++寫的。sql server 容許開發人員使用託管代碼建立存儲過程。首次請求數據庫運行一個用託管代碼寫的存儲過程時,sql server會加載clr。存儲過程在它們本身的安全AppDomain中運行,這避免了存儲過程對數據庫服務器產生負面影響。
system.appDomainManager類容許宿主使用託管代碼(而不是非託管代碼)覆蓋clr的默認行爲。你惟一要作的就是定義本身的類,讓它從system.appDomainManager派生,重寫想要接手控制的任何虛方法。而後,在專用的程序集中生成類,並將程序集安裝到GAC中。
託管代碼出現錯誤時,宿主可告訴clr採起什麼行動。
1 若是線程執行時間過長,clr可終止線程並返回一個響應。
2 clr可卸載appDomain。這會終止線程並返回一個響應。
3 clr可被禁用。這會阻止更多的託管代碼在程序中運行,但仍容許非託管代碼運行。
4 clr 可退出windows進程。首先會終止全部線程,並卸載全部appDomain,使資源清理操做得以執行,而後纔會終止進程。
宿主應用程序通常都想保持對本身的線程的控制。以一個數據庫服務器爲例。當一個請求抵達數據庫服務器時,線程A得到請求,並將該請求派發給線程b以執行實際工做。線程B可能要執行並非由數據庫服務器的開發團隊建立和測試的代碼。例如,假定一個請求到達數據庫服務器,要執行由運行服務器的公司用託管代碼寫的存儲過程。數據庫服務器要求存儲過程在本身的AppDomain中運行,這個設計天然是極好的,由於能保障安全,防止存儲過程訪問其AppDomain外部的對象,還能防止代碼訪問不容許訪問的資源。
可是,若是存儲過程的代碼進入死循環怎麼辦?在這種狀況下,數據庫服務器把它的一個線程派發給存儲過程代碼,但這個線程一去不復返。這便將數據庫服務器置於一個危險的境地;服務器服務器誒將來的行爲變得不可預測了。例如,因爲線程進入死循環,因此服務器的性能可能變得很糟。服務器是否是應該建立更多的線程?這樣會消耗更多的資源,並且這些線程自己也可能進入死循環。
爲了解決這些問題,宿主可利用線程終止功能。下圖展現了旨在解決落跑(runway)線程的宿主應用程序的典型架構。
1 客戶端向服務器發送請求
2 服務器線程得到請求,把它派發給一個線程池線程來執行實際工做。
3 線程池線程得到客戶端的請求,執行由構建並測試宿主應用程序的那個公司寫的可信代碼
4 可信代碼進入一個try塊。從這個try塊中,跨越一個appDomain的邊界進行調用(經過派生自MarshalByRefObject的一個類型)。AppDomain中包含的是不可信代碼(多是存儲過程),這些代碼不是由製做宿主應用程序的的啊那個公司生成和測試的。在這個時候,服務器至關於把它的線程的控制權交給了一些不可信的代碼,服務器感到有點緊張了。
5 宿主會記錄接收到客戶端請求的時間。不可信代碼在管理員設定的時間內沒有對客戶端作出響應,宿主就會調用Thread的Abort方法要求clr終止線程池線程,強制它拋出一個ThreadAbortException。
6 這時,線程池線程開始展開(unwind),調用finally塊,使清理代碼得以執行。最後,線程池線程穿越AppDomain邊界返回。因爲宿主的存根代碼是從一個try塊中調用不可信代碼,因此宿主的存根代碼有一個catch塊捕捉ThreadAbortException。
7 爲了響應捕捉到的ThreadAbortException異常,宿主調用Thread的ResetAbort方法。
8 如今,宿主代碼已捕捉到ThreadAbortException異常。所以,宿主可向客戶端返回某種形式的錯誤,容許線程池線程返回線程池,供將來的客戶端請求使用。
澄清一下上述架構中容易被忽視的地方。首先,thread的Abort方法是異步的。調用Abort方法時,會在設置目標線程的AbortRequested標誌後當即返回。「運行時」檢測到一個線程要停止時,會嘗試將線程弄到一個安全地點(safe place)。若是「運行時」認爲能安全地中止線程正在作的事情,不會形成災難性後果,就說線程在安全地點。若是線程正在執行一個託管的阻塞,他就在一個安全地點。若是線程正在執行類型的類構造器、catch塊或者finally塊中的代碼、cer中的代碼或者非託管代碼,線程就不在安全地點。
線程到達安全地點後,「運行時」檢測到線程已設置了AbortRequested標誌。這致使線程拋出一個ThreadAbortException,若是該異常未被捕捉,異常就會成爲未處理的異常,全部掛起的finally塊將執行,線程得體地終止。和其餘全部異常不一樣,未處理的ThreadAbortException不會致使應用程序終止。「運行時」會悄悄地吞噬這個異常(僞裝它沒有發生),線程將死亡。當應用程序及其剩餘的全部線程都將繼續運行。
在本例中,宿主捕捉ThreadAbortException,容許宿主從新獲取該線程的控制權,並把它歸還到線程池中。但還有一個問題:宿主用什麼辦法阻止不可信代碼本身捕獲ThreadAbortException,從而保持宿主對線程的控制呢?答案是CLR以一種很是特殊的方法對待ThreadAbortException。即便代碼捕捉了ThreadAbortException,clr也不容許代碼悄悄地吞噬該異常。換言之,在catch塊的尾部,clr會自動從新拋出ThreadAbortException。
clr 的這個功能又引發另外一個問題:若是clr在catch塊的尾部從新拋出了ThreadAbortException異常,宿主如何捕捉它並從新獲取線程的控制權呢?宿主的catch塊中有一個對Thread的ResetAbort方法的調用。調用該方法會告訴clr在catch塊的尾部不要從新拋出ThreadAbortException異常。
這又引發了另外一個問題:宿主怎麼阻止不可信代碼本身捕捉ThreadAbortException並調用Thread的ResetAbort方法,從而保持宿主對線程的控制呢?答案是Thread的ResetAbort方法要求調用者被受權了SecurityPermission權限,並且其ControlThread標誌已被設置爲true。宿主爲不可信代碼建立AppDomain時,不會向其授予這個權限,因此不可信代碼不能保持對宿主的線程的控制權。
須要指出的是,這裏仍然存在一個潛在的漏洞:當線程從它的ThreadAbortException展開時,不可信代碼可執行catch塊和finally塊。在這些塊中,不可信代碼可能進入死循環,阻止宿主從新獲取線程的控制權。宿主應用程序經過設置一個升級策略來修正這個問題。要終止的線程在合理的時間內沒有完成,clr可將線程的終止方式升級成「粗魯」的線程終止、「粗魯」的AppDomain卸載、禁用clr甚至殺死整個進程。還要注意,不可信代碼可捕捉ThreadAbortException,並在catch塊中拋出其餘種類的一個異常。若是這個其餘的異常被捕捉到,clr會在catch塊的尾部自動從新拋出ThreadAbortException異常。
須要指出的是,大多數不可信的代碼實際並不是故意寫成惡意代碼:只是根據宿主的標準,它們的執行時間太長了一點。一般,catch塊和finally塊只包含及少許代碼,這些代碼能夠很快地執行,不會形成死循環,也不會執行耗時很長的任務。因此,宿主爲了從新獲取線程的控制權限,通常狀況都不會動用升級策略(開始各類各樣的「粗魯」行爲)。
順便說一句,thread類實際提供了兩個abort方法:一個無參;另外一個獲取一個object參數,容許傳遞任何東西進來。代碼捕捉到ThreadAbortException時,可查詢它的只讀Exception屬性。該屬性返回的就是傳給Abort的對象。這就容許調用Abort的線程指定了一些額外的信息,供捕捉ThreadAbortException異常的代碼檢查。宿主可利用這個功能讓本身的處理代碼知道它爲何要停止線程。