【发布时间】:2014-06-27 00:15:10
【问题描述】:
受约束的执行区域是 C# / .Net 的一项功能,它允许开发人员尝试从代码的关键区域(OutOfMemory、StackOverflow 和 ThreadAbort)中提升“三大”异常。
CER 通过推迟 ThreadAborts、准备调用图中的所有方法(因此不必发生可能导致分配的 JIT)以及确保有足够的堆栈空间来适应随后的调用堆栈来实现这一点。
典型的不间断区域可能如下所示:
public static void GetNativeFlag()
{
IntPtr nativeResource = new IntPtr();
int flag;
// Remember, only the finally block is constrained; try is normal.
RuntimeHelpers.PrepareConstrainedRegions();
try
{ }
finally
{
NativeMethods.GetPackageFlags( ref nativeResource );
if ( nativeResource != IntPtr.Zero ) {
flag = Marshal.ReadInt32( nativeResource );
NativeMethods.FreeBuffer( nativeResource );
}
}
}
以上内容大部分都很好,因为 CER 内部没有违反任何规则 - 所有 .Net 分配都在 CER 之外,Marshal.ReadInt32() 具有兼容的ReliabilityContract,我们假设我的 NativeMethods 类似标记,以便 VM 在准备 CER 时可以正确考虑它们。
因此,在解决了所有这些问题后,您如何处理必须在 CER 内部进行分配的情况?分配违反了规则,因为很可能会出现 OutOfMemoryException。
我在查询强制我违反这些规则的本机 API(SSPI 的 QuerySecurityPackageInfo)时遇到了这个问题。本机 API 确实执行其自己的(本机)分配,但如果失败,我只会得到一个空结果,所以没什么大不了的。但是,在它分配的结构中,它存储了一些未知大小的 C 字符串。
当它给我一个指向它分配的结构的指针时,我必须复制整个东西,并分配空间来将这些 c 字符串存储为 .Net 字符串对象。毕竟,我应该告诉它释放分配。
但是,由于我在 CER 中执行 .Net 分配,因此我违反了规则并且可能泄漏了句柄。
处理这个问题的正确方法是什么?
不管怎样,这是我幼稚的做法:
internal static SecPkgInfo GetPackageCapabilities_Bad( string packageName )
{
SecPkgInfo info;
IntPtr rawInfoPtr;
rawInfoPtr = new IntPtr();
info = new SecPkgInfo();
RuntimeHelpers.PrepareConstrainedRegions();
try
{ }
finally
{
NativeMethods.QuerySecurityPackageInfo( packageName, ref rawInfoPtr );
if ( rawInfoPtr != IntPtr.Zero )
{
// This performs allocations as it makes room for the strings contained in the result.
Marshal.PtrToStructure( rawInfoPtr, info );
NativeMethods.FreeContextBuffer( rawInfoPtr );
}
}
return info;
}
编辑
我应该提到,在这种情况下,我的“成功”是我从不泄漏句柄;如果我执行失败的分配并释放句柄,然后向我的调用者返回一个错误,指示分配失败,那也没关系。就是不能泄露句柄。
编辑以回应 Frank Hileman
我们对执行互操作调用时所需的内存分配没有太多控制。
取决于您的意思 - 可能分配给执行调用调用的内存,还是被调用的调用创建的内存?
我们可以完美地控制分配给执行调用的内存——即由 JIT 创建的用于编译相关方法的内存,以及堆栈执行调用所需的内存。 JIT 编译内存是在准备 CER 期间分配的;如果失败,则永远不会执行整个 CER。 CER 准备还计算 CER 执行的静态调用图中需要多少堆栈空间,如果堆栈不足,则中止 CER 准备。
巧合的是,这涉及到任何 try-catch-finally 帧的堆栈空间准备,甚至是嵌套的 try-catch-finally 帧,它们恰好定义并参与了 CER。将 try-catch-finally 嵌套在 CER 中是完全合理的,因为 JIT 可以计算记录 try-catch-finally 上下文所需的堆栈内存量,如果需要太多,则同样中止 CER 准备。
调用本身可能会在 .net 堆之外进行一些内存分配;我很惊讶在 CER 中允许本地调用。
如果您指的是由调用的调用执行的本机内存分配,那么这对于 CER 来说也不是问题。本机内存分配要么成功,要么返回状态码。 OOM 不是由本机内存分配生成的。如果本机分配失败,大概是我正在调用的本机 API 通过返回状态代码或空指针来处理它。调用仍然是确定性的。唯一的副作用是它可能导致后续托管分配由于内存压力增加而失败。 然而,如果我们要么从不执行分配,要么可以确定性地处理失败的托管分配,那么它仍然不是问题。
因此,在 CER 中唯一不好的分配是托管分配,因为它可能导致“异步”OOM 异常。所以现在的问题是我如何确定性地处理 CER 内失败的托管分配..
但这完全有可能。 CER 可以有嵌套的 try-catch-finally 块。 所有 CER 中的调用,以及 CER 所需的所有堆栈空间,甚至用于在 CER 的 finally 中记录嵌套 try-finally 的上下文,都可以在整个 CER 的准备过程中进行确定性计算,在我的任何代码实际执行之前。
【问题讨论】:
-
请解释为什么需要在 CER 中执行此操作。这似乎是不可能的;你必须分配内存才能做到这一点。
-
好吧,我想这是一个答案——不要在 CER 中做所有事情。将句柄保存在 SafeHandle 中,离开 CER,进行分配,然后输入新的 CER。
-
只是一个(可能是愚蠢的)想法:您不能将指针读入包含预分配字符数组而不是字符串的结构吗?
-
@FrankHileman - 本地调用可能会损坏内存,但如果你导致这种情况发生,那是你的错,CER 提供的保证不再适用。我花了一段时间才明白,但 CER 并不神奇 - 它们只是要求运行时在 finally 的调用图中预先准备您的方法、在执行任何操作之前检查堆栈空间并推迟的简化方式关于发送 ThreadAbortExceptions。
-
@FrankHileman - 此外,发明 CER 的几个原因之一是启用可靠的 PInvoke。您必须引用计数句柄来解决终结器竞赛,如果您没有 CER,那么当句柄正在使用时您将无法可靠地引用计数并且您会泄漏它。有关该模式的典型示例,请参见stackoverflow.com/a/24390662/344638的后半部分