CLR 集成体系结构 - CLR 托管环境

适用于:SQL ServerAzure SQL 托管实例

SQL Server 与 .NET Framework 公共语言运行时 (CLR) 的集成使数据库程序员能够使用 C#、Visual Basic .NET 和 Visual C++ 等语言。 函数、存储过程、触发器、数据类型和聚合即属于程序员可以用这些语言编写的业务逻辑种类。

CLR 具有垃圾收集的内存、抢先线程处理、元数据服务(类型反射)、代码可验证性和代码访问安全性。 CLR 使用元数据来完成以下任务:查找和加载类、在内存中安排实例、解析方法调用、生成本机代码、强制安全性以及设置运行时上下文边界。

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

CLR 体系结构的基本概念

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

注意

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

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

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

托管代码是在 CLR 中执行的,而不是由操作系统直接执行的。 托管代码应用程序可以获得 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 集成安全性

性能

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

CLR 服务

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

类型安全验证

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

应用程序域

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

代码访问安全性 (CAS)

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

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

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

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

主机保护属性 (HPA)

CLR 提供了一种机制,用于批注作为 .NET Framework 的一部分的托管 API,这些 API 具有某些可能对 CLR 主机感兴趣的属性。 这类属性的示例包括:

  • 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()的抢占模式(预计长时间运行的代码部分)来显式生成。 下面的代码示例演示如何使用这些方法手动生成。

例子

手动向 SOS 计划程序生成

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);
  }
}

使用 ThreadAffinity 抢先运行

在此示例中,CLR 代码在 BeginThreadAffinityEndThreadAffinity中以抢占模式运行。

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 允许用户指定部署到数据库中的代码的可靠性和安全要求。 将程序集上传到数据库中时,程序集的作者可以为该程序集指定三个权限集之一:SAFEEXTERNAL_ACCESSUNSAFE

功能 SAFE EXTERNAL_ACCESS UNSAFE
Code Access Security 仅执行 执行和访问外部资源 非受限
Programming model restrictions 无限制
Verifiability requirement
Ability to call native code

SAFE 是允许的编程模型方面具有关联限制的最可靠且最安全的模式。 SAFE 程序集具有足够的权限来运行、执行计算并有权访问本地数据库。 SAFE 程序集必须可验证类型安全,并且不允许调用非托管代码。

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

EXTERNAL_ACCESS 提供中间安全选项,允许代码访问数据库外部的资源,但仍具有 SAFE的可靠性保证。

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

编程模型限制

SQL Server 中托管代码的编程模型涉及编写函数、过程和类型,这些函数、过程和类型通常不需要在多个调用之间使用状态,也不需要跨多个用户会话共享状态。 此外,如前所述,共享状态的存在可能会导致影响应用程序的可伸缩性和可靠性的关键异常。

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

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