重温CLR(十六) CLR寄宿和AppDomain

       寄宿(hosting)使任何应用程序都能利用clr的功能。特别要指出的是,它使现有应用程序至少能部分使用托管代码编写。另外,寄宿还为应用程序提供了通过编程来进行自定义和扩展的能力。

       允许可扩展性意味着第三方代码可在你的进程中运行。在windows中将第三方dll加载到进程中意味着冒险。dll中的代码很容易破坏应用程序的数据结构和代码。dll还可能企图利用应用程序的安全上下文来访问它本来无权访问的资源。clr的appDomain功能解决了所有这些问题。AppDomain允许第三方、不受信任的代码在现有的进程中运行,而CLR保证数据结构、代码和安全上下文不被滥用或破坏。

       程序员经常讲寄宿和AppDomain与程序集的加载和反射一起使用。这4种技术一起使用,使clr称为一个功能及其丰富和强大的平台。本章重点在于寄宿和AppDomain。

CLR寄宿

       .net在windows平台的顶部运行。这意味着.net必须用windows能理解的技术来构建。首先,所有托管模块和程序集文件都必须使用windows PE文件格式,而且要么是windows EXE文件,要么是Dll文件。

       开发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一起发布的那个版本。

       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清理进程使用的所有资源。

AppDomain

       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中的代码,再执行另一个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);
        }
    }
}
跨AppDomain边界访问对象

演示1:使用“按引用封送(Marshal-by-Refernce)”AppDomain通信

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边界的对象访问,会产生一些性能上的开销。所以一般应该尽量少用这个功能

演示2:使用“按值封送(Marshal-by-Value)”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的边界按值封送了。

演示3:使用不可封送的类型跨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很强大的一个地方就是可以卸载它。卸载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

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);
        }
    }
}
监视AppDomain

AppDomain FirstCance异常通知

①异常首次抛出时,CLR调用向抛出异常的AppDomain登记的所有FirstChanceException回调方法。

②然后。CLR查找栈上同一个AppDomain中的任何catch块,有一个catch块能处理异常,则异常处理完成,将继续执行

③如果AppDomain中没有一个catch块能处理异常,则CLR沿着栈向上来到调用AppDomain,再次抛出同一异常对象(序列化和反序列化之后)

④这时感觉就像是抛出一个全新新的异常,CLR调用当前AppDomain登记的所有FirstChanceException回调方法

⑤这个过程会一直执行,直到抵达线程栈顶部。如果异常还未处理,则进程终止

宿主如何使用AppDomain

       前面已讨论了宿主以及宿主加载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函数。

MicrosoftASP.NET和XML Web服务应用程序

       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的一个名为影像复制功能

Mircrosoft Sql Server

Microsoft sql server是非托管应用程序,它的大部分代码仍是用非托管c++写的。sql server 允许开发人员使用托管代码创建存储过程。首次请求数据库运行一个用托管代码写的存储过程时,sql server会加载clr。存储过程在它们自己的安全AppDomain中运行,这避免了存储过程对数据库服务器产生负面影响。

高级宿主控制

使用托管代码管理clr

       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异常的代码检查。宿主可利用这个功能让自己的处理代码知道它为什么要中止线程。

猜你喜欢

转载自www.cnblogs.com/qixinbo/p/10762419.html
CLR