寄宿(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异常的代码检查。宿主可利用这个功能让本身的处理代码知道它为何要停止线程。