Можно ли в WinAPI установить размер стека для текущего потока во время выполнения, как setrlimit
делает на Linux?
Я имею в виду увеличить зарезервированный размер стека для текущего потока, если он слишком мал для текущих требований.
Это находится в библиотеке, которая может вызываться потоками из других языков программирования, так что нельзя выбирать размер стека во время компиляции.
Если нет, есть ли какие-либо идеи по поводу решения, такого как сборочный батут, который изменяет указатель стека на динамически выделяемый блок памяти?
Часто задаваемые вопросы: поток прокси является верным решением (если только поток вызывающей стороны не имеет очень маленького стека). Однако переключение потоков кажется снижением производительности. Мне нужно значительное количество стека для рекурсии или для _alloca
. Это также для производительности, потому что распределение кучи происходит медленно, особенно если несколько потоков выделяются из кучи параллельно (они блокируются одним и тем же libc
/CRT
мьютекс, поэтому код становится последовательным).
Вы не можете заполнить стек подкачки в текущем потоке (выделить себя, удалить старый) в коде библиотеки, потому что в старом стеке — адреса возврата, могут быть указатели на переменные в стеке и т. д.
и вы не можете расширить стек (виртуальная память для него уже выделена (зарезервирована / зафиксирована) и не расширяется.
однако возможно выделить временный стек и переключиться на этот стек во время вызова. Вы должны в этом случае сохранить старое StackBase
а также StackLimit
от NT_TIB
(посмотрите эту структуру в winnt.h
), установите новые значения (вам нужно выделить память для нового стека), сделайте вызов (для стека коммутаторов вам нужен код сборки — вы не можете сделать это только на C / C ++) и вернуть оригинал StackBase
а также StackLimit
, в kernelmode существует поддержка для этого — KeExpandKernelStackAndCallout
Однако в пользовательском режиме существуют Волокна — это очень редко используется, но выглядит как идеально подходит для задачи. с Fiber мы можем создать дополнительный контекст стека / исполнения внутри текущая тема.
В общем, решение следующее (для библиотеки):
на DLL_THREAD_ATTACH
:
ConvertThreadToFiber
) (если он вернется false
проверьте такжеGetLastError
за ERROR_ALREADY_FIBER
— это тоже нормально, код)CreateFiberEx
мы делаем это только один раз. чем каждый раз, когда вызывается ваша процедура, требующая большого стекового пространства:
GetCurrentFiber
SwitchToFiber
GetCurrentFiber
)SwitchToFiber
и наконец на DLL_THREAD_DETACH
тебе нужно:
DeleteFiber
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
при первом вызове внутри потока, но не для всех потоков. также __declspec(thread)
не правильно работал (не работал вообще) в XP для длл. так что некоторые модификации могут быть
в 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()
гораздо лучше выделять его только для потоков, когда это действительно необходимо (при первом вызове)
и этот код может потенциально иметь проблемы с волокнами, если кто-то еще также попытается использовать волокна. скажи в мсдн пример код не проверяется на ERROR_ALREADY_FIBER
в случае ConvertThreadToFiber
верните 0. поэтому мы можем подождать, что это дело будет неправильно обработано основным приложением, если мы перед тем, как решим создать волокно, а также попробуем использовать волокно после нас. также ERROR_ALREADY_FIBER
не работал в хр (начать с перспективы).
Так что возможно и другое решение — самостоятельно создать стек потоков и временно переключиться на него с помощью вызовов, которые требуют большого стекового пространства. Главное нужно не только выделить место под стек и подкачку особ (или же RSP) но не забудьте исправить установить StackBase
а также StackLimit
в NT_TIB
— это необходимое и достаточное условие (в противном случае исключения и расширение страницы защиты не будут работать).
несмотря на то, что для этого альтернативного решения требуется больше кода (вручную создайте стек потоков и переключите стек), он будет работать и на 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
и для х64:
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;
}
Максимальный размер стека определяется при создании потока. Это не может быть изменено после этого времени.