From 75a62f856b64fda9517778a44b3f9eb1d741dc54 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Wed, 12 Apr 2023 09:35:09 -0700 Subject: [PATCH] Threads: Moves pthread logic to FEXLoader This is not an attempt to clean up the various issues with the pthread logic, instead just moving the pthread specific logic out of FEXCore in to FEXLoader. FEXCore needs to know how to create threads in an agnostic way. Which is why we obfuscate the details with this inteface. Initially this was implemented with the pthread handlers in FEXCore and expected eventually for those to get moved to the frontend. This is the time when it has been moved. This is the first step towards compiling with llvm-mingw. Still a long way to go. --- External/FEXCore/Source/Utils/Threads.cpp | 189 +--------------- .../FEXCore/include/FEXCore/Utils/Threads.h | 4 - Source/Tools/FEXLoader/FEXLoader.cpp | 7 +- .../FEXLoader/LinuxSyscalls/CMakeLists.txt | 1 + .../FEXLoader/LinuxSyscalls/Syscalls.cpp | 14 +- .../FEXLoader/LinuxSyscalls/Utils/Threads.cpp | 205 ++++++++++++++++++ .../FEXLoader/LinuxSyscalls/Utils/Threads.h | 34 +++ 7 files changed, 259 insertions(+), 195 deletions(-) create mode 100644 Source/Tools/FEXLoader/LinuxSyscalls/Utils/Threads.cpp create mode 100644 Source/Tools/FEXLoader/LinuxSyscalls/Utils/Threads.h diff --git a/External/FEXCore/Source/Utils/Threads.cpp b/External/FEXCore/Source/Utils/Threads.cpp index 0e9469421..c44074474 100644 --- a/External/FEXCore/Source/Utils/Threads.cpp +++ b/External/FEXCore/Source/Utils/Threads.cpp @@ -16,196 +16,19 @@ #include namespace FEXCore::Threads { - // Stack pool handling - struct StackPoolItem { - void *Ptr; - size_t Size; - }; - std::mutex DeadStackPoolMutex{}; - std::mutex LiveStackPoolMutex{}; - - static fextl::deque DeadStackPool{}; - static fextl::deque LiveStackPool{}; - - void *AllocateStackObject(size_t Size) { - std::lock_guard lk{DeadStackPoolMutex}; - if (DeadStackPool.size() == 0) { - // Nothing in the pool, just allocate - return FEXCore::Allocator::mmap(nullptr, Size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - } - - // Keep the first item in the stack pool - auto Result = DeadStackPool.front().Ptr; - DeadStackPool.pop_front(); - - // Erase the rest as a garbage collection step - for (auto &Item : DeadStackPool) { - FEXCore::Allocator::munmap(Item.Ptr, Item.Size); - } - return Result; - } - - void AddStackToDeadPool(void *Ptr, size_t Size) { - std::lock_guard lk{DeadStackPoolMutex}; - DeadStackPool.emplace_back(StackPoolItem{Ptr, Size}); - } - - void AddStackToLivePool(void *Ptr, size_t Size) { - std::lock_guard lk{LiveStackPoolMutex}; - LiveStackPool.emplace_back(StackPoolItem{Ptr, Size}); - } - - void RemoveStackFromLivePool(void *Ptr) { - std::lock_guard lk{LiveStackPoolMutex}; - for (auto it = LiveStackPool.begin(); it != LiveStackPool.end(); ++it) { - if (it->Ptr == Ptr) { - LiveStackPool.erase(it); - return; - } - } - } - - void DeallocateStackObject(void *Ptr, size_t Size) { - RemoveStackFromLivePool(Ptr); - AddStackToDeadPool(Ptr, Size); - } - - void Shutdown() { - std::lock_guard lk{DeadStackPoolMutex}; - std::lock_guard lk2{LiveStackPoolMutex}; - // Erase all the dead stack pools - for (auto &Item : DeadStackPool) { - FEXCore::Allocator::munmap(Item.Ptr, Item.Size); - } - - // Now clean up any that are considered to still be live - // We are in shutdown phase, everything in the process is dead - for (auto &Item : LiveStackPool) { - FEXCore::Allocator::munmap(Item.Ptr, Item.Size); - } - - DeadStackPool.clear(); - LiveStackPool.clear(); - } - - void *InitializeThread(void *Ptr); - - class PThread final : public Thread { - public: - PThread(FEXCore::Threads::ThreadFunc Func, void *Arg) - : UserFunc {Func} - , UserArg {Arg} { - pthread_attr_t Attr{}; - Stack = AllocateStackObject(STACK_SIZE); - // pthreads allocates its dtv region behind our back and there is nothing we can do about it. - FEXCore::Allocator::YesIKnowImNotSupposedToUseTheGlibcAllocator glibc; - AddStackToLivePool(Stack, STACK_SIZE); - pthread_attr_init(&Attr); - pthread_attr_setstack(&Attr, Stack, STACK_SIZE); - // TODO: Thread creation should be using this instead. - // Causes Steam to crash early though. - // pthread_create(&Thread, &Attr, InitializeThread, this); - pthread_create(&Thread, &Attr, Func, Arg); - - pthread_attr_destroy(&Attr); - } - - bool joinable() override { - pthread_attr_t Attr{}; - if (pthread_getattr_np(Thread, &Attr) == 0) { - int AttachState{}; - if (pthread_attr_getdetachstate(&Attr, &AttachState) == 0) { - if (AttachState == PTHREAD_CREATE_JOINABLE) { - return true; - } - } - } - return false; - } - - bool join(void **ret) override { - return pthread_join(Thread, ret) == 0; - } - - bool detach() override { - return pthread_detach(Thread) == 0; - } - - bool IsSelf() override { - auto self = pthread_self(); - return self == Thread; - } - - void *Execute() { - return UserFunc(UserArg); - } - - void FreeStack() { - DeallocateStackObject(Stack, STACK_SIZE); - } - - private: - pthread_t Thread; - FEXCore::Threads::ThreadFunc UserFunc; - void *UserArg; - void *Stack{}; - constexpr static size_t STACK_SIZE = 8 * 1024 * 1024; - }; - - void *InitializeThread(void *Ptr) { - PThread *Thread{reinterpret_cast(Ptr)}; - - // Run the user function - void *Result = Thread->Execute(); - - // Put the stack back in to the stack pool - Thread->FreeStack(); - - // TLS/DTV teardown is something FEX can't control. Disable glibc checking when we leave a pthread. - FEXCore::Allocator::YesIKnowImNotSupposedToUseTheGlibcAllocator::HardDisable(); - - return Result; - } - - fextl::unique_ptr CreateThread_PThread( + fextl::unique_ptr CreateThread_Default( ThreadFunc Func, void* Arg) { - return fextl::make_unique(Func, Arg); + ERROR_AND_DIE_FMT("Frontend didn't setup thread creation!"); } - void CleanupAfterFork_PThread() { - // We don't need to pull the mutex here - // After a fork we are the only thread running - // Just need to make sure not to delete our own stack - uintptr_t StackLocation = reinterpret_cast(alloca(0)); - - auto ClearStackPool = [&](auto &StackPool) { - for (auto it = StackPool.begin(); it != StackPool.end(); ) { - StackPoolItem &Item = *it; - uintptr_t ItemStack = reinterpret_cast(Item.Ptr); - if (ItemStack <= StackLocation && (ItemStack + Item.Size) > StackLocation) { - // This is our stack item, skip it - ++it; - } - else { - // Untracked stack. Clean it up - FEXCore::Allocator::munmap(Item.Ptr, Item.Size); - it = StackPool.erase(it); - } - } - }; - - // Clear both dead stacks and live stacks - ClearStackPool(DeadStackPool); - ClearStackPool(LiveStackPool); - - LogMan::Throw::AFmt((DeadStackPool.size() + LiveStackPool.size()) <= 1, - "After fork we should only have zero or one tracked stacks!"); + void CleanupAfterFork_Default() { + ERROR_AND_DIE_FMT("Frontend didn't setup thread creation!"); } static FEXCore::Threads::Pointers Ptrs = { - .CreateThread = CreateThread_PThread, - .CleanupAfterFork = CleanupAfterFork_PThread, + .CreateThread = CreateThread_Default, + .CleanupAfterFork = CleanupAfterFork_Default, }; fextl::unique_ptr FEXCore::Threads::Thread::Create( diff --git a/External/FEXCore/include/FEXCore/Utils/Threads.h b/External/FEXCore/include/FEXCore/Utils/Threads.h index 9adfc51b2..8cb369800 100644 --- a/External/FEXCore/include/FEXCore/Utils/Threads.h +++ b/External/FEXCore/include/FEXCore/Utils/Threads.h @@ -40,10 +40,6 @@ namespace FEXCore::Threads { static void SetInternalPointers(Pointers const &_Ptrs); }; - void *AllocateStackObject(size_t Size); - void DeallocateStackObject(void *Ptr, size_t Size); - void Shutdown(); - /** * @brief Sets the calling thread's signal mask to the one provided * diff --git a/Source/Tools/FEXLoader/FEXLoader.cpp b/Source/Tools/FEXLoader/FEXLoader.cpp index e92ae7c53..b7a1da36c 100644 --- a/Source/Tools/FEXLoader/FEXLoader.cpp +++ b/Source/Tools/FEXLoader/FEXLoader.cpp @@ -12,6 +12,7 @@ $end_info$ #include "VDSO_Emulation.h" #include "LinuxSyscalls/LinuxAllocator.h" #include "LinuxSyscalls/Syscalls.h" +#include "LinuxSyscalls/Utils/Threads.h" #include "LinuxSyscalls/x32/Syscalls.h" #include "LinuxSyscalls/x64/Syscalls.h" #include "LinuxSyscalls/SignalDelegator.h" @@ -387,6 +388,10 @@ int main(int argc, char **argv, char **const envp) { } FEXCore::Config::EraseSet(FEXCore::Config::CONFIG_APP_CONFIG_NAME, Program.ProgramName); } + + // Setup Thread handlers, so FEXCore can create threads. + FEX::LinuxEmulation::Threads::SetupThreadHandlers(); + FEXCore::Config::EraseSet(FEXCore::Config::CONFIG_IS64BIT_MODE, Loader.Is64BitMode() ? "1" : "0"); fextl::unique_ptr Allocator; @@ -546,7 +551,7 @@ int main(int argc, char **argv, char **const envp) { SyscallHandler.reset(); SignalDelegation.reset(); - FEXCore::Threads::Shutdown(); + FEX::LinuxEmulation::Threads::Shutdown(); Loader.FreeSections(); diff --git a/Source/Tools/FEXLoader/LinuxSyscalls/CMakeLists.txt b/Source/Tools/FEXLoader/LinuxSyscalls/CMakeLists.txt index a34b3600e..5667b29ee 100644 --- a/Source/Tools/FEXLoader/LinuxSyscalls/CMakeLists.txt +++ b/Source/Tools/FEXLoader/LinuxSyscalls/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(LinuxEmulation STATIC Syscalls.cpp SyscallsSMCTracking.cpp SyscallsVMATracking.cpp + Utils/Threads.cpp x32/Syscalls.cpp x32/EPoll.cpp x32/FD.cpp diff --git a/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.cpp b/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.cpp index 821fdf21c..8ddbd2277 100644 --- a/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.cpp +++ b/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.cpp @@ -13,6 +13,7 @@ $end_info$ #include "LinuxSyscalls/SignalDelegator.h" #include "LinuxSyscalls/Syscalls.h" #include "LinuxSyscalls/Syscalls/Thread.h" +#include "LinuxSyscalls/Utils/Threads.h" #include "LinuxSyscalls/x32/Syscalls.h" #include "LinuxSyscalls/x64/Syscalls.h" #include "LinuxSyscalls/x32/Types.h" @@ -29,7 +30,6 @@ $end_info$ #include #include #include -#include #include #include #include @@ -426,7 +426,7 @@ struct StackFramePlusRet { static void Clone3HandlerRet() { StackFrameData *Data = (StackFrameData*)alloca(0); uint64_t Result = FEX::HLE::HandleNewClone(Data->Thread, Data->CTX, &Data->NewFrame, &Data->GuestArgs); - FEXCore::Threads::DeallocateStackObject(Data->NewStack, Data->StackSize); + FEX::LinuxEmulation::Threads::DeallocateStackObject(Data->NewStack); // To behave like a real clone, we now just need to call exit here exit(Result); FEX_UNREACHABLE; @@ -435,7 +435,7 @@ static void Clone3HandlerRet() { static int Clone2HandlerRet(void *arg) { StackFrameData *Data = (StackFrameData*)arg; uint64_t Result = FEX::HLE::HandleNewClone(Data->Thread, Data->CTX, &Data->NewFrame, &Data->GuestArgs); - FEXCore::Threads::DeallocateStackObject(Data->NewStack, Data->StackSize); + FEX::LinuxEmulation::Threads::DeallocateStackObject(Data->NewStack); FEXCore::Allocator::free(arg); return Result; } @@ -489,8 +489,8 @@ static uint64_t Clone2Handler(FEXCore::Core::CpuStateFrame *Frame, FEX::HLE::clo Data->GuestArgs = *args; // In the case of thread, we need a new stack - Data->StackSize = 8 * 1024 * 1024; - Data->NewStack = FEXCore::Threads::AllocateStackObject(Data->StackSize); + Data->StackSize = FEX::LinuxEmulation::Threads::STACK_SIZE; + Data->NewStack = FEX::LinuxEmulation::Threads::AllocateStackObject(); // Create a copy of the parent frame memcpy(&Data->NewFrame, Frame, sizeof(FEXCore::Core::CpuStateFrame)); @@ -514,8 +514,8 @@ static uint64_t Clone2Handler(FEXCore::Core::CpuStateFrame *Frame, FEX::HLE::clo static uint64_t Clone3Handler(FEXCore::Core::CpuStateFrame *Frame, FEX::HLE::clone3_args *args) { // In the case of thread, we need a new stack - uint64_t StackSize = 8 * 1024 * 1024; - void *NewStack = FEXCore::Threads::AllocateStackObject(StackSize); + const uint64_t StackSize = FEX::LinuxEmulation::Threads::STACK_SIZE; + void *NewStack = FEX::LinuxEmulation::Threads::AllocateStackObject(); constexpr size_t Offset = sizeof(StackFramePlusRet); StackFramePlusRet *Data = (StackFramePlusRet*)(reinterpret_cast(NewStack) + StackSize - Offset); diff --git a/Source/Tools/FEXLoader/LinuxSyscalls/Utils/Threads.cpp b/Source/Tools/FEXLoader/LinuxSyscalls/Utils/Threads.cpp new file mode 100644 index 000000000..c4761b0d7 --- /dev/null +++ b/Source/Tools/FEXLoader/LinuxSyscalls/Utils/Threads.cpp @@ -0,0 +1,205 @@ +#include "LinuxSyscalls/Utils/Threads.h" + +#include +#include + +#include + +namespace FEX::LinuxEmulation::Threads { + // Stack pool handling + struct StackPoolItem { + void *Ptr; + size_t Size; + }; + std::mutex DeadStackPoolMutex{}; + std::mutex LiveStackPoolMutex{}; + + static fextl::deque DeadStackPool{}; + static fextl::deque LiveStackPool{}; + + void *AllocateStackObject() { + std::lock_guard lk{DeadStackPoolMutex}; + if (DeadStackPool.size() == 0) { + // Nothing in the pool, just allocate + return FEXCore::Allocator::mmap(nullptr, FEX::LinuxEmulation::Threads::STACK_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + } + + // Keep the first item in the stack pool + auto Result = DeadStackPool.front().Ptr; + DeadStackPool.pop_front(); + + // Erase the rest as a garbage collection step + for (auto &Item : DeadStackPool) { + FEXCore::Allocator::munmap(Item.Ptr, Item.Size); + } + return Result; + } + + void AddStackToDeadPool(void *Ptr) { + std::lock_guard lk{DeadStackPoolMutex}; + DeadStackPool.emplace_back(StackPoolItem{Ptr, FEX::LinuxEmulation::Threads::STACK_SIZE}); + } + + void AddStackToLivePool(void *Ptr) { + std::lock_guard lk{LiveStackPoolMutex}; + LiveStackPool.emplace_back(StackPoolItem{Ptr, FEX::LinuxEmulation::Threads::STACK_SIZE}); + } + + void RemoveStackFromLivePool(void *Ptr) { + std::lock_guard lk{LiveStackPoolMutex}; + for (auto it = LiveStackPool.begin(); it != LiveStackPool.end(); ++it) { + if (it->Ptr == Ptr) { + LiveStackPool.erase(it); + return; + } + } + } + + void DeallocateStackObject(void *Ptr) { + RemoveStackFromLivePool(Ptr); + AddStackToDeadPool(Ptr); + } + + namespace PThreads { + void *InitializeThread(void *Ptr); + + class PThread final : public FEXCore::Threads::Thread { + public: + PThread(FEXCore::Threads::ThreadFunc Func, void *Arg) + : UserFunc {Func} + , UserArg {Arg} { + pthread_attr_t Attr{}; + Stack = AllocateStackObject(); + // pthreads allocates its dtv region behind our back and there is nothing we can do about it. + FEXCore::Allocator::YesIKnowImNotSupposedToUseTheGlibcAllocator glibc; + AddStackToLivePool(Stack); + pthread_attr_init(&Attr); + pthread_attr_setstack(&Attr, Stack, FEX::LinuxEmulation::Threads::STACK_SIZE); + // TODO: Thread creation should be using this instead. + // Causes Steam to crash early though. + // pthread_create(&Thread, &Attr, InitializeThread, this); + pthread_create(&Thread, &Attr, Func, Arg); + + pthread_attr_destroy(&Attr); + } + + bool joinable() override { + pthread_attr_t Attr{}; + if (pthread_getattr_np(Thread, &Attr) == 0) { + int AttachState{}; + if (pthread_attr_getdetachstate(&Attr, &AttachState) == 0) { + if (AttachState == PTHREAD_CREATE_JOINABLE) { + return true; + } + } + } + return false; + } + + bool join(void **ret) override { + return pthread_join(Thread, ret) == 0; + } + + bool detach() override { + return pthread_detach(Thread) == 0; + } + + bool IsSelf() override { + auto self = pthread_self(); + return self == Thread; + } + + void *Execute() { + return UserFunc(UserArg); + } + + void FreeStack() { + DeallocateStackObject(Stack); + } + + private: + pthread_t Thread; + FEXCore::Threads::ThreadFunc UserFunc; + void *UserArg; + void *Stack{}; + }; + + void *InitializeThread(void *Ptr) { + PThread *Thread{reinterpret_cast(Ptr)}; + + // Run the user function + void *Result = Thread->Execute(); + + // Put the stack back in to the stack pool + Thread->FreeStack(); + + // TLS/DTV teardown is something FEX can't control. Disable glibc checking when we leave a pthread. + FEXCore::Allocator::YesIKnowImNotSupposedToUseTheGlibcAllocator::HardDisable(); + + return Result; + } + + fextl::unique_ptr CreateThread_PThread( + FEXCore::Threads::ThreadFunc Func, + void* Arg) { + return fextl::make_unique(Func, Arg); + } + + void CleanupAfterFork_PThread() { + // We don't need to pull the mutex here + // After a fork we are the only thread running + // Just need to make sure not to delete our own stack + uintptr_t StackLocation = reinterpret_cast(alloca(0)); + + auto ClearStackPool = [&](auto &StackPool) { + for (auto it = StackPool.begin(); it != StackPool.end(); ) { + StackPoolItem &Item = *it; + uintptr_t ItemStack = reinterpret_cast(Item.Ptr); + if (ItemStack <= StackLocation && (ItemStack + Item.Size) > StackLocation) { + // This is our stack item, skip it + ++it; + } + else { + // Untracked stack. Clean it up + FEXCore::Allocator::munmap(Item.Ptr, Item.Size); + it = StackPool.erase(it); + } + } + }; + + // Clear both dead stacks and live stacks + ClearStackPool(DeadStackPool); + ClearStackPool(LiveStackPool); + + LogMan::Throw::AFmt((DeadStackPool.size() + LiveStackPool.size()) <= 1, + "After fork we should only have zero or one tracked stacks!"); + } + }; + + void SetupThreadHandlers() { + FEXCore::Threads::Pointers Ptrs = { + .CreateThread = PThreads::CreateThread_PThread, + .CleanupAfterFork = PThreads::CleanupAfterFork_PThread, + }; + + FEXCore::Threads::Thread::SetInternalPointers(Ptrs); + } + + void Shutdown() { + std::lock_guard lk{DeadStackPoolMutex}; + std::lock_guard lk2{LiveStackPoolMutex}; + // Erase all the dead stack pools + for (auto &Item : DeadStackPool) { + FEXCore::Allocator::munmap(Item.Ptr, Item.Size); + } + + // Now clean up any that are considered to still be live + // We are in shutdown phase, everything in the process is dead + for (auto &Item : LiveStackPool) { + FEXCore::Allocator::munmap(Item.Ptr, Item.Size); + } + + DeadStackPool.clear(); + LiveStackPool.clear(); + } +} diff --git a/Source/Tools/FEXLoader/LinuxSyscalls/Utils/Threads.h b/Source/Tools/FEXLoader/LinuxSyscalls/Utils/Threads.h new file mode 100644 index 000000000..c4ceeffe2 --- /dev/null +++ b/Source/Tools/FEXLoader/LinuxSyscalls/Utils/Threads.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +namespace FEX::LinuxEmulation::Threads { + /** + * @brief Size of the stack that this interface creates. + */ + constexpr size_t STACK_SIZE = 8 * 1024 * 1024; + + /** + * @brief Allocates a stack object from the internally managed stack pool. + */ + void *AllocateStackObject(); + + /** + * @brief Deallocates a stack from the internally managed stack pool. + * + * Will not free the memory immediately, instead saving for reuse temporarily to solve race conditions on stack usage while stack tears down. + * + * @param Ptr The stack base from `AllocateStackObject` + */ + void DeallocateStackObject(void *Ptr); + + /** + * @brief Registers thread creation handlers with FEXCore. + */ + void SetupThreadHandlers(); + + /** + * @brief Cleans up any remaining stack objects in the pools. + */ + void Shutdown(); +}