CLR via C#, 4th -- 【核心机制】 -- 第22章CLR寄宿和AppDomain

22.1 CLR寄宿

NET Framework在Windows平台的顶部运行。这意味着.NET Framework必须用Windows能理解的技术来构建。首先,所有托管模块和程序集文件都必须使用Windows PE文件格式,而且要么是Windows EXE文件,要么是DLL文件。
开发CLR时,Microsoft实际是把它实现成包含在一个DLL中的COM服务器。也就是说,Microsoft为CLR定义了一个标准的COM接口,并为该接口和COM服务器分配了GUID.
安装.NET Framework时,代表CLR的COM服务器和其他COM服务器一样在Windows注册表中注册。

“垫片”(shim)
任何Windows应用程序都能寄宿(容纳)CLR。非托管宿主应该调用MetaHost.h文件中声明的CLRCreatelnstance函数来创建CLR COM服务器的实例
CLRCreateInstance函数在MSCorEE.dll文件中实现,该文件一般在C:WindowslSystem32目录中。这个DLL被人们亲切地称为“垫片”(shim),它的工作是决定创建哪个版本的CLR。
一台机器可安装多个版本的CLR,但只有一个版本的MSCorEE.dll文件(垫片)。机器上安装的MSCorEE.dll是与机器上安装的最新版本的CLR一起发布的那个版本。所以,该版本的MSCorEE.dll知道如何查找机器上的老版本CLR。

CLRCreatelnstance函数可返回一个ICLRMetaHost接口。宿主应用程序可调用这个接口的GetRuntime函数,指定宿主要创建的CLR的版本。然后,垫片将所需版本的CLR加载到宿主的进程中。GetRuntime函数返回指向非托管ICLRRuntimelnfo接口的指针。有了这个指针后,就可利用GetInterface方法获得ICLRRuntimeHost接口。

 宿主应用程序可调用ICLRRuntimeHost接口定义的方法来做下面这些事情:

  • 设置宿主管理器。告诉CLR宿主想参与涉及以下操作的决策:内存分配、线程调度/同步以及程序集加载等。宿主还可声明它想获得有关垃圾回收启动和停止以及特定操作超时的通知。
  • 获取CLR管理器。告诉CLR阻止使用某些类/成员。另外,宿主能分辨哪些代码可以调试,哪些不可以,以及当特定事件(例如AppDomain卸载、CLR停止或者堆栈溢出异常)发生时宿主应调用哪个方法。
  • 初始化并启动CLR.
  • 加载程序集并执行其中的代码。
  • 停止CLR,阻止任何更多的托管代码在Windows进程中运行。

在.NET Framework4之前,CLR只允许它的一个实例寄宿在Windows进程中。

22.2 AppDomain

CLR COM服务器初始化时会创建一个AppDomain.AppDomain是一组程序集的逻辑容器。
AppDomain是为了提供隔离而设计的。
AppDomain的具体功能:

  • 一个AppDomain的代码不能直接访问另一个AppDomain的代码创建的对象
    一个AppDomain中的代码要访问另一个AppDomain中的对象,只能使用“按引用封送”(marshal-by-reference)或者“按值封送”(marshal-by-value)的语义。
  • AppDomain可以卸载
    CLR不支持从AppDomain中卸载特定的程序集。但可以告诉CLR卸载一个AppDomain,从而卸载该AppDomain当前包含的所有程序集。
  • AppDomain可以单独保护
    AppDomain创建后会应用一个权限集,它决定了向这个AppDomain中运行的程序集授予的最大权限。
  • AppDomain可以单独配置
    AppDomain创建后会关联一组配置设置。这些设置主要影响CLR在AppDomain中加载程序集的方式。涉及搜索路径、版本绑定重定向、卷影复制以及加载器优化。

两个AppDomain的Loader堆会为相同的类型分别分配一个类型对象;类型对象的内存不会由两个AppDomain共享。
AppDomain中立
了减少资源消耗,MSCorLib.dll程序集以一种"AppDomain中立”的方式加载。也就是说,针对以"AppDomain中立”的方式加载的程序集,CLR会为它们维护一个特殊的Loader堆。该Loader堆中的所有类型对象,以及为这些类型定义的方法JIT编译生成的所有本机代码,都会由进程中的所有AppDomain共享。
以"AppDomain中立”的方式加载的所有程序集永远不能卸载。要回收它们占用的资源,唯一的办法就是终止Windows进程,让Windows去回收资源。
跨越AppDomain边界访问对象

  • “按引用封送”(Marshal-by-Reference)类型
  • “按值封送”(Marshal-by-Value)类型
  • 完全不能封送的类型

由于一个Windows进程可包含多个AppDomain,所以线程能执行一个AppDomain中的代码,再执行另一个AppDomain中的代码。

22.3 卸载AppDomain

卸载AppDomain的办法是调用AppDomain的静态Unload方法。
CLR执行一系列操作来得体地卸载指定的AppDomain.
1.CLR挂起进程中执行过托管代码的所有线程。
2.CLR检查所有线程栈,查看哪些线程正在执行要卸载的AppDomain中的代码,或者哪些线程会在某个时候返回至要卸载的AppDomain。任何栈上有要卸载的AppDomain,CLR都会强迫对应的线程抛出一个ThreadAbortException(同时恢复线程的执行),这将导致线程展开(unwind),并执行遇到的所有finally块以清理资源。如果没有代码捕捉ThreadAbortException,它最终会成为未处理的异常,CLR会“吞噬”这个异常;线程会终止,但进程可继续运行。这是很特别的一点,因为对于其他所有未经处理的异常,CLR都会终止进程。
3.当第2步发现的所有线程都离开AppDomain后,CLR遍历堆,为引用了“由已卸载的AppDomain创建的对象”的每个代理对象都设置一个标志(flag),这些代理对象现在知道它们引用的真实对象已经不在了。现在,任何代码在无效的代理对象上调用方法都会抛出一个AppDomainUnloadedException异常。
4.CLR强制垃圾回收,回收由己卸载的AppDomain创建的任何对象的内存。这些对象的Finalize方法被调用,使对象有机会正确清理它们占用的资源。
5.CLR恢复剩余所有线程的执行。调用AppDomain.Unload方法的线程将继续运行;对AppDomain.Unload的调用是同步进行的。

22.4 监视AppDomain

由于AppDomain监视本身也会产生开销,所以宿主必须将AppDomain的静态MonitoringEnabled属性设为true,从而显式地打开监视。监视一旦打开便不能关闭;将MonitoringEnabled属性设为false会抛出一个ArgumentException异常。
监视打开后,AppDomain类提供的4个只读属性:(前三个数字只保证在上一次垃圾回收时是准确的。)

  • MonitoringSurvivedProcessMemorySize
    这个Int64静态属性返回由当前CLR实例控制的所有AppDomain使用的字节数。
  • MonitoringTotalAllocatedMemorySize
    这个Int64实例属性返回特定AppDomain已分配的字节数。
  • MonitoringSurvivedMemorySize
    这个Int64实例属性返回特定AppDomain当前正在使用的字节数。
  • MonitoringTotalProcessorTime
    这个TimeSpan实例属性返回特定AppDomain的CPU占用率。

下面这个类演示了如何利用这些属性检查两个时间点之间一个AppDomain发生的变化:

private sealed class AppDomainMonitorDelta : IDisposable {  
   private AppDomain m_appDomain;  
   private TimeSpan m_thisADCpu; 
   private Int64 m_thisADMemoryInUse; 
   private Int64 m_thisADMemoryAllocated;  
 
   static AppDomainMonitorDelta() { 
      // Make sure that AppDomain monitoring is turned on 
      AppDomain.MonitoringIsEnabled = true;  
   } 
 
   public AppDomainMonitorDelta(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(“FriendlyName={0}, CPU={1}ms”, m_appDomain.FriendlyName, 
         (m_appDomain.MonitoringTotalProcessorTime  ­ m_thisADCpu).TotalMilliseconds);  
      Console.WriteLine(“   Allocated {0:N0} bytes of which {1:N0} survived GCs”,  
         m_appDomain.MonitoringTotalAllocatedMemorySize  ­ m_thisADMemoryAllocated, 
         m_appDomain.MonitoringSurvivedMemorySize ­ m_thisADMemoryInUse); 
    } 
}
View Code

以下代码演示了如何使用AppDomainMonitorDelta类:

private static void AppDomainResourceMonitoring() {  
    using (new AppDomainMonitorDelta(null)) { 
       // Allocate about 10 million bytes that will survive collections  
       var list = new List<Object>(); 
       for (Int32 x = 0; x < 1000; x++) list.Add(new Byte[10000]);  
 
       // Allocate about 20 million bytes that will NOT survive collections 
       for (Int32 x = 0; x < 2000; x++) new Byte[10000].GetType();  
 
       // Spin the CPU for about 5 seconds 
       Int64 stop = Environment.TickCount + 5000; 
       while (Environment.TickCount < stop) ; 
   } 
}

22.5 AppDomain FirstChance异常通知

FirstChanceException

22.6宿主如何使用AppDomain

22.6.1 可执行应用程序

应用程序都是自寄宿(self-hosted,即自己容纳CLR)的应用程序,它们都有托管EXE文件。Windows用托管EXE文件初始化进程时,会加载垫片。垫片检查应用程序的程序集(EXE文件)中的CLR头信息。头信息指明了生成和测试应用程序时使用的CLR版本。垫片根据这些信息决定将哪个版本的CLR加载到进程中,CLR加载并初始化好之后,会再次检查程序集的CLR头,判断哪个方法是应用程序的入口方(Main),CLR调用该方法,此时应用程序才真正启动并运行起来。

22.6.2 Microsoft Silverlight富Internet应用程序

22.6.3 Microsoft ASP.NET和XML Web服务应用程序

ASPNET作为一个ISAPI DLL(ASPNetISAPL.dl1)实现。客户端首次请求由这个DLL处理的URL时,ASP.NET会加载CLR。
客户端请求一个Web应用程序时,ASP.NET判断这是不是第一次请求。如果是,ASP.NET要求CLR为该Web应用程序创建新AppDomain;每个Web应用程序都根据虚拟根目录来标识。
ASP.NET的一个亮点是允许在不关闭Web服务器的前提下动态更改网站代码。网站的文件在硬盘上发生改动时,ASP.NET会检测到这个情况,并卸载包含旧版本文件的AppDomain(在当前运行的最后一个请求完成之后),并创建一个新AppDomain,向其中加载新版本的文件。

22.6.4 Microsoft SQL Server

Microsoft SQL Server是非托管应用程序,它的大部分代码仍是用非托管C++写的。SQL Server允许开发人员使用托管代码创建存储过程。首次请求数据库运行一个用托管代码写内存储过程时,SQL Server会加载CLR。

22.7高级宿主控制

22.7.1 使用托管代码管理CLR

System.AppDomainManager类允许宿主使用托管代码(而不是非托管代码)覆盖CLR的默认行为。
AppDomainManager派生类的作用是使宿主保持控制权,即使是在加载项(add-in)试图创建自己的AppDomain时。

22.7.2 写健壮的宿主应用程序

托管代码出现错误时,宿主可告诉CLR采取什么行动。

  • 如果线程执行时间过长,CLR可终止线程并返回一个响应。
  • CLR可卸载AppDomain。这会终止该AppDomain中的所有线程,导致有问题的代码卸载。
  • CLR可被禁用。这会阻止更多的托管代码在程序中运行,但仍然允许非托管代码运行
  • CLR可退出Windows进程。首先会终止所有线程,并卸载所有AppDomain,使资源i理操作得以执行,然后才会终止进程。

22.7.3 宿主如何拿回它的线程

1.客户端向服务器发送请求。
2,服务器线程获得请求,把它派发给一个线程池线程来执行实际工作。
3,线程池线程获得客户端的请求,执行由构建并测试宿主应用程序的那个公司写的可信代码(trusted code)。
4·可信代码进入一个try块。从这个try块中,跨越一个AppDomain的边界进行调用(通过派生自MarshalByRefObject的一个类型),AppDomain中包含的是不可信代码(可能是存储过程),这些代码不是由制作宿主应用程序的那个公司生成和测试的。在这个时候,服务器相当于把它的线程的控制权交给了一些不可信的代码。服务器感到有点儿“紧张”了。
5,宿主会记录接收到客户端请求的时间。不可信代码在管理员设定的时间内没有对客户端做出响应,宿主就会调用Thread的Abort方法要求CLR中止线程池线程,强制它抛出一个ThreadAbortException异常。
6,这时,线程池线程开始展开(unwind),调用finally块,使清理代码得以执行。最后,线程池线程穿越AppDomain边界返回。由于宿主的存根代码是从一个try块中调用不可信代码,所以宿主的存根代码有一个catch块捕捉ThreadAbortException.
7,为了响应捕捉到的ThreadAbortException异常,宿主调用Thread的ResetAbort方法。
稍后将介绍该调用的目的。
8,现在,宿主的代码已捕捉到ThreadAbortException异常。因此,宿主可向客户端返回某种形式的错误,允许线程池线程返回线程池,供未来的客户端请求使用。

posted @ 2019-10-23 20:35  FH1004322  阅读(129)  评论(0)    收藏  举报