【问题标题】:Set stack size programmatically on Windows在 Windows 上以编程方式设置堆栈大小
【发布时间】:2017-12-28 16:58:15
【问题描述】:

WinAPI 是否可以像setrlimit 在 Linux 上那样在运行时为当前线程设置堆栈大小? 我的意思是增加当前线程的保留堆栈大小,如果它对于当前要求来说太小了。 这是在一个库中,可以由其他编程语言的线程调用,因此不能在编译时设置堆栈大小。

如果没有,关于将堆栈指针更改为动态分配的内存块的装配蹦床之类的解决方案有什么想法吗?

常见问题解答:代理线程是一个万无一失的解决方案(除非调用者线程的堆栈非常小)。但是,线程切换似乎是性能杀手。我需要大量的堆栈用于递归或_alloca。这也是为了性能,因为堆分配很慢,特别是如果多个线程从堆中并行分配(它们被相同的libc/CRT 互斥锁阻塞,因此代码变为串行)。

【问题讨论】:

  • 创建话题的时候可以设置,有帮助吗?
  • 不,线程是在我的库之外创建的。所以我必须处理在我的库中进入 API 的任何线程。
  • 对于这种特殊形式(堆栈溢出)似乎是一个很好的问题。在您的 API 中移动堆栈的有趣想法,但如果您进行 winapi 调用,我认为您会敬酒。
  • 你需要更大的堆栈做什么?我猜想为此想出一个替代方案可能会更容易。
  • 你的解决方案是CreateFiberEx

标签: c++ winapi memory stack setrlimit


【解决方案1】:

最大堆栈大小在创建线程时确定。过了那个时间就不能修改了。

【讨论】:

    【解决方案2】:

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

    并且您不能扩展堆栈(已分配(保留/提交)的虚拟内存且不可扩展。

    但是可能会分配临时堆栈并在调用期间切换到此堆栈。在这种情况下,您必须从NT_TIB 中保存旧的StackBaseStackLimit(在winnt.h 中查看此结构),设置新值(您需要为新堆栈分配内存),调用(对于开关堆栈,您需要一些汇编代码 - 您不能仅在 c/c++ 上执行此操作并返回原始 StackBaseStackLimit。在内核模式中存在对此的支持 - KeExpandKernelStackAndCallout

    但是在用户模式下存在Fibers - 这是非常罕见的使用,但看起来与任务完美匹配。使用 Fiber 我们可以在当前线程内部创建额外的堆栈/执行上下文。

    所以一般的解决方案是下一个(对于图书馆):

    DLL_THREAD_ATTACH

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

    我们只做一次。比,每次调用你的程序时,都需要大的堆栈空间:

    1. 通过电话GetCurrentFiber记住当前光纤
    2. 光纤设置任务
    3. 通过电话SwitchToFiber切换到您的光纤
    4. 光纤内部调用过程
    5. 返回原始光纤(已从电话GetCurrentFiber保存) 再次由SwitchToFiber

    最后在DLL_THREAD_DETACH 你需要:

    1. 通过DeleteFiber 删除您的光纤
    2. 通过调用 ConvertFiberToThread 将光纤转换为线程,但仅限 如果初始ConvertThreadToFiber 返回true(如果是 ERROR_ALREADY_FIBER- 让谁先将线程转换为光纤 它回来了 - 在这种情况下,这不是你的任务)

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

    接下来是演示实现:

    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,但不适用于所有线程。在 XP 中对于 dll,__declspec(thread) 也没有正确工作(根本没有工作)。所以可以进行一些修改

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

    所以改为通过OnAttach()DLL_THREAD_ATTACH 上的每个新线程分配堆栈,这样更好地仅在真正需要时(在第一次调用时)为线程分配堆栈

    如果其他人也尝试使用光纤,则此代码可能会出现光纤问题。在 msdn example 中说代码不检查 ERROR_ALREADY_FIBER 以防 ConvertThreadToFiber 返回 0。所以我们可以等待,如果我们在决定创建光纤之前,主应用程序将不正确地处理这种情况,并且它也尝试在我们之后使用光纤. ERROR_ALREADY_FIBER 也没有在 xp 中工作(从 vista 开始)。

    所以可能和另一个解决方案 - 自己创建线程堆栈,并临时切换到它需要大堆栈空间的 doring 调用。 main 不仅需要为堆栈分配空间并交换 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;
        }
    }
    

    SwitchToStack 的汇编代码:在 x86 上

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

    【讨论】:

      猜你喜欢
      • 2011-06-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-08
      • 2015-07-08
      • 1970-01-01
      • 1970-01-01
      • 2011-01-29
      相关资源
      最近更新 更多