寄宿允许使任务应用程序都能利用CLR的功能。寄宿(hosting)允许使任务应用程序都能利用CLR的功能。另外,寄宿还为应用程序提供了通过编程来进行自定义和扩展能力。AppDomain允许第三方的,不受信任的代码在一个现有的进程中运行,而CLR保证数据结构、代码和安全上下文不会被滥用或破坏。
22.1 CLR寄宿
CLR Hosting(CLR 宿主)的概念:初始启动.Net Application时,Windows进程的执行和初始化跟传统的Win32程序是一样的,执行的还是非托管代码,只不过由于PE文件中引入了CLR Header,OS进程加载了mscoree.dll,从而启动了CLR。CLR本身不是一个可执行程序,它需要一个进程来装载并启动它,从而接管进程并创建自身的程序运行上下文,这个过程可称之为CLR Hosting。从本质上来讲,CLR是一个COM服务器,它自身封装了一系列称之为CLR Hosting APIs的接口,以便于CLR寄宿于非托管程序,从而在非托管环境上下文中执行托管程序。比如SQL Server2005和ASP.NET都运用了此种技术使CLR寄宿于其运行环境。
所有托管模块和程序集都必须使用Windows PE文件格式。
开发CLR时,Microsoft实际是将CLR实现成包含在一个DLL中的COM服务器。也就是说,Microsoft为CLR定义了一个标准的COM接口,并为该接口和COM服务器分配了GUID(全局通用标识符)。安装.Net Framework时,代表CLR的COM服务器和其他COM服务器一样在Windows注册表中注册。
任何Windows应用程序都可以寄宿CLR。非托管宿主应该调用MetaHost.h文件中声明的CLRCreateInstance函数。CLRCreateInstance函数是在MSCorEE.dll文件中实现的,该文件一般是在C:\Windows\System32目录中。这个DLL被称为“垫片”(shim),它的工作是决定创建哪个版本的CLR,注意垫片DLL本身并不包含CLR COM服务器。
一台机器可安装多个版本的CLR,但只有一个版本的MSCorEE.dll文件(垫片)。
CLRCreateInstance函数可以返回一个ICLRMetaHost接口。宿主应用程序可调用这个接口的GetRuntime函数,指定宿主要创建的CLR的版本。然后,垫片将所需版本的CLR加载到宿主的进程中。
宿主应用程序可调用ICLRRuntimeHost接口定义的方法做下面的事情:
- 设置宿主管理器。告诉CLR,宿主想参与涉及以下操作的决策:内存分配、线程调度/同步以及程序集加载等。宿主还可声明它想获得有关垃圾回收启动和停止以及特定操作超时的通知。
- 获取CLR管理器。告诉CLR阻止使用某些类/成员。另外,宿主能分辨哪些代码可以调试,哪些代码不能,以及当一个特定事件(例如AppDomain卸载、CLR停止或者堆栈溢出异常)发生时宿主应该调用哪个方法。
- 初始化并启动CLR。
- 加载一个程序集并执行其中的代码。
- 停止CLR,阻止任何更改的托管代码在Windows进程中运行。
寄宿使任何应用程序都能提供CLR功能和可编程性,以下是寄宿CLR的部分好处:
- 可以用任何编程语言来编写。
- 代码在JIT编译后执行,所有速度很快(而不是一边解释一边执行)
- 代码使用垃圾回收避免内存泄露和损坏
- 宿主不必操心提供一个丰富的开发环境
托管程序和非托管程序的区别:
对于托管的和非托管的程序集编译器都会把程序集编译成以.exe或.dll等为扩展名的文件,可见Windows加载器并没有区分是托管还是非托管的程序集.
对于托管还是非托管程序集,他们在编译器执行编译时都会编译成一个特殊的文件格式,即PE文件(可移植可执行文件格式),操作系统加载器通过加载这样的PE文件来执行程序集的。可以这么说吧,无论是托管程序还是非托管程序他们实际上都是编译成这样的PE文件(只是有部分内容不一样而已).
然后这个PE文件会指示如何执行托管程序集和非托管程序集,加载器首先会查找到PE头中的AddressOfEntryPoint域,这个域指示PE文件的入口点位置,在.NET程序集中是指向.text段中的CLR头--〉包含一个结构IMAGE_COR20_HEADER—>包含许多信息如托管代码应用程序的入口点,目标CLR的主版本号和从版本号,以及程序集的强名称签名等--〉Windows加载器根据这个数据结构决定加载哪个版本的CLR以及一些基本的程序集信息。在.text段中还包含了程序集的元数据表,MSIL以及非托管启动存根代码,而非托管启动存根代码包好了由Windows加载器执行役启动PE文件执行的代码。
(1)非托管程序的执行过程
在非托管程序中,可执行里面保存的是机器代码,CPU可以直接加载并执行,当系统加载了可执行程序后,系统就将可执行文件的段基址加上偏移地址形成实际的物理地址,并直接加载到内存中运行。
(2)托管程序的执行过程
托管程序的可执行文件中,包含的是中间语言以及元数据,当然不能直接运行,必须启动CLR,由CLR对中间
语言进行即时编译成机器代码,并加载到内存里面执行(具体过程:程序在进入入口函数前会提前跳转到MSCoree.dll中,调用它的代码来启动CLR并
完成一些初始化工作)。当然,IL中的方法并不是每次被调用都会被编译一次,而是它只有在第一次调用时才进行编译,即时编译器会将方法名称以及对应的入口
地址存放在映射表中,当下次调用该方法时,会直接从映射表里去而不是再编译一次。
22.2 AppDomain
CLR COM服务器初始化时,会创建一个AppDomain。AppDomain是一组程序集的逻辑容器。CLR初始化时创建的第一个AppDomain称为默认AppDomain,这个默认的AppDomain只有在Windows进程终止时才会被销毁。
除了默认AppDomain,托管类型方法的一个宿主还可指示CLR创建额外的AppDomain。AppDomain唯一的作用是进行隔离。
CLR在我们的任何代码运行之前就创建了默认的 AppDomain,并且用可执行文件的文件名作为默认的AppDomain的友好名称。
下面总结了AppDomain的具体功能:
- 一个AppDomain中的代码创建的对象不能由另一个AppDomain中的代码直接访问
一个AppDomain中的代码创建了一个对象后,该对象被该AppDomain“拥有”。换言之,它的生存期不能比创建它的代码所在的AppDomain还要长。一个AppDomain中的代码为了访问另一个AppDomain中的对象,只能使用“按引用封送”或者“按值封送”的语义。这就加强了一个清晰的分隔和边界,因为一个AppDomian中的代码没有对另一个AppDomain中的代码所创建的对象的直接引用。
- AppDomain可以卸载
CLR不支持从AppDomain中卸载一个程序集的能力。但是,可以告诉CLR卸载一个AppDomain,从而卸载当前包含在该AppDomain内的所有程序集。
- AppDomain可以单独保护
AppDomain在创建之后,会应用一个权限集,它决定了向这个AppDomain中运行的程序集授予的最大权限。
- AppDomain可以单独实施配置
AppDomain在创建之后,会关联一组配置设置。
下图演示了一个Windows进程,其中运行着一个CLR COM服务器。该CLR当前管理着两个AppDomain。每个AppDomain都有自己的Loader堆,每个Loader堆都记录了自AppDomain创建以来已访问过哪些类型。Loader堆中的每个类型对象都有一个方法表,方法表中的每个记录项都指向JIT编译的本地代码(前提是方法至少执行过一次)。
除此之外,每个AppDomain都加载了一些程序集。AppDomain有三个程序集:MyApp.exe,TypeLib.dll和System.dll。AppDomain#2有两个程序集:Wintellect.dll和System.dll。
如图所示,System.dll程序集被加载到两个AppDomain中。如果这两个AppDomain都使用了来自System.dll的一个类型,那么在两个AppDomain的Loader堆中,都会为同一个类型分配一个类型对象;类型对象的内存不会由两个AppDomain共享。
AppDomain的全部目的就是提供隔离性;CLR要求在卸载某个AppDomain并释放它的所有资源的同时,不会对其他AppDomain产生负面影响。通过复制CLR的数据结构,就可以保证这一点。除此之外,还能保证由多个AppDomain使用的一个类型在每个AppDomain中都有一个静态字段。
跨越AppDomain边界访问对象
一个AppDomain中的代码可以和另一个AppDomain中的类型和对象通信。但是,只允许通过良好定义的机制访问。下面的代码演示了如何创建一个新的AppDomain,在其中加载一个程序集,然后构造程序集所定义的类型实例。代码演示构造了三种类型的不同行为:
(1)”按引用封送“(Marshal-by-Reference)的类型;
(2)”按值封送“(Marshal-by-value)的类型;
(3)不能封送的类型。
using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Remoting; using System.Threading; public sealed class Program { public static void Main() { Marshalling(); } private static void Marshalling() { // Get a reference to the AppDomain that that calling thread is executing in //获取AppDomain的一个引用 AppDomain adCallingThreadDomain = Thread.GetDomain(); // Every AppDomain is assigned a friendly string name (helpful for debugging) //每个AppDomain都被赋予了一个友好字符串名称 // Get this AppDomain‘s friendly string name and display it String callingDomainName = adCallingThreadDomain.FriendlyName; Console.WriteLine("Default AppDomain‘s friendly name={0}", callingDomainName); // Get & display the assembly in our AppDomain that contains the ‘Main‘ method //获取AppDomain中包含了“Main”方法的程序集 String exeAssembly = Assembly.GetEntryAssembly().FullName; Console.WriteLine("Main assembly={0}", exeAssembly); // Define a local variable that can refer to an AppDomain //定义一个局部变量来引用一个AppDomain AppDomain ad2 = null; // *** DEMO 1: Cross-AppDomain Communication using Marshal-by-Reference //使用“按引用封送”进行跨AppDomain通信 Console.WriteLine("{0}Demo #1", Environment.NewLine); // Create new AppDomain (security & configuration match current AppDomain) ad2 = AppDomain.CreateDomain("AD #2", null, null); MarshalByRefType mbrt = null; // Load our assembly into the new AppDomain, construct an object, marshal //将我们的程序集加载到新AppDomain中,构造一个对象,把它封送回我们的AppDomain // it back to our AD (we really get a reference to a proxy) mbrt = (MarshalByRefType) ad2.CreateInstanceAndUnwrap(exeAssembly, "MarshalByRefType"); Console.WriteLine("Type={0}", mbrt.GetType()); // The CLR lies about the type // Prove that we got a reference to a proxy object Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbrt)); // This looks like we‘re calling a method on MarshalByRefType but, we‘re not. // We‘re calling a method on the proxy type. The proxy transitions the thread // to the AppDomain owning the object and calls this method on the real object. mbrt.SomeMethod(); // Unload the new AppDomain //卸载新的AppDomain AppDomain.Unload(ad2); // mbrt refers to a valid proxy object; the proxy object refers to an invalid AppDomain try { // We‘re calling a method on the proxy type. The AD is invalid, exception is thrown mbrt.SomeMethod(); Console.WriteLine("Successful call."); } catch (AppDomainUnloadedException) { Console.WriteLine("Failed call."); } // *** DEMO 2: Cross-AppDomain Communication using Marshal-by-Value Console.WriteLine("{0}Demo #2", Environment.NewLine); // Create new AppDomain (security & configuration match current AppDomain) ad2 = AppDomain.CreateDomain("AD #2", null, null); // Load our assembly into the new AppDomain, construct an object, marshal // it back to our AD (we really get a reference to a proxy) MarshalByValType mbvt = (MarshalByValType) ad2.CreateInstanceAndUnwrap(exeAssembly, "MarshalByValType"); // Prove that we did NOT get a reference to a proxy object Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbvt)); // This looks like we‘re calling a method on MarshalByValType and we are. Console.WriteLine("Returned object created " + mbvt.ToString()); // Unload the new AppDomain AppDomain.Unload(ad2); // mbvt refers to valid object; unloading the AppDomain has no impact. try { // We‘re calling a method on an object; no exception is thrown Console.WriteLine("Returned object created " + mbvt.ToString()); Console.WriteLine("Successful call."); } catch (AppDomainUnloadedException) { Console.WriteLine("Failed call."); } // DEMO 3: Cross-AppDomain Communication using non-marshalable type Console.WriteLine("{0}Demo #3", Environment.NewLine); // Create new AppDomain (security & configuration match current AppDomain) ad2 = AppDomain.CreateDomain("AD #2", null, null); // Load our assembly into the new AppDomain, construct an object, marshal // The object‘s method returns an non-marshalable object; exception NonMarshalableType nmt = (NonMarshalableType) ad2.CreateInstanceAndUnwrap(exeAssembly, "NonMarshalableType"); // The object‘s method returns an non-marshalable object; exception } } // Instances can be marshaled-by-reference across AppDomain boundaries public sealed class MarshalByRefType : MarshalByRefObject { public MarshalByRefType() { Console.WriteLine("{0} ctor running in {1}", this.GetType().ToString(), Thread.GetDomain().FriendlyName); } public void SomeMethod() { Console.WriteLine("Executing in " + Thread.GetDomain().FriendlyName); } } // Instances can be marshaled-by-value across AppDomain boundaries [Serializable] public sealed class MarshalByValType : Object { private DateTime m_creationTime = DateTime.Now; // NOTE: DateTime is [Serializable] public MarshalByValType() { Console.WriteLine("{0} ctor running in {1}, Created on {2:D}", this.GetType().ToString(), Thread.GetDomain().FriendlyName, m_creationTime); } public override String ToString() { return m_creationTime.ToLongDateString(); } } // Instances cannot be marshaled across AppDomain boundaries // [Serializable] public sealed class NonMarshalableType : Object { public NonMarshalableType() { Console.WriteLine("Executing in " + Thread.GetDomain().FriendlyName); } }
AppDomain是CLR的功能,Windows对此一无所知。线程和AppDomain没有一对一的关系。
CreateInstanceAndUnwrap方法导致调用线程从当前AppDomain到新的AppDomain。线程将制定程序集加载到新 AppDomain中,并扫描程序集类型定义元数据表,查找指定类型“MarshalByRefType”)。找到类型后,调用它的无参构造函数。然后, 线程又范围默认AppDomain,对CreateInstanceAndUnwrap返回的MarshalByRefType对象进行操作。
如何将一个对象从一个AppDomain(源AppDomain,这里指真正创建对象的地方)封送到另一个AppDomain(目标AppDomain,这里指调用CreateInstanceAndUnwrap的地方)?
1. Marshal-by-Reference
CLR会在目标AppDomain的Loader堆中定义一个代理类型。这个代理类型是用原始类型的数据定义的。因此,它看起来和原始类型完全一样;有完全一样的实例成员(属性、事件和方法)。但是,实例字段不会成为(代理)类型的一部分。
2. Marshal-by-Value
CLR将对象字段序列化一个字节数组。这个字节数组从源AppDomain复制到目标AppDomain。然后,CLR在目标AppDomain中 反序列化字节数组,这会强制CLR将定义了的“被反序列化的类型”的程序集加载到目标AppDomain中。接着,CLR创建类型的一个实例,并利用字节 数组初始化对象的字段,使之与源对象中的值相同。换言之,CLR在目标AppDomain中准确的复制了源对象。
22.3卸载AppDomain
AppDomain.Unload()中执行操作:
(1)CLR挂起进程中执行中执行的托管代码的所有线程;
(2)CLR检查所有线程栈,查看哪些线程正在执行要卸载的那个AppDomain中的代码,或者哪些线程会在某个时刻返回至要卸载的那个 AppDomain。在任何一个栈上,如果准备卸载的AppDomain,CLR都会强迫对应的线程抛出一个ThreadAbortException异 常(同时恢复线程的执行)。这将导致线程展开(unwind),在展开的过程中执行遇到的所有finally块中的内容,以执行资源清理代码。如果没有代 码捕捉ThreadAbortException,它最终会成为一个未处理的异常,CLR会“吞噬”这个异常,线程会终止,但进程可以继续运行。这一点是 非常特别的,因为对于其他所有未处理的异常,CLR都会终止进程。
重要提示:如果一个线程当前正在finally块、catch块、类构造器、临界执行区(critical execution region)域或非托管代码中执行,那么CLR不会立即终止该线程。否则,资源清理代码、错误恢复代码、类型初始化代码、关键代码或者其他任何CLR不 了解的代码都无法完成,导致应用程序的行为变得无法预测,甚至可能造成安全漏洞。线程在终止时,会等待这些代码块执行完毕。然后当代码块结束时,CLR再 强制线程抛出一个ThreadAbortException。
临界区是指线程终止或未处理异常的影响可能不限于当前任务的区域。相反,非临界区中的终止或失败只对出现错误的任务有影响。
(3)当上一步发现的所有线程都离开AppDomain后,CLR遍历堆,为引用了“已卸载的AppDomain创建的对象”的每一个代理都设置一 个标志(flag)。这些代理对象现在知道它们引用的真实对象已经不在了。如果任何代码在一个无效的代理对象上调用一个方法,该方法会抛出一个 AppDomainUnloadedException
(4)CLR强制垃圾回收,对现已卸载AppDomain创建的任何对象占用的内存进行回收。这些对象的Finalize方法被调用(如果存在Finalize方法),使对象有机会彻底清理它们占用的资源
(5)CLR恢复剩余所有线程的执行。调用AppDomain.Unload方法的线程将继续执行,对AppDomain.Unload的调用是同 步进行的在前面的例子中,所有工作都用一个线程来做。因此,任何时候只要调用AppDomain.Unload都不可能有另一个线程在要卸载的 AppDomain中。因此,CLR不必抛出任何ThreadAbortException异常。
22.4 监视AppDomain
使用AppDomain.MonitoringIsEnabled=true获取或设置一个值,该值指示是否对当前进程启用应用程序域的 CPU 和内存监视。 一旦对进程启用了监视,则无法将其禁用。