65.9K
CodeProject 正在变化。 阅读更多。
Home

SafeHandle 和受约束的执行区域

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.30/5 (7投票s)

2006年10月30日

CPOL

8分钟阅读

viewsIcon

39565

解释了新的 SafeHandle 类和受约束的执行区域,并展示了如何使用它们。

引言

在 .NET 中编程的最大好处之一是您不必像“过去”那样了解 Win32 API 集和 MFC。编写托管代码比使用 Win32 API 编写 C 或 C++ 代码要简单得多。

虽然这些说法都是事实,但它们只在一定程度上成立。在托管世界中,仍然有些事情您无法做到。这种情况可能不常发生,但总有一天您需要编写一个需要使用非托管资源才能工作的托管类型。幸运的是,.NET 提供了 P/Invoke 层。P/Invoke 是“Platform Invoke”的缩写,其中“platform”是指非托管的 Win32 API 集。

实际上,您使用的 Win32 API 比您意识到的要多得多。许多 Framework BCL(Base Class Library)类实际上会为您处理这个问题,而不会向您公开非托管资源。这种方法隐藏了使用非托管资源的复杂性,更重要的是,它确保在不再需要资源时能够正确地释放资源。

大多数这些非托管类型都是句柄。句柄本质上是指向内存一部分的指针。

在早期版本的 .NET Framework(1.0 和 1.1 版本)中,所有操作系统句柄只能由 IntPtr 对象封装。这提供了一种非常方便的托管方式来与本机(非托管)代码进行互操作;但是,它没有提供任何安全或可靠性措施。由于缺乏这些功能,IntPtr 允许句柄泄露,尤其是在异步异常的情况下。这些异常对垃圾收集器(GC)清理这些资源造成了巨大的障碍。有时,对象可能会在执行平台调用中的方法时被垃圾回收。释放传递给平台调用的句柄会导致句柄损坏。

更重要的是,由于句柄是操作系统工作方式的核心,Windows 会非常积极地回收和重新使用句柄。因此,句柄可能会被回收以指向包含敏感数据的另一个资源。这被称为回收攻击,可能导致数据损坏并构成安全威胁。

作为 Microsoft 持续改进 .NET Framework 的努力的一部分,他们认识到这些问题并着手解决。这项努力的最终成果是 SafeHandleContrainedExecutionRegion (CER),两者都在 Framework 2.0 版本中引入。

SafeHandle

在当今的托管代码中,SafeHandle 是表示句柄的最佳方式。SafeHandle 类代表操作系统句柄的托管包装器,旨在解决使用句柄的可靠性和安全问题。SafeHandle 的目标是保证托管代码不会泄露句柄。

为了做到这一点,SafeHandle 上有一个终结器,以确保句柄被关闭。由于 SafeHandle 继承自 CriticalFinalizerObject,因此它的终结器保证会运行而不会被中止,并且它会在其他对象的终结器之后运行。

SafeHandle 实现终结器还有一个额外的好处。大多数以前使用 IntPtr 来包装句柄的类不再需要自己提供终结器。这有助于减少由于不正确的 Dispose 模式而导致的错误和泄露数量,并有助于减少在等待您的终结器运行时处于活动状态的可收集对象的数量,从而帮助 GC 更快地回收内存。

SafeHandle 还与平台调用集成,它会在底层句柄每次传递到平台调用时自动增加内部引用计数,并在调用完成后递减。这可以防止在仍有挂起调用时释放句柄。

从 SafeHandle 派生

SafeHandle 是一个抽象类,这意味着您不能直接使用它。这迫使您创建一个派生类,该类专门表示您要封装的句柄。通过在平台调用定义(原型)中使用这些派生类,您可以为您的句柄获得类型安全性。

要创建 SafeHandle 派生类,您必须知道如何创建和释放您要封装的句柄。这对于不同的句柄来说是不同的,因为有些使用 CloseHandle,而有些使用更具体的方法,例如 UnmapViewOfFile。您还必须重写 IsInvalidReleaseHandle 方法。为了安全起见,默认构造函数应调用基构造函数,并提供一个表示无效句柄的值以及一个布尔值,该值指示本机句柄是否将由 SafeHandle 拥有。

正如您所见,创建 SafeHandle 派生类并不适合胆小的人。Framework 提供了一组预先编写的抽象类,它们从 SafeHandle 派生,为文件和操作系统句柄提供了一些通用功能,这些功能可用于帮助创建您自己的 SafeHandle 派生类。它们包含在 Microsoft.Win32.SafeHandles 命名空间中。

Microsoft.Win32.SafeHandles 命名空间公开了以下类

CriticalHandleMinusOneIsInvalid
Win32 关键句柄,其中 -1 表示无效句柄。

CriticalHandleZeroOrMinusOneIsInvalid
Win32 关键句柄,其中 0 或 -1 表示无效句柄。

SafeFileHandle
文件句柄包装器。此类不能被继承。

SafeHandleMinusOneIsInvalid
Win32 安全句柄,其中 -1 表示无效句柄。

SafeHandleZeroOrMinusOneIsInvalid
Win32 安全句柄,其中 0 或 -1 表示无效句柄。

SafeWaitHandle
等待句柄包装器。此类不能被继承。

示例

关于 SafeHandle 类的 MSDN 文档[^] 提供了一个非常完整的示例,演示了如何从 SafeHandleZeroOrMinusOneIsInvalid 派生一个自定义的安全句柄,用于操作系统文件句柄。

受约束的执行区域

受约束的执行区域 (CER) 提供代码执行的保证,即使在发生异常(如 ThreadAbortExcpetionOutOfMemoryExceptionStackOverflowException)时也能无中断地执行。CER 的存在是为了帮助编写代码以维护一致性,但它们不保证代码是*正确*的。CER 唯一保证的是区域内的代码将无中断地执行,不受异常干扰。

为了做出这种保证,CLR 会在执行任何代码指令之前,提前 JIT 编译 CER 中的任何代码。这使得在执行期间可能抛出的任何异常要么在代码运行之前遇到,要么在代码运行完成后遇到。此外,对可以在 CER 中执行的代码有一些限制。CER 的这两个方面提供了一种对代码是否会执行做出有力保证的方式。

CER 内部的限制

在实现 CER 时,开发人员必须遵循一些严格的规则

  • 不能调用任意的虚拟方法(除非它们已被提前准备好)
  • 不能分配内存
  • 只能调用具有足够强的可靠性契约的方法

关于不分配内存的限制可能是最严格的。CLR 在访问多维数组(但不是交错数组)、锁定代码段或编译器添加了装箱指令的任何时候都会分配内存。此外,某些平台调用定义需要分配。

运行时通过三种方式暴露 CER

  • try/finally 块的堆栈溢出安全版本,称为 ExecuteCodeWithGuaranteedCleanup
  • 调用 RuntimeHelpers.PrepareConstrainedRegions,紧接着是一个 try/finally。在这种情况下,try 块实际上不是受约束的,但所有 catchfinally 块都是。
  • 任何 CriticalFinalizerObject 的子类,它们具有一个被提前准备好的终结器。

可靠性契约

可靠性契约描述了方法的预期成功以及从 CER 中调用该方法时保证的一致性级别。在设计可能在 CER 中调用的方法时,可靠性契约非常重要。如果您的方法验证输入并抛出异常,您需要告知调用者该方法在从 CER 调用时可能会失败。

示例

Brian Grunkemeyer[^] 提供了一个取自他参与过的 ReliableArrayList 原型的示例,展示了如何以原子方式编辑数据结构的多个字段。

结论

虽然受约束的执行区域和 SafeHandle 为清理非托管资源提供了出色的框架,但对于可靠性问题并没有“万能药”。您需要在一个编辑进程全局状态或在未使用 SafeHandle 来保证清理的情况下分配非托管句柄的任何地方使用 CER。

运行时对从 CER 中可以调用哪些内容强制执行的限制以及它们可能带来的性能影响,无疑使得使用 CER 成为一项高级编程功能。希望您永远不需要在自己的代码中达到这种可靠性级别,而可以简单地依赖运行时为您提供。

参考文献与延伸阅读

有关更多信息,您可以查阅以下参考资料

修订历史

2006 年 10 月 30 日

  • 修订了关于受约束执行区域的部分。
  • 用 MSDN 文章的引用替换了示例代码。

2006 年 9 月 2 日

  • 原始文章。
© . All rights reserved.