【问题标题】:How to deal with allocations in constrained execution regions?如何处理受限执行区域中的分配?
【发布时间】: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的后半部分

标签: c# .net pinvoke


【解决方案1】:

只要 CER 准备好处理失败的分配,就可以在 CER 内部执行托管分配。

首先,这是损坏的代码:

SecPkgInfo info;
SecurityStatus status = SecurityStatus.InternalError;
SecurityStatus freeStatus;

IntPtr rawInfoPtr;

rawInfoPtr = new IntPtr();
info = new SecPkgInfo();

RuntimeHelpers.PrepareConstrainedRegions();
try
{ }
finally
{
    status = NativeMethods.QuerySecurityPackageInfo( packageName, ref rawInfoPtr );

    if ( rawInfoPtr != IntPtr.Zero  )
    {
        if ( status == SecurityStatus.OK )
        {
            // *** BWOOOP **** BWOOOP ***
            // This performs allocations as it makes room for the strings contained 
            // in the SecPkgInfo class. That means that we're performing managed 
            // allocation inside of a CER. This CER is broken and may cause a leak because
            // it never calls FreeContextBuffer if an OOM is caused by the Marshal.
            Marshal.PtrToStructure( rawInfoPtr, info );
        }

        freeStatus = NativeMethods.FreeContextBuffer( rawInfoPtr );
    }
}

由于 try-catch-finally 可以嵌套,并且嵌套的 try-catch-finally 所需的任何额外堆栈空间都在 CER 准备期间预先计算,我们可以在 CER 的 main finally 中使用 try-finally 以确保我们的FreeContextBuffer 永不泄露:

SecPkgInfo info;
SecurityStatus status = SecurityStatus.InternalError;
SecurityStatus freeStatus;

IntPtr rawInfoPtr;

rawInfoPtr = new IntPtr();
info = new SecPkgInfo();

RuntimeHelpers.PrepareConstrainedRegions();
try
{ }
finally
{
    status = NativeMethods.QuerySecurityPackageInfo( packageName, ref rawInfoPtr );

    if ( rawInfoPtr != IntPtr.Zero  )
    {
        try
        {
            if ( status == SecurityStatus.OK )
            {
                // This may fail but the finally will make sure we always free the native pointer.
                Marshal.PtrToStructure( rawInfoPtr, info );
            }
        }
        finally
        {
            freeStatus = NativeMethods.FreeContextBuffer( rawInfoPtr );
        }
    }
}

我还编写了一个演示程序,可在http://www.antiduh.com/tests/LeakTest.zip 获得。它有一个小的自定义本机 DLL 来跟踪分配,以及一个调用该 DLL 的托管应用程序。它显示了 CER 如何使用嵌套的 try-finally 仍然可以确定性地释放非托管资源,即使在 CER 的一部分导致 OOM 异常时也是如此。

【讨论】:

  • 感谢您向其他开发人员提供所有这些信息。这是一个晦涩的话题。
  • 为什么不把你的分配放在第一个/外部的 try 块中,然后在 finally 中单独调用你的 FreeContextBuffer 呢?
  • @DanField - 因为 try 块是不受保护的块。我可以调用 QuerySecurityPackageInfo() 并立即抛出异常(StackOverflow、ThreadAbort)并泄漏句柄。唯一的另一种选择是在 SafeHandle 中捕获句柄,以便它可以比函数调用更长寿,但这对于只需要生存 3 步的东西来说是很多工作。我在这里解释更多:antiduh.com/blog/?q=node/4
猜你喜欢
  • 2010-11-09
  • 1970-01-01
  • 2018-02-16
  • 1970-01-01
  • 1970-01-01
  • 2012-01-28
  • 1970-01-01
  • 2011-05-29
  • 2013-03-19
相关资源
最近更新 更多