CLR 集成体系结构 - CLR 宿主环境

适用于: SQL Server Azure SQL 托管实例

SQL Server 与 .NET Framework 公共语言运行时 (CLR) 集成使数据库程序员能够使用 Visual C#、Visual Basic .NET 和 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 集成的设计目标

当用户代码在 SQL Server(称为 CLR 集成)的 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 的线程、计划、同步和内存管理模型。 具体而言,本节将在可伸缩性、可靠性和安全性目标方面来介绍集成。 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 中的“失控”线程并管理其优先级。 挂起此类逃逸线程并将它们放回队列中。 在一段规定的时间内不允许运行重复标识为逃逸线程的线程,以便执行其他工作线程。

在某些情况下,长时间运行的托管代码将自动产生,在某些情况下,它不会生成。 在以下情况下,长时间运行的托管代码将自动产生:

  • 如果代码调用 SQL OS(例如查询数据)
  • 如果分配了足够的内存来触发垃圾回收
  • 如果代码通过调用 OS 函数进入抢占模式

不执行上述任何操作的代码(例如仅包含计算的紧密循环)不会自动生成计划程序,这可能会导致系统中的其他工作负荷长时间等待。 在这些情况下,开发人员应通过调用 .NET Framework 的 System.Thread.Sleep() 函数或显式进入 System.Thread.BeginThreadAffinity()的预制模式()来显式生成,这些代码部分预计会长时间运行。 下面的代码示例演示如何使用这些方法手动生成。

// Example 1: Manually yield to SOS scheduler.
for (int i = 0; i < Int32.MaxValue; i++)
{
 // *Code that does compute-heavy operation, and does not call into
 // any OS functions.*

 // Manually yield to the scheduler regularly after every few cycles.
 if (i % 1000 == 0)
 {
   Thread.Sleep(0);
 }
}
// Example 2: Use ThreadAffinity to run preemptively.
// Within BeginThreadAffinity/EndThreadAffinity the CLR code runs in preemptive mode.
Thread.BeginThreadAffinity();
for (int i = 0; i < Int32.MaxValue; i++)
{
  // *Code that does compute-heavy operation, and does not call into
  // any OS functions.*
}
Thread.EndThreadAffinity();
可伸缩性:公共内存管理

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
代码访问安全性 仅执行 执行和访问外部资源 非受限
编程模型限制 无限制
可验证性要求
调用本机代码的能力 No

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 集成的性能