CLR 宿主环境

Microsoft .NET Framework公共语言运行时 (CLR) 是一个执行许多新式编程语言(包括 Microsoft Visual C#、Microsoft Visual Basic 和 Microsoft Visual C++)的环境。 CLR 具有收集垃圾的内存、抢先线程化、元数据服务(类型反射)、代码可验证和代码访问安全性等特点。 CLR 使用元数据来完成以下任务:查找和加载类、在内存中安排实例、解析方法调用、生成本机代码、强制安全性以及设置运行时上下文边界。

CLR 和 SQL Server在运行时环境处理内存、线程和同步的方式上有所不同。 本主题说明如何集成这两种运行时以便统一管理所有系统资源, 本主题还介绍了如何集成 CLR 代码访问安全性 (CAS) 和SQL Server安全性,为用户代码提供可靠且安全的执行环境。

CLR 体系结构的基本概念

在 .NET Framework 中,程序员使用高级语言实现定义其结构的类(例如,类的字段或属性)和方法。 其中某些方法可能是静态函数。 程序的编译将生成一个名为 程序集的文件,该文件包含 Microsoft 中间语言 (MSIL) 的已编译代码,以及一个清单,其中包含对依赖程序集的所有引用。

注意

程序集是 CLR 体系结构中的关键元素。 它们是 .NET Framework 中对应用程序代码进行打包、部署和版本化的单位。 通过使用程序集,可以在数据库内部署应用程序代码并以统一方式管理、备份和还原完整的数据库应用程序。

程序集清单包含有关程序集的元数据,描述在程序中定义的所有结构、字段、属性、类、继承关系、函数和方法。 该清单确定程序集标识,指定组成程序集实现的文件,指定组成程序集的类型和资源,列举对其他程序集的编译时依赖项,并指定确保程序集正常运行所需的权限集。 在运行时使用此信息来解析引用,强制执行版本绑定策略,并验证已加载的程序集的完整性。

.NET Framework 支持自定义属性,这些属性使用应用程序可以在元数据中捕获的其他信息来对类、属性、函数和方法进行批注。 所有 .NET Framework 编译器不需要解释就可以使用这些批注并将它们作为程序集元数据存储。 可以像检查任何其他元数据那样检查这些批注。

托管代码是在 CLR 中执行而非直接由操作系统执行的 MSIL。 托管代码应用程序可以获得 CLR 服务,例如自动垃圾收集、运行时类型检查和安全支持等。 这些服务有助于提供托管代码应用程序的统一平台和语言无关行为。

CLR 集成的设计目标

当用户代码在名为 CLR 集成) SQL Server (CLR 托管的环境中运行时,以下设计目标适用:

可靠性(安全性)

不应允许用户代码执行损害数据库引擎进程完整性的操作,例如弹出请求用户响应的消息框或退出进程。 用户代码应不能覆盖数据库引擎内存缓冲区或内部数据结构。

可伸缩性

SQL Server和 CLR 具有不同的内部模型用于计划和内存管理。 SQL Server支持协作的、非抢占式线程模型,在此模型中,线程自愿定期执行,或者在等待锁或 I/O 时执行。 CLR 则支持抢先的线程化模型。 如果在 SQL Server 内部运行的用户代码可以直接调用操作系统线程基元,则它无法很好地集成到SQL Server任务计划程序中,并且可能会降低系统的可伸缩性。 CLR 不区分虚拟内存和物理内存,但SQL Server直接管理物理内存,需要在可配置的限制内使用物理内存。

不同的线程化、计划和内存管理的模型给需要支持成千上万的并发用户会话的关系数据库管理系统 (RDBMS) 带来了集成挑战。 体系结构应确保直接调用线程化、内存和同步基元的应用程序编程接口 (API) 的用户代码不损害系统的可伸缩性。

安全性

访问数据库对象(如表和列)时,数据库中运行的用户代码必须遵循SQL Server身份验证和授权规则。 此外,数据库管理员应能从在数据库中运行的用户代码控制对操作系统资源的访问,如文件和网络访问。 这对于托管编程语言很重要,因为与诸如 Transact-SQL 之类的非托管语言不同,托管编程语言提供访问这类资源的 API。 系统必须为用户提供一种安全的方式来访问数据库引擎进程外部的计算机资源。 有关详细信息,请参阅 CLR Integration Security

性能

在数据库引擎中运行的托管用户代码的计算性能应与服务器外部运行的相同代码相当。 从托管用户代码访问数据库的速度不如本机 Transact-SQL 快。 有关详细信息,请参阅 CLR 集成的性能

CLR Services

CLR 提供了许多服务来帮助实现 CLR 与 SQL Server 集成的设计目标。

类型安全验证

类型安全代码是仅以定义完善的方式访问内存结构的代码。 例如,给定有效的对象引用,类型安全代码可以按对应于实际字段成员的固定偏移量来访问内存。 但是,如果代码以任意偏移量访问内存,该偏移量可能超出也可能不超出属于该对象的内存范围,则它就不是类型安全的代码。 在 CLR 中加载程序集时,在使用实时 (JIT) 编译方式编译 MSIL 之前,运行时执行验证阶段,该阶段检查代码以确定其类型是否安全。 成功通过此验证的代码称为可验证的类型安全代码。

应用程序域

CLR 支持应用程序域作为主机进程内的执行区的概念,在其中可以加载和执行托管代码程序集。 应用程序域边界提供程序集之间的隔离。 根据静态变量和数据成员的可见性以及是否可以动态调用代码对这些程序集进行隔离。 应用程序域也提供加载和卸载代码的机制。 只能通过卸载应用程序域,从内存中卸载代码。 有关详细信息,请参阅 应用程序域和 CLR 集成安全性

代码访问安全性 (CAS)

CLR 安全系统通过给代码分配权限,提供了一种控制托管代码可以执行何种操作的方法。 基于代码标识(例如,程序集的签名或代码的来源)分配代码访问权限。

CLR 提供了一种可由计算机管理员设置的计算机范围的策略。 此策略为在计算机上运行的所有托管代码定义权限授予方式。 此外,主机级安全策略(如 SQL Server)可用于指定对托管代码的其他限制。

如果 .NET Framework 中的托管 API 公开针对由代码访问权限保护的资源的操作,该 API 在访问资源前将需要该权限。 这将导致 CLR 安全系统触发对调用堆栈中的每个代码单元(程序集)的全面检查。 只有整个调用链具有权限时,才能授予对资源的访问权限。

请注意,在 SQL Server 的 CLR 托管环境中,不支持使用 Reflection.Emit API 动态生成托管代码的功能。 此类代码将不具有 CAS 运行权限,因此在运行时会失败。 有关详细信息,请参阅 CLR 集成代码访问安全性

宿主保护属性 (HPA)

CLR 提供一种机制,用于使用宿主 CLR 可能所需的特定属性对属于 .NET Framework 的托管 API 进行批注。 这类属性的示例包括:

  • SharedState,它指示 API 是否公开创建或管理共享状态(例如,静态类字段)的功能。

  • Synchronization,它指示 API 是否公开在线程之间执行同步的功能。

  • ExternalProcessMgmt,它指示 API 是否公开控制主机进程的方法。

在给定这些属性的基础上,宿主可指定在宿主环境下不允许的 HPA 的列表(如 SharedState 属性)。 在这种情况下,CLR 拒绝用户代码调用某些 API,这些 API 由禁止列表中的 HPA 批注。 有关详细信息,请参阅 主机保护属性和 CLR 集成编程

SQL Server 如何与 CLR 协同工作

本部分讨论SQL Server如何集成 SQL Server 和 CLR 的线程、计划、同步和内存管理模型。 具体而言,本节将在可伸缩性、可靠性和安全性目标方面来介绍集成。 当 CLR 托管在 SQL Server 内时,SQL Server实质上充当 CLR 的操作系统。 CLR 调用由 SQL Server 实现的低级别例程,用于线程、计划、同步和内存管理。 这些基元与SQL Server引擎的其余部分使用的基元相同。 这种方法确保了系统的可伸缩性、可靠性和安全性。

可伸缩性:公共线程化、计划和同步

CLR 调用SQL Server API 来创建线程,以便运行用户代码和供其内部使用。 为了在多个线程之间同步,CLR 调用SQL Server同步对象。 这样,SQL Server计划程序就可以在线程等待同步对象时计划其他任务。 例如,在 CLR 启动垃圾收集时,所有线程均要等待垃圾收集完成。 由于SQL Server计划程序知道 CLR 线程和它们正在等待的同步对象,因此SQL Server可以计划运行不涉及 CLR 的其他数据库任务的线程。 这也使SQL Server能够检测涉及 CLR 同步对象采取的锁的死锁,并采用传统技术删除死锁。

托管代码在 SQL Server 中抢占式运行。 SQL Server计划程序能够检测和停止长时间未产生的线程。 将 CLR 线程挂钩到SQL Server线程的功能意味着SQL Server计划程序可以识别 CLR 中的“失控”线程并管理其优先级。 挂起此类逃逸线程并将它们放回队列中。 在一段规定的时间内不允许运行重复标识为逃逸线程的线程,以便执行其他工作线程。

注意

将自动生成访问数据或分配足够内存来触发垃圾收集的长时间运行的托管代码。 对于不访问数据或分配足够内存来触发垃圾收集的长时间运行的托管代码,应通过调用 .NET Framework 的 System.Thread.Sleep() 函数显式生成它。

可伸缩性:公共内存管理

CLR 调用SQL Server基元来分配和取消分配其内存。 由于 CLR 使用的内存在系统的总内存使用量中考虑,因此SQL Server可以保持在其配置的内存限制范围内,并确保 CLR 和SQL Server不会相互争用内存。 SQL Server还可以在系统内存受限时拒绝 CLR 内存请求,并在其他任务需要内存时要求 CLR 减少其内存使用量。

可靠性:应用程序域和无法恢复的异常

.NET Framework API 中的托管代码遇到严重异常(如内存不足或堆栈溢出)时,并不是总能从这类故障中恢复并确保实现的一致和正确的语义。 这些 API 会引发线程中止异常来应对这类故障。

托管在 SQL Server 中时,此类线程中止的处理方式如下:CLR 检测发生线程中止的应用程序域中的任何共享状态。 CLR 通过检查同步对象的存在来做到这点。 如果应用程序域中存在共享状态,则卸载应用程序域本身。 卸载应用程序域将停止当前在该应用程序域中运行的数据库事务。 由于共享状态的存在可能会扩大此类关键异常对触发异常的用户会话的影响,因此SQL Server和 CLR 已采取措施降低共享状态的可能性。 有关详细信息,请参阅 .NET Framework 文档。

安全性:权限集

SQL Server允许用户指定部署到数据库中的代码的可靠性和安全性要求。 当程序集上载到数据库中时,程序集作者可为该程序集指定以下三个权限集中的一个:SAFE、EXTERNAL_ACCESS 和 UNSAFE。

权限集 SAFE EXTERNAL_ACCESS UNSAFE
代码访问安全性 仅执行 执行和访问外部资源 非受限
编程模型限制 无限制
可验证性要求
调用本机代码的能力

SAFE 是最可靠和安全的模式,并且在允许的编程模型方面也具有相关的限制。 给 SAFE 程序集授予了足够的权限,以便运行、执行计算以及访问本地数据库。 SAFE 程序集需要具有可验证的类型安全性,并且不允许调用非托管代码。

UNSAFE 用于仅能由数据库管理员创建的高度受信任的代码。 这种受信任代码没有代码访问安全性限制,并且可以调用非托管(本机)代码。

EXTERNAL_ACCESS 提供了一个中间安全选项,允许代码访问数据库外部的资源,而且仍然具有与 SAFE 相同的可靠性。

SQL Server使用主机级 CAS 策略层来设置主机策略,该策略基于存储在 SQL Server 目录中的权限集授予三组权限之一。 在数据库内运行的托管代码始终获取这些代码访问权限集中的一个。

编程模型限制

SQL Server 中托管代码的编程模型涉及编写函数、过程和类型,这些函数、过程和类型通常不需要使用跨多个调用保留的状态或跨多个用户会话共享状态。 而且,如前文所述,共享状态的存在可导致能够影响应用程序的可伸缩性和可靠性的严重异常。

鉴于这些注意事项,我们不建议使用SQL Server中使用的类的静态变量和静态数据成员。 对于 SAFE 和EXTERNAL_ACCESS程序集,SQL Server在 CREATE ASSEMBLY 时间检查程序集的元数据,如果发现使用静态数据成员和变量,则无法创建此类程序集。

SQL Server还禁止调用使用 SharedStateSynchronizationExternalProcessMgmt 主机保护属性批注的 .NET Framework API。 这可以防止 SAFE 和EXTERNAL_ACCESS程序集调用任何启用共享状态、执行同步和影响SQL Server进程完整性的 API。 有关详细信息,请参阅 CLR 集成编程模型限制

另请参阅

CLR 集成安全性
CLR 集成的性能