CLR寄宿和AppDomain
寄宿(Hosting)
使任何应用程序都能利用CLR的功能.
AppDomain
允许第三方的,不受信任的代码在现有的进程中运行,而CLR保证数据结构,代码和安全上下文不被滥用和破坏.
CLR寄宿
.Net Framework
在Windows平台的顶部运行, 这意味着.net必须用Windows能理解的技术来构建。首先,所有托管模块和程序集文件都必须使用windows PE文件格式
,而且要么是windows EXE文件
,要么是Dll文件
。
开发CLR时, Microsoft实际是把它写成一个包含一个DLL中的COM服务器. 安装.Net Framework
时, 代表CLR的COM服务器一样在注册表中注册. 任何Windows应用程序都能寄宿(容纳)CLR。
CLRCreateInstance函数在MSCorEE.dll文件中实现,该文件一般在system32目录中。这个dll被人们亲切地称为垫片(shim),它的工作是决定创建哪个版本的CLR:垫片DLL本身不包含CLR com服务器。
CLRCreateInstance
函数可返回一个ICLRMetaHost接口
。宿主应用程序可调用这个接口的GetRuntime函数
,指定数组要创建的CLR
的版本。然后,垫片将所需版本的CLR加载到宿主的进程中。
AppDomain
CLR COM服务器服务器初始化时会创建一个AppDomain。AppDomain是一组程序集的逻辑容器. CLR初始化时创建的第一个AppDomain称为默认AppDomain
,这个默认的AppDomain只有在windows进程终止时才会被注销。
除了默认AppDomain,正在使用非托管com接口方法或托管类型方法的宿主还可要求CLR创建额外的AppDomain。AppDomain是为了提供隔离而设计的。下面总结了AppDomain的具体功能。
- 一个AppDomain的代码不能直接访问另一个AppDomain的代码创建的对象
一个AppDomain中的代码创建了一个对象后,该对象便被该AppDomain拥有。换言之,它的生存期不能超过创建它的代码所在的AppDomain。一个AppDomain中的代码要访问另一个AppDomain中的对象,只能使用按引用封送(marshal-by-reference)
或者按值封送(marshal-by-value)
的语义。这就强制建立了清晰的分隔和边界,因为一个AppDomain中的代码不能直接引用另一个AppDomain中的代码创建的对象。这种隔离使得AppDomain能很容易地从进程中卸载,不会影响其他AppDomain正在运行的代码。
- AppDomain可以卸载
CLR不支持从AppDomain中卸载特定的程序集。但可以告诉clr卸载一个AppDomain,从而卸载该AppDomain当前包含的所有程序集。
- AppDomain可以单独保护
AppDomain创建后会应用一个权限集,它决定了向这个AppDomain中运行的程序集授予的最大权限。正式由于存在这些权限,所以当宿主加载一些代码后,可以保证这些代码不会破坏(或读取)宿主本身使用的一些重要数据结构。
- AppDomain可以单独配置
AppDomain创建后会管理一组配置设置,这些设置主要影响clr在AppDomain中加载程序集的方式。涉及搜索路径、版本绑定重定向、劵影复制以及加载器优化。
在windows中创建进程的开销很大。win32 createProcess函数的速度很慢,而且windows需要大量内存来虚拟化进程的地址空间。但是,如果应用程序完全由托管代码构成,同时这些代码没有调用非托管代码,那么在一个windows进程中运行多个托管应用程序是没有问题的。
图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中的类型和对象通信. 但只能通过良好定义的机制进行, 以下例子演示三种类型构造时的不同行为,以及卸载时的不同行为:
引用封送(marshal-by-reference)
按值封送(marshal-by-value)
- 完全不能封送的类型
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;
class Program
{
static void Main(string[] args)
{
Marshalling();
}
private static void Marshalling()
{
//获取AppDomain引用(“调用线程”当前正在该AppDomain中执行)
AppDomain adCallingThreadDomain = Thread.GetDomain();
//每个AppDomain都分配了友好字符串名称(以便调试)
//获取这个AppDomain的友好名称并显示它
String CallingDomainName = adCallingThreadDomain.FriendlyName; // ClassLibrary1.exe\r\n无上下文策略\r\n
Console.WriteLine("默认AppDomain友好的名称={0}", adCallingThreadDomain);
//获取并显示我们的AppDomain中包含了“Main”方法的程序集
String exeAssembly = Assembly.GetEntryAssembly().FullName;
Console.WriteLine("包含“Main”方法的程序集={0}",
exeAssembly); // ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken = null
//定义局部变量来引用一个AppDomain
AppDomain ad2 = null;
//***********************************************
//DEMO 1:使用“按引用封送”进行跨AppDomain通信 ***
//***********************************************
//Environment.NewLine 获取为该环境定义的换行字符串。
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, "MarshalByRefType");
// CLR在类型上撒谎了
Console.WriteLine("Type={0}", mbrt.GetType()); //Type=MarshalByRefType
//证明得到的是对一个代理对象的引用
Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbrt)); // Is proxy=True
//看起来像是在MarshalByRefType上调用了一个方法,实则不然。
//我们是在代理类型上调用了一个方法,代理是线程切换到拥有对象的那个
//AppDomain,并在真实的对象上调用这个方法
mbrt.SomeMethod();
//卸载新的AppDomain
AppDomain.Unload(ad2);
//此时,mbrt引用了一个有效的代理对象;代理对象引用一个无效的AppDomain
// 会抛出异常,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, "MarshalByRefType");
//对象的方法返回所返回对象的副本
//对象按值(而非按引用)封送
MarshalByValType mbvt = mbrt.MethodWithReturn();
//证明得到的是对一个代理对象的引用
Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbvt)); // Is proxy=False
//看起来在MarshalByValType上调用一个方法,实际也是如此
Console.WriteLine("Return object created " + mbvt.ToString()); // Return object created 2019年9月13日
//卸载新的AppDomain
AppDomain.Unload(ad2);
// 此时,mbrt引用了一个有效的x代理对象;代理对象引用一个无效的AppDomain
// 不会抛出异常
try
{
//卸载AppDomain之后调用mbvt方法不会抛出异常
Console.WriteLine("Return object created " + mbvt.ToString()); // Return object created 2019年9月13日
Console.WriteLine("调用成功");
}
catch (AppDomainUnloadedException)
{
Console.WriteLine("调用失败,AppDomain被卸载了");
}
//**********************************************
//DEMO 3:使用不可封送的类型进行跨AppDomain通信*
//**********************************************
ad2 = AppDomain.CreateDomain("AD #2", null, null);
mbrt = (MarshalByRefType) ad2.CreateInstanceAndUnwrap(exeAssembly, "MarshalByRefType");
try
{
// 对象的方法返回一个不可封送的对象,抛出异常
NonMarshalableType nmt = mbrt.MethodArgAndReturn(CallingDomainName); //抛出异常:未标记为可序列化
}
catch (SerializationException)
{
Console.WriteLine("抛出异常:未标记为可序列化");
}
Console.ReadKey();
}
}
//该类型的实例可跨越AppDomain的边界“按引用封送”
public sealed class MarshalByRefType : MarshalByRefObject
{
public MarshalByRefType()
{
Console.WriteLine("{0} 构造器运行在 {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);
}
}
- Marshalling方法首先获得一个AppDomain对象引用, 当前”调用线程”当前正在该AppDomain中执行.
在Windows中, 线程 总是在一个进程的上下文中创建, 而且线程的整个生存期都在该进程的生存期内. 但线程和AppDomain没有一对一关系. AppDomain是一项CLR功能. Windows对AppDomain无所知.
由于一个Windows进程可包含多个AppDomain,所以线程能执行一个AppDomain中的代码,再执行另一个AppDomain. 从CLR的角度看, 线程一次只能执行一个AppDomain中的代码.
线程可以调用
System.Threading.Thread
的静态方法GetDomain
向CLR询问它正在哪个AppDomain中执行. 线程还可以查询AppDomain的静态只读属性CurrentDomain
获得相同的信息.AppDomain创建后可以赋予一个
友好名称
, 是用于标识AppDomain的一个String
. 作用是用来方便的调试. CLR在任何代码执行前创建默认的AppDomain, 所以使用可执行文件的文件名作为默认的AppDomain名称.Marshalling
方法使用AppDomain的只读FriendlyName
属性来查询默认AppDomain的友好名称.Marshalling
方法查询默认AppDomain中加载的程序集的强命名标识, 这个程序集定义了入口方法Main(其中调用了Marshalling方法),类型MarshalByRefType
,MarshalByValType
,NonMarshalableType
.
演示Demo 1 : 使用”按引用封送”进行跨AppDomain通信
AppDomain.CreateDomain
三个参数:
- 友好名称
AD #2
- 用于计算AppDomain
权限集
的证据, 传null代表新的AppDomain从创建它的AppDomain继承权限集. - 代表AppDomain使用的配置参数, 传null代表新的AppDomain从创建它的AppDomain继承配置设置.
CreateDomain
方法内部会在进程中新建一个AppDomain, 有自己的Loader堆,这个堆目前是空的.
现在要在新的AppDomain中创建类型的实例. 首先要将程序集加载到新的AppDomain中,然后构造定义类型的实例. 这就是CreateInstanceAndUnwrap
方法所做的事情: 传递了两个参数
String
: 标识了想在新AppDomain中加载的程序集String
: 要构建实例的类型名称MarshalByRefType
CreateInstanceAndUnwrap
方法会在内部从当前AppDomain切换到新AppDomain, 线程将指定程序集加载到新AppDomain中, 扫描程序集的类型定义元数据表, 找到类型后,线程调用MarshalByRefType
的无参构造器, 现在线程又切换回默认的AppDomain. 使CreateInstanceAndUnwrap
能返回对新MarshalByRefType
对象的引用.
CreateInstanceAndUnwrap 有重载方法可以传递实参
但是, CLR不允许一个AppDomain中的变量
引用另一个AppDomain中创建的对象. 如果CreateInstanceAndUnwrap
方法照顾会返回对象引用, 隔离性就会被打破, 隔离是AppDomain的全部目的! 因此CreateInstanceAndUnwrap
方法在返回对象引用前要执行一些额外的逻辑.
MarshalByRefType
类型从一个很特别的基类MarshalByRefObject
派生, 当CreateInstanceAndUnwrap
发现它封送的对象的类型继承自MarshalByRefObject
时, CLR就会跨AppDomain边界按引用封送对象
.
将一个引用对象从源AppDomain
封送到目标AppDomain
的具体含义:
- CLR会在目标AppDomain的Loader堆中定义一个
代理类型
, 是用原始类型的元数据定义的- 有完全的一样的实例成员(属性,事件,方法)
- 但是唯独没有实例字段, 因为代理类型会定义自己的实例字段, 和原始字段不一致.
- 这些字段只是指出哪个AppDomain拥有真实的对象, 以及如何找到真实的对象
- 在内部,
代理对象
用一个GCHandle实例引用真实的对象
在目标AppDomain中定义好这个类型之后,CreateInstanceAndUnwrap
方法就会创建代理类型的实例,初始化它的字段来标识源AppDomain和真实的对象, 然后将这个代理对象的引用返回给源AppDomain
.
MarshalByRefType mbrt变量
被设为引用这个代理. 注意! 实际上返回的对象不是MarshalByRefType
类型的实例,当前情况下CLR之所以允许转型, 是因为新类型具有和原始类型一样的实例成员. 代理对象调用GetType()
,它会向你撒谎说自己是一个MarshalByRefType
对象.
RemotingServices.IsTransparentProxy(mbrt)
用这个证明mbrt是代理, 也就是CreateInstanceAndUnwrap
方法返回的是代理.
mbrt.SomeMethod();
通过代理, 调用由代理实现的SomeMethod方法, 代理的实现利用代理对象中的信息字段,将调用线程从默认AppDomain切换至对应AppDomain. 线程接着使用代理对象的GCHandle
字段查找新AppDomain
中的真实对象, 并用真实对象调用真实的SomeMethod方法.
两个办法可以证明调用线程已经从默认的AppDomain切换至新AppDomain.
Thread.GetDomain().FriendlyName
将返回AD #2
- 通过逐语句调试代码,并打开调用堆栈窗口, [外部代码]行会标注一个线程什么位置跨域AppDomain边界.(VS中)
在Rider中是:
MarshalByRefType.SomeMethod() in , ClassLibrary1.exe
[Application Domain Transition]
Program.Marshalling() in , ClassLibrary1.exe
Program.Main() in , ClassLibrary1.exe
真实的SomeMethod方法
返回后, 会返回至代理的SomeMethod方法
, 后者会将线程切换至默认AppDomain,线程继续执行默认AppDomain中的代码.
AppDomain.Unload(ad2);
这会指定卸载指定的AppDomain包括加载到其中的所有程序集. 并强行执行一次垃圾回收, 来释放由卸载的AppDomain中的代码创建的所有对象. mbrt变量引用的仍然的是有效的代理对象, 但是代理对象已不再引用一个有效的AppDomain(已经卸载了).
当默认AppDomain试图使用代理对象调用SomeMethod方法时, 调用的是该方法在代理中的实现, 代理发现包含真实对象的AppDomain已经卸载, 就抛出AppDomainUnloadedException
异常,告诉操作者无法完成.
一个AppDomain的方法执行完毕,才能执行另一个AppDomain的方法,不能多个AppDomain的代码并发执行.
使用”按引用封送”的语义进行跨AppDomain边界的对象访问,会产生一些性能上的开销. 一般尽量少用这个功能.
从MarshalByRefObject
派生的类型可以定义实例字段, 但是这些字段不会成为代理类型的一部分,也不会包含在代理对象中. 这实例字段的读写会用到反射,性能很差, 访问默认AppDomain中的性能也好不到那里去.
派生自MarshalByRefObject
的类型应该避免定义任何静态成员, 因为静态成员总是在调用AppDomain的上下文中访问. 静态成员的一个AppDomain中执行, 实例成员却在另一个AppDomain中执行. 这样的编程模型太丑了.
租约管理器
演示Demo 2 : 使用”按值封送”进行跨AppDomain通信
前面一样, 不一样的是, 代理还需要调用 MarshalByValType mbvt = mbrt.MethodWithReturn();
, 将在新的AppDomain中执行以创建MarshalByValType
类型的实例, 并将一个对象引用返回给默认的AppDomain.
MarshalByValType
不从MarshalByRefObject
派生, 所以CLR不能定义一个代理类型并创建代理类型的实例. 对象不能按引用跨AppDomain边界进行封送.
由于MarshalByValType
标记了自定义特性[Serializable], 所以MethodWithReturn
方法能按值封送对象. 下面描述将一个对象按值从源AppDomain封送到目标AppDomain的含义.
源AppDomain想向目标AppDomain发送或返回一个对象引用时, CLR将对象的实例字段序列化成一个字节数组. 字节数组从源AppDomain复制到目标AppDomain. 然后CLR在目标AppDomain中反序列化字节数组, 这里会强制CLR将定义了此类型的程序集加载到目标AppDomain中. CLR创建类型时,并利用字节数组中的值初始化对象的字段,使之与源对象中的值相同. 也就是说 CLR在目标AppDomain中精确复制了源对象. 然后MethodWithReturn
返回对这个副本的引用.
RemotingServices.IsTransparentProxy(mbvt)
返回是false, 说明对象是一个真实的对象,不是代理. 由于mbvt引用真实的对象, 在卸载新AppDomain之后, mbvt.ToString()
会调用这个方法的真实实现, 线程不会在AppDomain之间切换.
演示Demo 3 : 使用不可封送的进行跨AppDomain通信
与之前大致相似, 不同的是 NonMarshalableType nmt = mbrt.MethodArgAndReturn(CallingDomainName);
, 由于NonMarshalableType
不是MarshalByRefObject
派生的, 也没有用[Serializable]
定制特性进行标记, 所以不允许MethodArgAndReturn
按引用或按值封送对象. 此对象完全不能跨越AppDomain边界进行封送.
此例子中传递的参数String不是从MarshalByRefObject
派生,但是String被标记为[Serializable]
,所以CLR能按值封送它.
对于String对象,CLR会采取特殊的优化措施. 跨越AppDomain边界封送一个String对象时, CLR只是跨越边界传递对String对的引用, 不会真的生成String对象的副本, 之所以提供这个优化,是因为String对象是不可变的.
卸载AppDomain
AppDomain很强大的一个地方就是可以卸载它. 卸载AppDomain会导致CLR卸载AppDomain中的所有程序集. 还会释放AppDomain的Loader堆. 卸载AppDomain的办法是调用AppDomain的静态Unload方法.
- CLR挂起进程中执行过托管代码的所有线程
- CLR检查所有线程栈, 查看哪些线程正在执行要卸载的AppDomain中的代码,或者哪些线程会在某个时候返回至要卸载的AppDomain. 任何栈上有要卸载的AppDomain, CLR都会强迫对应线程抛出一个
ThreadAbortException
(同时恢复线程的执行), 这导致线程展开(unwind), 并执行遇到的所有finally块以清理资源. 如果没有代码捕捉ThreadAbortException
,会成为未处理异常, 特殊的是CLR会吞噬这个异常, 线程终止,但进程可以继续. 对于其他所有未经处理的异常CLR都会终止进程.
临界执行区 : 指线程终止或未处理异常的影响可能不限于当前任务的区域.
非临界执行区: 终止或失败只对出错的任务有影响.
- 当第二步发现的而所有线程都离开AppDomain后, CLR遍历堆, 为引用了由已卸载的AppDomain创建的对象的每个代理对象都设置一个标志(flag) .这些对象现在知道它们引用的真实对象已经不在了, 现在, 任何代码在无效的代理对象上调用方法都会抛出一个AppDomainUnloadedException异常.
- CLR强制垃圾回收,回收由已卸载的AppDomain创建的任何对象的内存. 这些对象Finalize方法被调用, 使对象有机会正确清理它们占用的资源.
- CLR恢复剩余所有线程的执行, 调用AppDomain.Unload方法的线程将继续运行; 对AppDomain.Unload的调用是同步进行的.
当一个线程 调用AppDomain.Unload方法时, 针对要卸载的AppDomain中的线程
,CLR会给它们10秒钟的时间离开. 10秒钟后, 如果调用AppDomain.Unload方法的线程还没有返回,CLR将抛出CannotUnloadAppDomianException
异常, AppDomain将来可能会,也可能不会卸载.
监视AppDomain
AppDomain的静态MonitoringEnabled属性
设置为true, 显式打开监控。打开监控后便不能关闭,再设为false会抛ArgumentException, 代码可查询AppDomain类提供的以下4个只读属性
MonitoringSurvivedProcessMemorySize
:这个Int64静态属性返回由当前CLR实例控制的所有AppDomain使用的字节数。这个数字值保证在上一次垃圾回收时时准确的MonitoringTotalAllocatedMemorySize
:这个Int64实例属性返回特定AppDomain已分配的字节数。这个数字只保证在上一次垃圾回收时是准确的MonitoringSuvivedMemorySize
:这个Int64实例属性返回特定AppDomain当前正在使用的字节数。这个数字只保证在上一次垃圾回收时是准确的MonitoringTotalProcessorTime
:这个TimeSpan实例返回特定AppDomain的CPU占用率
AppDomain FirstCance异常通知
每个AppDomain都可关联一组回调方法. 回调方法不能处理异常,也不能吞噬异常, 它们只是接收关于异常发生的通知. 要接收通知, 为AppDomain的实例事件FirstChanceException添加一个委托就就可以了.
- 异常首次抛出时,CLR调用向抛出异常的AppDomain登记的所有FirstChanceException回调方法。
- 然后。CLR查找栈上同一个AppDomain中的任何catch块,有一个catch块能处理异常,则异常处理完成,将继续执行
- 如果AppDomain中没有一个catch块能处理异常,则CLR沿着栈向上来到调用AppDomain,再次抛出同一异常对象(序列化和反序列化之后)
- 这时感觉就像是抛出一个全新新的异常,CLR调用当前AppDomain登记的所有FirstChanceException回调方法.
- 这个过程会一直执行,直到抵达线程栈顶部。如果异常还未处理,则进程终止.
宿主如何使用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是终止进程最得体的方式,因为它首先调用托管堆上的所有对象的Finalize方法,再释放CLR容纳的所有非托管com对象。最后,exit调用win32
ExitProcess函数
。
高级宿主控制
使用托管代码管理CLR
System.AppDomainManager类
允许宿主使用托管代码(而不是非托管代码)覆盖CLR的默认行为。你唯一要做的就是定义自己的类,让它从System.AppDomainManager派生
,重写想要接手控制的任何虚方法。然后,在专用的程序集中生成类,并将程序集安装到GAC中。(GAC中的所有程序集都总是被授予完全信任权限).
写健壮的宿主应用程序
托管代码出现错误时,宿主可告诉CLR采取什么行动。
- 如果线程执行时间过长,CLR可终止线程并返回一个响应。
- CLR可卸载AppDomain。这会终止线程并返回一个响应。
- CLR可被禁用。这会阻止更多的托管代码在程序中运行,但仍允许非托管代码运行。
- CLR 可退出Windows进程。首先会终止所有线程,并卸载所有AppDomain,使资源清理操作得以执行,然后才会终止进程。
CLR可以得体地或者粗鲁地终止线程或者AppDomain.
- 得体地: 会执行资源清理代码, finally块会运行, 对象的Finalize方法也将被执行.
- 如果正在一个catch块或者finally块中的线程, 则无法终止.
- 粗鲁地: 清理代码不会执行, finally和Finalize可能不会执行.
- 能终止在一个catch块或者finally块中的线程
- 非托管代码或约束执行区中的线程完全无法终止.
宿主如何拿回它的线程
客户端向服务器发送请求
服务器线程获得请求,把它派发给一个线程池线程来执行实际工作。
线程池线程获得客户端的请求,执行由构建并测试宿主应用程序的那个公司写的可信代码
可信代码进入一个try块。从这个try块中,跨越一个AppDomain的边界进行调用(通过派生自MarshalByRefObject的一个类型)。AppDomain中包含的是不可信代码(可能是存储过程),这些代码不是由制作宿主应用程序的那个公司生成和测试的可信代码。在这个时候,服务器相当于把它的线程的控制权交给了一些不可信的代码,服务器感到有点紧张了。
宿主会记录接收到客户端请求的时间。不可信代码在管理员设定的时间内没有对客户端做出响应,宿主就会调用Thread的
Abort方法
要求CLR终止线程池线程,强制它抛出一个ThreadAbortException
。这时,线程池线程开始展开(unwind),调用finally块,使清理代码得以执行。最后,线程池线程穿越
AppDomain
边界返回。由于宿主的存根代码是从一个try块
中调用不可信代码,所以宿主的存根代码有一个catch块
捕捉ThreadAbortException
。为了响应捕捉到的
ThreadAbortException异常
,宿主调用Thread的ResetAbort
方法。现在,宿主代码已捕捉到
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异常的代码检查。宿主可利用这个功能让自己的处理代码知道它为什么要中止线程。