公共语言运行库支持多种不同的应用程序。例如,运行库可以运行 Web 服务器应用程序和控制台应用程序,以及带有传统的 Windows 用户界面的应用程序。
每种应用程序都需要使用一段名为运行库宿主的代码才能启动。运行库宿主会将运行库加载到一个进程中,在该进程中创建应用程序域,然后在这些应用程序域内加载并执行用户代码。
宿主概述
.NET 框架附带了支持几种常见情况的运行库宿主,其中包括 ASP.NET 所使用的宿主和 Microsoft Internet Explorer 所使用的宿主。.NET 框架 SDK 还提供了一种非托管 API,它可用于编写自定义的运行库宿主。例如,您可以为应用程序服务器产品编写自定义的运行库宿主,以用于同时运行来自多个用户的代码。利用自定义的运行库宿主,应用程序服务器的客户可以编写托管代码,这些代码将具有高度可缩放性、通用类型系统、多语言支持、自动内存管理等优点。如果将调试器等专用工具用作运行库宿主,它们就可以访问诸如在进程中运行的应用程序域列表等信息。
大多数运行库宿主都包括非托管代码和托管代码。非托管宿主代码将在进程开始时将运行库加载到进程中。当运行库加载到进程中后,就可以将控制权转移给宿主代码的托管部分,从而提高性能。通过实现宿主中与托管代码中的用户代码进行交互的部分,可获得更好的性能,因为宿主代码对用户代码的调用是在托管环境中进行的。如果用非托管代码来编写整个宿主,那么每当宿主代码与用户代码进行交互时,都需要将非托管代码转换为托管代码。
非托管宿主代码用于配置公共语言运行库,将其加载到进程中,并将程序转换为托管代码。而宿主代码的托管部分将创建用户代码运行时所在的应用程序域,并将用户请求调度给所创建的应用程序域。
将运行库加载到进程中
执行任何托管代码之前,宿主必须首先加载并初始化公共语言运行库。由于此时运行库尚未在进程中运行,所有宿主都将用非托管的 Stub 启动。.NET 框架提供了一组名为宿主 API 的非托管 API,宿主可以利用它们来启动运行库。有关更多信息,请参阅 .NET 框架 SDK 的《工具开发人员指南》中的公共语言运行库宿主接口规范。
为了将运行库加载到进程中,宿主将调用 CorBindToRuntimeEx,它位于 .NET 框架 SDK 的公共语言运行库宿主接口规范中。CorBindToRuntimeEx 的原型位于 .NET 框架 SDK 的“Include”目录下的 Mscoree.h 中。当调用 CorBindToRuntimeEx 时,宿主可以设置相应的值,以控制所加载的运行库版本和基本功能(如垃圾回收和程序集加载)的行为。宿主可以设置下表中所列的值。
值
说明
并行垃圾回收
指定垃圾回收是在后台线程上进行还是在运行用户代码的线程上进行。
有关更多信息,请参阅 .NET 框架 SDK 的《工具开发人员指南》中的公共语言运行库宿主接口规范。
加载程序优化
控制是否以非特定于域的方式来加载程序集。如果以非特定于域的方式加载程序集,一个进程中的所有应用程序域就能够共享程序集代码和只读运行库数据结构。
服务器与工作站
指定是加载工作站内部版本还是服务器内部版本。
有关更多信息,请参阅 .NET 框架 SDK 《工具开发人员指南》中的公共语言运行库宿主接口规范。
版本
指定将加载到进程中的运行库版本。
除了在调用 CorBindToRuntimeEx 时设置上表所述的值之外,宿主还可以请求指向 IcorRuntimeHost 的接口指针。利用该指针,宿主可以完成诸如设置配置选项和转换为托管宿主代码等任务,以创建应用程序域并执行用户代码。
宿主可以使用 IcorRuntimeHost 来配置运行库的各个方面(如垃圾回收),以将其加载到进程中或注册附加的事件。例如,宿主可以使用 GetConfiguration 来注册回调函数(当特定线程将要在调试器中停止时,该函数会收到通知)或指定垃圾回收堆的大小。
ICorRuntimeHost 还提供了 Start 和 Stop 方法,使宿主能够显式地控制运行库在进程中的生存期。当第一个托管代码在进程中运行时,将隐式调用 Start;当关闭进程时,将隐式调用 Stop。虽然宿主无需(通常也不会)显式调用这些方法,但在某些情况下,这样做还是有用的。例如,当宿主运行完托管代码,需要为节省内存和其他资源而从进程中卸载运行库时,它就可能需要显式调用这些方法。
转换为托管宿主代码
当加载并初始化运行库后,宿主必须从非托管代码转换为托管代码,以便执行托管宿主代码和用户代码。托管宿主代码通常在默认应用程序域中运行。每当运行库初始化时,它都将自动创建默认应用程序域。当关闭进程时,将卸载默认的应用程序域。大多数宿主都不在默认应用程序域中运行用户代码,因为它无法独立于进程关闭。
要转换为托管代码,宿主必须获取指向默认应用程序域的指针,然后将宿主的托管部分加载到该域中。当完成向托管代码的转换后,宿主的托管部分可以创建其他应用程序域,以查找用户代码或更多的宿主代码。
通过调用 ICorRuntimeHost::GetDefaultDomain,宿主可以获取指向默认应用程序域的接口指针。此调用将返回指向 System. AppDomain(表示默认应用程序域)实例的指针。接口指针的类型为 _AppDomain。简而言之,宿主通过 COM 互操作性服务来调用托管类 System.AppDomain 实例的方法,从而将托管宿主代码加载到默认域中。当宿主获取指向默认域的指针后,您就可以调用 System.AppDomain 的一种 Load 方法,将宿主的托管部分加载到默认域中。
宿主的托管部分通常包含宿主的大部分逻辑。当完成向托管代码的转换后,将不再需要非托管的宿主代码。由于宿主所加载和运行的用户代码都是托管代码,所以如果用托管代码实现宿主中的大部分,就可以提供更高的性能。性能之所以会提高,是因为宿主代码对用户代码的调用均是在托管环境中进行的,而不必每次宿主代码与用户代码进行交互时都在非托管代码和托管代码之间进行转换。
确定应用程序域边界
当宿主代码完成从非托管代码到托管代码的转换后,它必须新建一个或多个用于运行用户代码的应用程序域。应用程序域是宿主隔离在进程中运行的代码时所使用的构造。为了确保不应交互的代码无法进行交互,这种隔离是必要的。例如,如果代码从两个不同的 Web 站点下载到 Internet Explorer 宿主中,则必须将其隔离。为了确保这种隔离,Internet Explorer 宿主将为每个站点创建一个应用程序域。
宿主在创建用于运行用户代码的应用程序域之前,必须确定新应用程序域的边界位置。此决定的影响因素包括在限制特定类型对其他类型的访问、配置、安全性以及能否卸载不需要的代码等方面的要求。
限制对类型的访问
在一个应用程序域中运行的类型能够发现在该域中运行的其他类型并直接调用这些类型。但是,一个类型永远也无法发现在其他应用程序域中运行的类型,因此也无法调用这些类型。当确定在何处创建边界时,能否限制特定类型对其他类型的访问是需要考虑的主要事项。
配置设置
应用程序域是运行库中配置的基本单位。每个应用程序域都有一个可选的关联配置文件,该文件描述与该域中所运行代码相关的设置。
例如,配置文件中可以包含用于查找专用程序集的目录列表、共享程序集的版本绑定信息、可远程访问的类型的位置等。
安全
宿主可以为域设置代码访问安全策略和基于角色的安全策略。这样,宿主既能够控制向特定域中的代码授予的权限集,也能为基于角色的安全策略设置当前线程的原则和默认的原则。
例如,宿主可以定义应用程序域级别的代码访问安全策略,以确保在该域中只能运行从特定站点下载的代码。或者,宿主可以设置基于角色的安全原则,以实现自定义的身份验证方案。
代码卸载
要从内存中卸载在进程中运行的托管代码,以便将内存用于其他目的,宿主必须卸载代码运行时所在的应用程序域。不能卸载单独的程序集或类型。在何时可以卸载用户代码这一方面,宿主有其自己的规则。例如,Internet 浏览器可能会将托管控件加载到由站点确定的域中。此时,浏览器可能会应用相应的规则,在内存中保留最近查看过的网页,以便使“前进”和“后退”按钮更迅速地作出响应。当浏览器确定不再需要在内存中保留某页时,它将删除应用程序域,因此也删除了托管控件的代码。
创建和配置应用程序域
当宿主根据上一节所述的条件确定域边界的位置后,它将使用 System.AppDomain 类型的 CreateDomain 方法来创建用于运行用户代码的域。每个应用程序域都包含一个名称/值对的集合,宿主可以将有关域的信息存储在这些名称/值对中。然后,名称/值对将作为参数传递给 CreateDomain。
.NET 框架定义了大量运行库本身即可理解的属性。这些属性的名称由 System.AppDomain 类中的静态字符串来定义。宿主可以通过设置本身可理解的属性来自定义应用程序域。例如,这些属性可以控制如何隔离在不同域中运行的代码。为了使宿主能够定义用于存储方案特定信息的自定义属性,可以对名称/值对进行扩展。
总的说来,应用程序域提供的隔离具有两种形式:
应用程序域防止一个域中的类型发现和调用其他域中的类型,从而防止在一个应用程序域中运行的代码影响其他域。应用程序域将依赖于这样的事实:代码已经过验证,不会受到内存故障的损害。
宿主控制运行库从何处查找代码,以代表宿主将代码加载到特定的应用程序域中。这一点很重要,因为它将防止一个应用程序中的代码意外地影响其他应用程序。以这种方式控制代码加载请求范围的能力与 Microsoft Win32 和 COM 当前的工作方式有很大的区别。当前,在 Windows 中,由于任何应用程序都可以使用注册表中所述的任何代码或位于已知位置(如 Windows system 目录)中的任何代码,所以解析范围是整个计算机。以这种方式进行共享是当前的默认方式,而这种行为可能会导致 DLL 冲突。
除了确立代码加载方式的范围外,还必须将配置信息的范围缩小到一个应用程序。不过,对于多数配置设置,目前尚不太可能。
例如,如果配置一个远程计算机来运行 COM 类,那么特定类在注册表中的 RemoteServerName 键设置将影响所有使用该类的应用程序。与非有意的代码共享类似,非有意共享地配置数据将防止应用程序完全控制其自身的行为。
System.AppDomainFlags.ApplicationBase 和 System.AppDomainFlags.ConfigurationFile 属性分别控制以下两方面的能力:指定运行库查找程序集的目录;控制特定应用程序域的配置设置范围。
ApplicationBase 将为应用程序域建立一个根目录,运行库会在该目录下查找专用程序集。如果宿主允许从磁盘加载程序集,它必须提供一个 ApplicationBase,让运行库知道从何处查找已加载的程序集。
ConfigurationFile 属性可指定包含配置应用程序(这些应用程序在应用程序域中运行)设置的 XML 文件的名称。应用程序配置文件中的设置示例包括程序集版本控制规则,以及有关如何查找应用程序域中运行的类型可远程访问类型的指南。
加载和执行用户代码
之所以要编写宿主,是为了设置用于运行托管用户代码的应用程序环境。在此上下文中,用户代码是指任何不专门属于宿主一部分的托管代码。例如,对于 Internet Explorer 宿主,用户代码是组成 HTML 页的托管控件和脚本。对于应用程序服务器宿主,用户代码是包含应用程序服务器所管理和执行的企业业务规则的代码。
所有托管代码都是 Assembly 类的一部分。因此,可用来加载和运行托管代码的方法都基于程序集。例如,System.AppDomain 和 System.Reflection.Assembly 类包含使宿主能够加载程序集的方法。Load 方法具有各种形式:有些方法采用程序集名称,有些方法采用程序集清单所在文件的完整文件系统路径。这些方法用于加载先前已创建并保存到磁盘上的程序集。
例如,假设上述应用程序服务器宿主允许用户编写托管代码业务规则,以加载到应用程序服务器进程中运行。当对特定业务规则运行方法的请求发送到应用程序服务器中时,服务器运行库宿主代码将确定在哪一个域中运行代码,或者是否必须新建域。然后,运行库宿主代码使用一种程序集 Load 方法来加载包含业务规则的程序集,并使用反射来执行该业务规则上的方法。有关更多信息,请参阅 System.Reflection namespace 的文档。
System.Reflection.Emit 命名空间还提供了用于动态创建程序集的类型。当应用程序处理脚本代码时,将以这种方法加载程序集。
例如,字处理程序可能支持用户可用来自定义应用程序行为的宏语言。当加载运行库并创建应用程序域后,字处理程序可能会将宏脚本编译为托管代码,并使用 System.Reflection.Emit 创建一个程序集。然后,可以将所创建的程序集加载到应用程序域中运行。根据具体的情况,程序集可能仅存在于应用程序的生存期中(即从不保存到磁盘中)。
设置应用程序域级别的安全策略
.NET 框架既提供代码访问安全机制,也提供基于角色的安全机制。由于具有这两种安全机制,您能够对代码可执行的操作进行细微的控制。它还提供一种结构,使组件能够决定用户可以执行的操作。对于在宿主创建的应用程序域中运行的代码,宿主对两种安全机制都具有高度的控制。
当管理员和宿主使用代码访问安全机制时,无论是哪个用户在执行代码,都将根据代码本身的特性来确定代码可执行的操作。代码特性称作证据,它可以包括下载代码的 Web 站点或区域,或发布代码的供应商的数字签名。
当加载并运行代码时,代码访问安全机制会将此证据映射到一组权限。这些权限定义代码可以执行的特定操作。管理员或宿主会将特定的证据映射到向代码授予的权限。此映射称作安全策略。例如,管理员可能会创建一种安全策略,以便向下载自 Intranet 的代码授予一组较高的权限(如访问文件系统),而向下载自 Internet 的代码授予一组较低的权限。
宿主对应用程序域设置的安全策略称为应用程序域安全策略。管理员也会在企业、计算机和用户级别定义一些策略,以确定向代码授予的全部权限,这些策略和应用程序域安全策略之间存在相交的情况。请注意,应用程序域策略只能限制更高级策略(企业、计算机或用户)所授予的权限集。
宿主通过对 System.AppDomain 类调用 AppDomain.SetAppDomainPolicy 方法来设置应用程序域级别的策略。宿主只有在被授予了 SecurityPermission 来控制证据时才能设置应用程序域级别的策略。
设置基于角色的安全策略和原则
利用基于角色的安全策略,组件可以在运行时识别当前用户及其关联角色。然后,将使用代码访问安全策略映射此信息,以确定在运行时授予的权限集。宿主可以为给定的应用程序域设置基于角色的安全策略和当前安全原则。安全原则表示用户和与该用户相关联的角色。
基于角色的安全策略通常用于实现自定义的身份验证方案。例如,ASP.NET 宿主使用基于角色的安全策略来实现基于用户信息的身份验证方案,这些用户信息将从 Internet 信息服务 (IIS) 中获取。
用户和用户角色的定义都针对于特定的应用程序。应用程序可以具有不同于 Windows 的用户概念。例如,应用程序可以要求用户在登录到该应用程序时提供用户名和密码。该用户名/密码与用户登录到 Windows 时使用的用户名/密码无关。
宿主可以通过调用 System.AppDomain 类的 SetThreadPrincipal 方法来设置当前线程的规则。当前原则用作代码访问安全策略的输入。它确定给定原则是否可以执行指定的操作。
.NET 框架提供了默认原则的概念。此原则是在未显式设置原则时自动与运行线程相关联的原则。默认原则的内置值包括一个未经身份验证的用户和当前正在使用其帐户执行线程的 Windows 用户。宿主可以通过调用 System.AppDomain 类的 SetPrincipalPolicy 来更改默认原则。
卸载域和关闭进程
应用程序域可以在不停止整个进程的情况下卸载。宿主可以利用这一特点来卸载不再需要的代码,从而减少内存占用并增加其应用程序的可缩放性。
System.AppDomain 类包括一种名为 Unload 的静态方法,宿主可以使用此方法来卸载特定的应用程序域。AppDomain.Unload 将执行正常关机,只要存在任何活动线程,就不会将域卸载。
如果程序集已加载到默认域中或者已经以非特定于域的形式加载,除非关闭整个进程或从进程中卸载运行库,否则无法卸载这些程序集。
ICorRuntimeHost 接口包括一个名为 Stop 的方法,宿主可以使用该方法从进程中强制卸载运行库。当调用 Stop 时,将立即卸载所有域(包括默认域和所有非特定于域的代码),并从进程中全部移除运行库。当对进程调用 Stop 后,不能将运行库加载回该进程。要再次开始运行托管代码,必须创建一个新的进程。
总结
上面是VS.NET中公共运行库的一些主要的特性和方法介绍,给大家参考一下。有任何建议请MAIL我 paulni@citiz.net。