在Windows上通过程序设置堆栈大小

11
在WinAPI中是否可以像Linux上的setrlimit一样在运行时为当前线程设置堆栈大小?我的意思是,如果当前线程的堆栈大小不足以满足当前的要求,则增加当前线程的保留堆栈大小。这是一个库,可能会被其他编程语言的线程调用,因此无法在编译时设置堆栈大小。
如果不行,有没有想法解决方案,例如更改堆栈指针到动态分配的内存块的汇编跳板?
FAQ:代理线程是一个可靠的解决方案(除非调用线程的堆栈极小)。但是,线程切换似乎会影响性能。我需要大量的堆栈用于递归或_alloca。这也是为了提高性能,因为堆分配很慢,特别是如果多个线程并行从堆中分配(它们被同一个libc/CRT互斥锁阻塞,所以代码变成串行)。

单独的线程怎么样?您的API可以在自己的线程中执行,使用一个阻塞层等待结果使其同步。 - Garr Godfrey
1
你需要更大的堆栈是为了什么?我猜可能会更容易想出一个替代方案。 - Michael Burr
是的,代理线程是一种可靠的解决方案(除非调用线程具有极小的堆栈)。然而,线程切换似乎会影响性能。我需要大量的堆栈来进行递归或使用 _alloca:https://msdn.microsoft.com/en-us/library/wb1s57t5.aspx。这也是为了提高性能,因为堆分配很慢,特别是如果多个线程并行从堆中分配(它们被相同的 libc/CRT 互斥锁阻塞,所以代码变成串行)。 - Serge Rogatch
2
你的解决方案是CreateFiberEx - RbMm
@selbie,我已经在上面解释过了:递归和 _alloca()。我也会把这个加到问题里。 - Serge Rogatch
显示剩余7条评论
2个回答

12

在库代码中,您不能在当前线程中完全交换堆栈(分配自己,删除旧的),因为在旧堆栈中有返回地址,可能是指向堆栈中变量的指针等。

并且您无法扩展堆栈(为其分配了虚拟内存(保留/提交)并且不可扩展)。

但是,在调用期间可以分配临时堆栈并切换到此堆栈。在这种情况下,您必须从NT_TIB(请参阅winnt.h中的此结构)保存旧的StackBaseStackLimit,设置新值(您需要为新堆栈分配内存),进行调用(要切换堆栈,您需要一些汇编代码-您不能仅使用),然后返回原始的StackBaseStackLimit。在内核模式下存在对此的支持 - KeExpandKernelStackAndCallout

但是,在用户模式下存在Fibers - 这很少使用,但看起来非常适合任务。使用Fiber,我们可以在当前线程内部创建附加堆栈/执行上下文。

因此,通常解决方案如下(针对库):

DLL_THREAD_ATTACH上:

  1. 将线程转换为纤程(ConvertThreadToFiber)(如果它返回false,还要检查GetLastError是否为ERROR_ALREADY_FIBER - 这也是可以的代码)
  2. 并通过调用CreateFiberEx创建自己的纤程

我们只需要执行一次。然后,每次调用需要大量堆栈空间的过程时:

  1. 通过调用GetCurrentFiber来记住当前的纤程
  2. 为您的纤程设置任务
  3. 通过调用SwitchToFiber切换到您的纤程
  4. 在纤程内调用过程
  5. 通过SwitchToFiber调用从调用GetCurrentFiber保存的原始纤程再次返回

最后,在DLL_THREAD_DETACH上,您需要:

  1. 通过 DeleteFiber 删除您的纤程
  2. 通过调用 ConvertFiberToThread 将纤程转换为线程,但仅在初始 ConvertThreadToFiber 返回 true 的情况下执行(如果是 ERROR_ALREADY_FIBER,则让最先将线程转换为纤程的人将其转换回去 - 在这种情况下,这不是您的任务)

您需要一些(通常很小的)与您的纤程/线程相关联的数据。这当然必须是每个线程变量。因此,您需要使用 __declspec(thread) 声明此数据。或者直接使用 TLS(或存在哪些现代功能)

演示实现如下:

typedef ULONG (WINAPI * MY_EXPAND_STACK_CALLOUT) (PVOID Parameter);

class FIBER_DATA 
{
public:
    PVOID _PrevFiber, _MyFiber;
    MY_EXPAND_STACK_CALLOUT _pfn;
    PVOID _Parameter;
    ULONG _dwError;
    BOOL _bConvertToThread;

    static VOID CALLBACK _FiberProc( PVOID lpParameter)
    {
        reinterpret_cast<FIBER_DATA*>(lpParameter)->FiberProc();
    }

    VOID FiberProc()
    {
        for (;;)
        {
            _dwError = _pfn(_Parameter);
            SwitchToFiber(_PrevFiber);
        }
    }

public:

    ~FIBER_DATA()
    {
        if (_MyFiber)
        {
            DeleteFiber(_MyFiber);
        }

        if (_bConvertToThread)
        {
            ConvertFiberToThread();
        }
    }

    FIBER_DATA()
    {
        _bConvertToThread = FALSE, _MyFiber = 0;
    }

    ULONG Create(SIZE_T dwStackCommitSize, SIZE_T dwStackReserveSize);

    ULONG DoCallout(MY_EXPAND_STACK_CALLOUT pfn, PVOID Parameter)
    {
        _PrevFiber = GetCurrentFiber();
        _pfn = pfn;
        _Parameter = Parameter;
        SwitchToFiber(_MyFiber);
        return _dwError;
    }
};

__declspec(thread) FIBER_DATA* g_pData;

ULONG FIBER_DATA::Create(SIZE_T dwStackCommitSize, SIZE_T dwStackReserveSize)
{
    if (ConvertThreadToFiber(this))
    {
        _bConvertToThread = TRUE;
    }
    else
    {
        ULONG dwError = GetLastError();

        if (dwError != ERROR_ALREADY_FIBER)
        {
            return dwError;
        }
    }

    return (_MyFiber = CreateFiberEx(dwStackCommitSize, dwStackReserveSize, 0, _FiberProc, this)) ? NOERROR : GetLastError();
}

void OnDetach()
{
    if (FIBER_DATA* pData = g_pData)
    {
        delete pData;
    }
}

ULONG OnAttach()
{
    if (FIBER_DATA* pData = new FIBER_DATA)
    {
        if (ULONG dwError = pData->Create(2*PAGE_SIZE, 512 * PAGE_SIZE))
        {
            delete pData;

            return dwError;
        }

        g_pData = pData;

        return NOERROR;
    }

    return ERROR_NO_SYSTEM_RESOURCES;
}

ULONG WINAPI TestCallout(PVOID param)
{
    DbgPrint("TestCallout(%s)\n", param);

    return NOERROR;
}

ULONG DoCallout(MY_EXPAND_STACK_CALLOUT pfn, PVOID Parameter)
{
    if (FIBER_DATA* pData = g_pData)
    {
        return pData->DoCallout(pfn, Parameter);
    }

    return ERROR_GEN_FAILURE;
}

if (!OnAttach())//DLL_THREAD_ATTACH
{
    DoCallout(TestCallout, "Demo Task #1");
    DoCallout(TestCallout, "Demo Task #2");
    OnDetach();//DLL_THREAD_DETACH
}

请注意,所有在单线程上下文中执行的纤维 - 与线程相关联的多个纤维不能并发执行 - 只能顺序执行,并且您自己控制切换时间。因此不需要任何额外的同步。而SwitchToFiber - 这是完整的用户模式进程。它执行非常快,永远不会失败(因为从不分配任何资源)。

更新


尽管使用__declspec(thread) FIBER_DATA* g_pData;更简单(代码更少),对于实现来说直接使用TlsGetValue / TlsSetValue并在线程内的第一次调用时分配FIBER_DATA更好,但不适用于所有线程。另外__declspec(thread)在XP的dll中不能正确工作(根本无法工作)。因此需要进行一些修改。

DLL_PROCESS_ATTACH中分配TLS插槽gTlsIndex = TlsAlloc();

并在DLL_PROCESS_DETACH中释放它。

if (gTlsIndex != TLS_OUT_OF_INDEXES) TlsFree(gTlsIndex);

在每个DLL_THREAD_DETACH通知调用时

void OnThreadDetach()
{
    if (FIBER_DATA* pData = (FIBER_DATA*)TlsGetValue(gTlsIndex))
    {
        delete pData;
    }
}

下一步需要修改DoCallout函数的方式

ULONG DoCallout(MY_EXPAND_STACK_CALLOUT pfn, PVOID Parameter)
{
    FIBER_DATA* pData = (FIBER_DATA*)TlsGetValue(gTlsIndex);

    if (!pData)
    {
        // this code executed only once on first call

        if (!(pData = new FIBER_DATA))
        {
            return ERROR_NO_SYSTEM_RESOURCES;
        }

        if (ULONG dwError = pData->Create(512*PAGE_SIZE, 4*PAGE_SIZE))// or what stack size you need
        {
            delete pData;
            return dwError;
        }

        TlsSetValue(gTlsIndex, pData);
    }

    return pData->DoCallout(pfn, Parameter);
}

因此,最好只在需要时为线程分配堆栈(即第一次调用时),而不是在每个新线程的DLL_THREAD_ATTACH上通过OnAttach()分配堆栈。

如果其他人也尝试使用纤程,这段代码可能会出现问题。例如,MSDN中的example代码没有检查ERROR_ALREADY_FIBER,以防ConvertThreadToFiber返回0。因此,如果我们在它之前决定创建纤程并且它在我们之后也尝试使用纤程,则可以等待主应用程序不正确处理此情况。此外,在xp中,ERROR_ALREADY_FIBER无效(从vista开始)。

因此,可能还有另一种解决方案-自己创建线程堆栈,并在需要大量堆栈空间的调用期间暂时切换到该堆栈。主要需分配堆栈空间并交换esp(或rsp),但不要忘记在NT_TIB中正确建立StackBaseStackLimit - 这是必要且充分的条件(否则异常和警戒页面扩展将无法工作)。

尽管这种替代解决方案需要更多的代码(手动创建线程堆栈和堆栈切换),但它也适用于xp,并且不会影响其他人在线程中尝试使用纤程的情况。

typedef ULONG (WINAPI * MY_EXPAND_STACK_CALLOUT) (PVOID Parameter);

extern "C" PVOID __fastcall SwitchToStack(PVOID param, PVOID stack);

struct FIBER_DATA
{
    PVOID _Stack, _StackLimit, _StackPtr, _StackBase;
    MY_EXPAND_STACK_CALLOUT _pfn;
    PVOID _Parameter;
    ULONG _dwError;

    static void __fastcall FiberProc(FIBER_DATA* pData, PVOID stack)
    {
        for (;;)
        {
            pData->_dwError = pData->_pfn(pData->_Parameter);

            // StackLimit can changed during _pfn call
            pData->_StackLimit = ((PNT_TIB)NtCurrentTeb())->StackLimit;

            stack = SwitchToStack(0, stack);
        }
    }

    ULONG Create(SIZE_T Reserve, SIZE_T Commit);

    ULONG DoCallout(MY_EXPAND_STACK_CALLOUT pfn, PVOID Parameter)
    {
        _pfn = pfn;
        _Parameter = Parameter;

        PNT_TIB tib = (PNT_TIB)NtCurrentTeb();

        PVOID StackBase = tib->StackBase, StackLimit = tib->StackLimit;

        tib->StackBase = _StackBase, tib->StackLimit = _StackLimit;

        _StackPtr = SwitchToStack(this, _StackPtr);

        tib->StackBase = StackBase, tib->StackLimit = StackLimit;

        return _dwError;
    }

    ~FIBER_DATA()
    {
        if (_Stack)
        {
            VirtualFree(_Stack, 0, MEM_RELEASE);
        }
    }

    FIBER_DATA()
    {
        _Stack = 0;
    }
};

ULONG FIBER_DATA::Create(SIZE_T Reserve, SIZE_T Commit)
{
    Reserve = (Reserve + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);
    Commit = (Commit + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);

    if (Reserve < Commit || !Reserve)
    {
        return ERROR_INVALID_PARAMETER;
    }

    if (PBYTE newStack = (PBYTE)VirtualAlloc(0, Reserve, MEM_RESERVE, PAGE_NOACCESS))
    {
        union {
            PBYTE newStackBase;
            void** ppvStack;
        };

        newStackBase = newStack + Reserve;

        PBYTE newStackLimit = newStackBase - Commit;

        if (newStackLimit = (PBYTE)VirtualAlloc(newStackLimit, Commit, MEM_COMMIT, PAGE_READWRITE))
        {
            if (Reserve == Commit || VirtualAlloc(newStackLimit - PAGE_SIZE, PAGE_SIZE, MEM_COMMIT, PAGE_READWRITE|PAGE_GUARD))
            {
                _StackBase = newStackBase, _StackLimit = newStackLimit, _Stack = newStack;

#if defined(_M_IX86) 
                *--ppvStack = FiberProc;
                ppvStack -= 4;// ebp,esi,edi,ebx
#elif defined(_M_AMD64)
                ppvStack -= 5;// x64 space
                *--ppvStack = FiberProc;
                ppvStack -= 8;// r15,r14,r13,r12,rbp,rsi,rdi,rbx
#else
#error "not supported"
#endif

                _StackPtr = ppvStack;

                return NOERROR;
            }
        }

        VirtualFree(newStack, 0, MEM_RELEASE);
    }

    return GetLastError();
}

ULONG gTlsIndex;

ULONG DoCallout(MY_EXPAND_STACK_CALLOUT pfn, PVOID Parameter)
{
    FIBER_DATA* pData = (FIBER_DATA*)TlsGetValue(gTlsIndex);

    if (!pData)
    {
        // this code executed only once on first call

        if (!(pData = new FIBER_DATA))
        {
            return ERROR_NO_SYSTEM_RESOURCES;
        }

        if (ULONG dwError = pData->Create(512*PAGE_SIZE, 4*PAGE_SIZE))
        {
            delete pData;
            return dwError;
        }

        TlsSetValue(gTlsIndex, pData);
    }

    return pData->DoCallout(pfn, Parameter);
}

void OnThreadDetach()
{
    if (FIBER_DATA* pData = (FIBER_DATA*)TlsGetValue(gTlsIndex))
    {
        delete pData;
    }
}

以及 x86 平台上用于 SwitchToStack 的汇编代码

@SwitchToStack@8 proc
    push    ebx
    push    edi
    push    esi
    push    ebp
    xchg    esp,edx
    mov     eax,edx
    pop     ebp
    pop     esi
    pop     edi
    pop     ebx
    ret
@SwitchToStack@8 endp

而对于 x64:

SwitchToStack proc
    push    rbx
    push    rdi
    push    rsi
    push    rbp
    push    r12
    push    r13
    push    r14
    push    r15
    xchg    rsp,rdx
    mov     rax,rdx
    pop     r15
    pop     r14
    pop     r13
    pop     r12
    pop     rbp
    pop     rsi
    pop     rdi
    pop     rbx
    ret
SwitchToStack endp

使用/测试可以是以下内容:

gTlsIndex = TlsAlloc();//DLL_PROCESS_ATTACH

if (gTlsIndex != TLS_OUT_OF_INDEXES)
{
    TestStackMemory();

    DoCallout(TestCallout, "test #1");

    //play with stack, excepions, guard pages
    PSTR str = (PSTR)alloca(256);
    DoCallout(zTestCallout, str);
    DbgPrint("str=%s\n", str);

    DoCallout(TestCallout, "test #2");

    OnThreadDetach();//DLL_THREAD_DETACH

    TlsFree(gTlsIndex);//DLL_PROCESS_DETACH
}

void TestMemory(PVOID AllocationBase)
{
    MEMORY_BASIC_INFORMATION mbi;
    PVOID BaseAddress = AllocationBase;
    while (VirtualQuery(BaseAddress, &mbi, sizeof(mbi)) >= sizeof(mbi) && mbi.AllocationBase == AllocationBase)
    {
        BaseAddress = (PBYTE)mbi.BaseAddress + mbi.RegionSize;
        DbgPrint("[%p, %p) %p %08x %08x\n", mbi.BaseAddress, BaseAddress, (PVOID)(mbi.RegionSize >> PAGE_SHIFT), mbi.State, mbi.Protect);
    }
}

void TestStackMemory()
{
    MEMORY_BASIC_INFORMATION mbi;
    if (VirtualQuery(_AddressOfReturnAddress(), &mbi, sizeof(mbi)) >= sizeof(mbi))
    {
        TestMemory(mbi.AllocationBase);
    }
}

ULONG WINAPI zTestCallout(PVOID Parameter)
{
    TestStackMemory();

    alloca(5*PAGE_SIZE);

    TestStackMemory();

    __try
    {
        *(int*)0=0;
    } 
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        DbgPrint("exception %x handled\n", GetExceptionCode());
    }

    strcpy((PSTR)Parameter, "zTestCallout demo");

    return NOERROR;
}

ULONG WINAPI TestCallout(PVOID param)
{
    TestStackMemory();

    DbgPrint("TestCallout(%s)\n", param);

    return NOERROR;
}

1
线程创建时确定了最大堆栈大小,之后无法修改。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接