Как бороться с распределениями в ограниченных регионах исполнения?

Области ограниченного выполнения - это особенность 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 помечены аналогичным образом, чтобы виртуальная машина могла должным образом учитывать их при подготовке CER.

Итак, со всем этим, как вы справляетесь с ситуациями, когда распределение должно происходить внутри CER? Распределение нарушает правила, поскольку очень возможно получить исключение OutOfMemoryException.

Я столкнулся с этой проблемой при запросе нативного API (SSPIQuerySecurityPackageInfo) что заставляет меня нарушать эти правила. Нативный API выполняет собственное (нативное) распределение, но если это не удается, я просто получаю пустой результат, так что ничего страшного. Однако в структуре, которую он выделяет, он хранит несколько C-строк неизвестного размера.

Когда он возвращает мне указатель на структуру, которую он выделил, я должен скопировать все это и выделить место для хранения этих c-строк как строковых объектов .Net. После всего этого я должен сказать это, чтобы освободить распределение.

Однако, поскольку я выполняю распределение .Net в CER, я нарушаю правила и, возможно, пропускаю ручку.

Как правильно справиться с этим?

Для чего это стоит, это мой наивный подход:

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

редактировать

Я должен отметить, что «успех» для меня в этом случае заключается в том, что я никогда не пропускаю ручку; Ничего страшного, если я выполню выделение, которое завершится неудачно, и отпущу дескриптор, а затем верну ошибку вызывающей стороне, указывающую, что выделение завершилось неудачно. Просто не могу течь ручками.

Отредактируйте, чтобы ответить Фрэнку Хилману

У нас нет большого контроля над распределением памяти, требуемым при выполнении вызовов взаимодействия.

Зависит от того, что вы имеете в виду - память, которая может быть выделена для выполнения вызова, или память, созданная вызванным вызовом?

У нас есть идеальный контроль над памятью, выделенной для выполнения вызова - это память, созданная 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, даже для записи контекста вложенного try-finally внутри CER finally, может быть детерминировано вычислено во время подготовки всего CER до того, как какой-либо из моих кодов фактически выполнится.

Ответы на вопрос(1)

Ваш ответ на вопрос