Files
FEX-Emu--FEX/Source/Tools/LinuxEmulation/LinuxSyscalls/ThreadManager.cpp
T
Ryan Houdek b83dd97762 FEXCore/Context: Removes InitialRIP/RSP from CreateThread
We actually never use this anymore, we instead always pass zero for
both, and then rely on the thread inheritance model or setting the
values manually. Now that we expose visibility of the
InternalThreadState to the frontend they just access it directly.

Just a smidge of cleanup, NFC.
2026-08-24 18:32:35 -07:00

516 lines
17 KiB
C++

// SPDX-License-Identifier: MIT
#include "LinuxSyscalls/ThreadManager.h"
#include "LinuxSyscalls/Syscalls.h"
#include "LinuxSyscalls/SignalDelegator.h"
#include "LinuxSyscalls/Seccomp/SeccompEmulator.h"
#include <FEXHeaderUtils/Syscalls.h>
#include <FEXCore/Utils/Allocator.h>
#include <FEXCore/Utils/Profiler.h>
#include <FEXCore/fextl/fmt.h>
#include <sys/mman.h>
#include <sys/personality.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <git_version.h>
namespace FEX::HLE {
ThreadManager::StatAlloc::StatAlloc() {
Initialize();
SaveHeader(Is64BitMode() ? FEXCore::SHMStats::AppType::LINUX_64 : FEXCore::SHMStats::AppType::LINUX_32);
}
void ThreadManager::StatAlloc::Initialize() {
if (!ProfileStats()) {
return;
}
int fd = shm_open(fextl::fmt::format("fex-{}-stats", ::getpid()).c_str(), O_CREAT | O_TRUNC | O_RDWR, USER_PERMS);
if (fd == -1) {
return;
}
CurrentSize = sysconf(_SC_PAGESIZE);
CurrentSize = CurrentSize > 0 ? CurrentSize : FEXCore::Utils::FEX_PAGE_SIZE;
if (ftruncate(fd, CurrentSize) == -1) {
LogMan::Msg::EFmt("[StatAlloc] ftruncate failed");
goto err;
}
// Reserve a region of MAX_STATS_SIZE so we can grow the allocation buffer.
// Number of thread slots when ThreadStatsHeader == 64bytes and ThreadStats == 40bytes:
// 1 page: 99 slots
// 1 MB: 26211 slots
// 128 MB: 3355440 slots
Base = FEXCore::Allocator::mmap(nullptr, MAX_STATS_SIZE, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
if (Base == MAP_FAILED) {
LogMan::Msg::EFmt("[StatAlloc] mmap base failed");
Base = nullptr;
goto err;
}
FEXCore::Allocator::VirtualName("FEXMem_Misc", reinterpret_cast<void*>(Base), MAX_STATS_SIZE);
// Allocate a small working shared space for now, grow as necessary.
{
auto SharedBase = FEXCore::Allocator::mmap(Base, CurrentSize, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, 0);
if (SharedBase == MAP_FAILED) {
LogMan::Msg::EFmt("[StatAlloc] mmap shm failed");
FEXCore::Allocator::munmap(Base, MAX_STATS_SIZE);
Base = nullptr;
goto err;
}
}
err:
close(fd);
}
uint32_t ThreadManager::StatAlloc::FrontendAllocateSlots(uint32_t NewSize) {
if (CurrentSize == MAX_STATS_SIZE) {
// Allocator has reached maximum slots. We can't allocate anymore.
// New threads won't get stats.
return CurrentSize;
}
NewSize = std::min(MAX_STATS_SIZE, NewSize);
// When allocating more slots, open the fd without O_TRUNC | O_CREAT.
int fd = shm_open(fextl::fmt::format("fex-{}-stats", ::getpid()).c_str(), O_RDWR, USER_PERMS);
if (fd == -1) {
return CurrentSize;
}
if (ftruncate(fd, NewSize) == -1) {
LogMan::Msg::EFmt("[StatAlloc] ftruncate more failed");
close(fd);
return CurrentSize;
}
auto SharedBase = FEXCore::Allocator::mmap(Base, NewSize, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, 0);
if (SharedBase == MAP_FAILED) {
LogMan::Msg::EFmt("[StatAlloc] allocate more mmap shm failed");
close(fd);
return CurrentSize;
}
close(fd);
return NewSize;
}
FEXCore::SHMStats::ThreadStats* ThreadManager::StatAlloc::AllocateSlot(uint32_t TID) {
std::scoped_lock lk(StatMutex);
return StatAllocBase::AllocateSlot(TID);
}
void ThreadManager::StatAlloc::DeallocateSlot(FEXCore::SHMStats::ThreadStats* AllocatedSlot) {
if (!AllocatedSlot) {
return;
}
std::scoped_lock lk(StatMutex);
StatAllocBase::DeallocateSlot(AllocatedSlot);
}
void ThreadManager::StatAlloc::CleanupForExit() {
shm_unlink(fextl::fmt::format("fex-{}-stats", ::getpid()).c_str());
}
void ThreadManager::StatAlloc::LockBeforeFork() {
if (!ProfileStats()) {
return;
}
StatMutex.lock();
}
void ThreadManager::StatAlloc::UnlockAfterFork(FEXCore::Core::InternalThreadState* Thread, bool Child) {
if (!ProfileStats()) {
return;
}
if (!Child) {
StatMutex.unlock();
return;
}
StatMutex.StealAndDropActiveLocks();
// shm_memory ownership is retained by the parent process, so the child must replace it with its own one.
// Otherwise this process will keep reporting in the original parent thread's stats region.
FEXCore::Allocator::munmap(Base, MAX_STATS_SIZE);
Base = nullptr;
CurrentSize = 0;
Head = nullptr;
Stats = nullptr;
StatTail = nullptr;
RemainingSlots = 0;
Thread->ThreadStats = nullptr;
Initialize();
SaveHeader(Is64BitMode() ? FEXCore::SHMStats::AppType::LINUX_64 : FEXCore::SHMStats::AppType::LINUX_32);
// Update this thread's ThreadStats object
auto ThreadObject = FEX::HLE::ThreadManager::GetStateObjectFromFEXCoreThread(Thread);
ThreadObject->Thread->ThreadStats = AllocateSlot(ThreadObject->ThreadInfo.TID);
}
uint64_t ThreadManager::SetSignalMask(uint64_t Mask) {
::syscall(SYSCALL_DEF(rt_sigprocmask), SIG_SETMASK, &Mask, &Mask, 8);
return Mask;
}
void ThreadManager::SetThreadName(const char* name) {
pthread_setname_np(pthread_self(), name);
}
constexpr size_t CALLRET_STACK_ALLOC_SIZE = FEXCore::Core::InternalThreadState::CALLRET_STACK_SIZE + 2 * FEXCore::Utils::FEX_PAGE_SIZE;
FEX::HLE::ThreadStateObject*
ThreadManager::CreateThread(const FEXCore::Core::CPUState* NewThreadState, uint64_t ParentTID, FEX::HLE::ThreadStateObject* InheritThread) {
auto ThreadStateObject = new FEX::HLE::ThreadStateObject;
ThreadStateObject->ThreadInfo.parent_tid = ParentTID;
ThreadStateObject->ThreadInfo.PID = ::getpid();
ThreadStateObject->ThreadInfo.TID = FHU::Syscalls::gettid();
ThreadStateObject->Thread = CTX->CreateThread(NewThreadState);
auto Frame = ThreadStateObject->Thread->CurrentFrame;
// Allocate the call-ret stack with guard pages on both sides
auto AllocBase =
reinterpret_cast<uint64_t>(FEXCore::Allocator::mmap(nullptr, CALLRET_STACK_ALLOC_SIZE, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
FEXCore::Allocator::VirtualName("FEXMem_CallRetStacks", reinterpret_cast<void*>(AllocBase), CALLRET_STACK_ALLOC_SIZE);
// Disable HUGEPAGE on callret stacks.
FEXCore::Allocator::VirtualTHPControl(reinterpret_cast<void*>(AllocBase), CALLRET_STACK_ALLOC_SIZE, FEXCore::Allocator::THPControl::Disable);
// Set the base used for invalidation to the start past the guard pages
ThreadStateObject->Thread->CallRetStackBase = reinterpret_cast<void*>(AllocBase + FEXCore::Utils::FEX_PAGE_SIZE);
::mprotect(ThreadStateObject->Thread->CallRetStackBase, FEXCore::Core::InternalThreadState::CALLRET_STACK_SIZE, PROT_READ | PROT_WRITE);
Frame->State.callret_sp = ThreadStateObject->GetCallRetStackInfo().DefaultLocation;
ThreadStateObject->Thread->FrontendPtr = ThreadStateObject;
if (ProfileStats()) {
ThreadStateObject->Thread->ThreadStats = Stat.AllocateSlot(ThreadStateObject->ThreadInfo.TID);
}
// GDT and LDT are tracked per thread.
Frame->State.segment_arrays[FEXCore::Core::CPUState::SEGMENT_ARRAY_INDEX_GDT] = &ThreadStateObject->gdt[0];
// Mirror LDT to the GDT by default. Not technically correctly, but fixes crashes in unittests.
Frame->State.segment_arrays[FEXCore::Core::CPUState::SEGMENT_ARRAY_INDEX_LDT] = &ThreadStateObject->gdt[0];
if (InheritThread) {
// If we are inheriting thread data then we inherit both the gdt and ldt arrays.
// They are then forked from the parent thread.
static_assert(sizeof(ThreadStateObject->gdt) == (8 * 32));
memcpy(ThreadStateObject->gdt, InheritThread->gdt, sizeof(ThreadStateObject->gdt));
if (InheritThread->ldt_entry_count) {
const auto new_ldt_size = InheritThread->ldt_entry_count * FEX::HLE::SyscallHandler::LDT_ENTRY_SIZE;
ThreadStateObject->ldt_entries = reinterpret_cast<FEXCore::Core::CPUState::gdt_segment*>(
FEXCore::Allocator::mmap(nullptr, new_ldt_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
FEXCore::Allocator::VirtualName("FEXMem_Misc", reinterpret_cast<void*>(ThreadStateObject->ldt_entries), new_ldt_size);
ThreadStateObject->ldt_entry_count = InheritThread->ldt_entry_count;
memcpy(ThreadStateObject->ldt_entries, InheritThread->ldt_entries, new_ldt_size);
}
} else {
// Without any thread data to inherit, setup the default gdt.
// Default code segment indexes match the numbers that the Linux kernel uses.
Frame->State.cs_idx = FEXCore::Core::CPUState::DEFAULT_USER_CS << 3;
auto GDT = FEXCore::Core::CPUState::GetSegmentFromIndex(Frame->State, Frame->State.cs_idx);
FEXCore::Core::CPUState::SetGDTBase(GDT, 0);
FEXCore::Core::CPUState::SetGDTLimit(GDT, 0xF'FFFFU);
Frame->State.cs_cached =
FEXCore::Core::CPUState::CalculateGDTBase(*FEXCore::Core::CPUState::GetSegmentFromIndex(Frame->State, Frame->State.cs_idx));
if (Is64BitMode()) {
GDT->L = 1; // L = Long Mode = 64-bit
GDT->D = 0; // D = Default Operand SIze = Reserved
} else {
GDT->L = 0; // L = Long Mode = 32-bit
GDT->D = 1; // D = Default Operand Size = 32-bit
}
}
if (InheritThread) {
FEX::HLE::_SyscallHandler->SeccompEmulator.InheritSeccompFilters(InheritThread, ThreadStateObject);
ThreadStateObject->persona = InheritThread->persona;
} else {
ThreadStateObject->persona = ::personality(0xffffffff);
}
++IdleWaitRefCount;
return ThreadStateObject;
}
void ThreadManager::DestroyThread(FEX::HLE::ThreadStateObject* Thread, bool NeedsTLSUninstall) {
{
std::lock_guard lk(ThreadCreationMutex);
auto It = std::find(Threads.begin(), Threads.end(), Thread);
LOGMAN_THROW_A_FMT(It != Threads.end(), "Thread wasn't in Threads");
Threads.erase(It);
if (Threads.empty()) {
Thread->Thread->CTX->FlushAndCloseCodeMap();
}
}
Stat.DeallocateSlot(Thread->Thread->ThreadStats);
HandleThreadDeletion(Thread, NeedsTLSUninstall);
}
void ThreadManager::StopThread(FEX::HLE::ThreadStateObject* Thread) {
SignalDelegation->SignalThread(Thread->Thread, SignalEvent::Stop);
}
void ThreadManager::HandleThreadDeletion(FEX::HLE::ThreadStateObject* Thread, bool NeedsTLSUninstall) {
if (Thread->ExecutionThread) {
if (Thread->ExecutionThread->joinable()) {
Thread->ExecutionThread->join(nullptr);
}
if (Thread->ExecutionThread->IsSelf()) {
Thread->ExecutionThread->detach();
}
}
if (NeedsTLSUninstall) {
#if defined(ASSERTIONS_ENABLED) && ASSERTIONS_ENABLED
// Sanity check. This can only be called from the owning thread.
{
const auto pid = ::getpid();
const auto tid = FHU::Syscalls::gettid();
LOGMAN_THROW_A_FMT(Thread->ThreadInfo.PID == pid && Thread->ThreadInfo.TID == tid, "Can't delete TLS data from a different thread!");
}
#endif
FEXCore::Allocator::UninstallTLSData(Thread->Thread);
}
// Free the call-ret stack
FEXCore::Allocator::munmap(reinterpret_cast<void*>(Thread->GetCallRetStackInfo().AllocationBase), CALLRET_STACK_ALLOC_SIZE);
// If the LDT segment exists then deallocate it.
if (Thread->ldt_entry_count) {
FEXCore::Allocator::munmap(Thread->ldt_entries, Thread->ldt_entry_count * FEX::HLE::SyscallHandler::LDT_ENTRY_SIZE);
}
CTX->DestroyThread(Thread->Thread);
FEX::HLE::_SyscallHandler->SeccompEmulator.FreeSeccompFilters(Thread);
delete Thread;
--IdleWaitRefCount;
IdleWaitCV.notify_all();
}
void ThreadManager::NotifyPause() {
// Tell all the threads that they should pause
std::lock_guard lk(ThreadCreationMutex);
for (auto& Thread : Threads) {
SignalDelegation->SignalThread(Thread->Thread, SignalEvent::Pause);
}
}
void ThreadManager::Pause() {
#if defined(ASSERTIONS_ENABLED) && ASSERTIONS_ENABLED
// Sanity check. This can't be called from an emulation thread.
{
const auto pid = ::getpid();
const auto tid = FHU::Syscalls::gettid();
std::lock_guard lk(ThreadCreationMutex);
for (auto& Thread : Threads) {
LOGMAN_THROW_A_FMT(!(Thread->ThreadInfo.PID == pid && Thread->ThreadInfo.TID == tid), "Can't put threads to sleep from inside "
"emulation thread!");
}
}
#endif
NotifyPause();
WaitForIdle();
}
void ThreadManager::Run() {
// Spin up all the threads
std::lock_guard lk(ThreadCreationMutex);
for (auto& Thread : Threads) {
Thread->SignalReason.store(SignalEvent::Return);
}
}
void ThreadManager::WaitForIdleWithTimeout() {
std::unique_lock<std::mutex> lk(IdleWaitMutex);
bool WaitResult = IdleWaitCV.wait_for(lk, std::chrono::milliseconds(1500), [this] { return IdleWaitRefCount.load() == 0; });
if (!WaitResult) {
// The wait failed, this will occur if we stepped in to a syscall
// That's okay, we just need to pause the threads manually
NotifyPause();
}
// We have sent every thread a pause signal
// Now wait again because they /will/ be going to sleep
WaitForIdle();
}
void ThreadManager::WaitForThreadsToRun() {
size_t NumThreads {};
{
std::lock_guard lk(ThreadCreationMutex);
NumThreads = Threads.size();
}
// Spin while waiting for the threads to start up
std::unique_lock<std::mutex> lk(IdleWaitMutex);
IdleWaitCV.wait(lk, [this, NumThreads] { return IdleWaitRefCount.load() >= NumThreads; });
Running = true;
}
void ThreadManager::Step() {
LogMan::Msg::AFmt("ThreadManager::Step currently not implemented");
{
std::lock_guard lk(ThreadCreationMutex);
// Walk the threads and tell them to clear their caches
// Useful when our block size is set to a large number and we need to step a single instruction
for (auto& Thread : Threads) {
CTX->ClearCodeCache(Thread->Thread, false);
}
}
// TODO: Set to single step mode.
Run();
WaitForThreadsToRun();
WaitForIdle();
// TODO: Set back to full running mode.
}
void ThreadManager::Stop(bool IgnoreCurrentThread) {
pid_t tid = FHU::Syscalls::gettid();
FEX::HLE::ThreadStateObject* CurrentThread {};
// Tell all the threads that they should stop
{
std::lock_guard lk(ThreadCreationMutex);
for (auto& Thread : Threads) {
if (IgnoreCurrentThread && Thread->ThreadInfo.TID == tid) {
// If we are callign stop from the current thread then we can ignore sending signals to this thread
// This means that this thread is already gone
} else if (Thread->ThreadInfo.TID == tid) {
// We need to save the current thread for last to ensure all threads receive their stop signals
CurrentThread = Thread;
continue;
}
StopThread(Thread);
}
}
// Stop the current thread now if we aren't ignoring it
if (CurrentThread) {
StopThread(CurrentThread);
}
}
void ThreadManager::SleepThread(FEXCore::Context::Context* CTX, FEXCore::Core::CpuStateFrame* Frame) {
auto ThreadObject = FEX::HLE::ThreadManager::GetStateObjectFromCPUState(Frame);
#if defined(ASSERTIONS_ENABLED) && ASSERTIONS_ENABLED
// Sanity check. This can only be called from the owning thread.
{
const auto pid = ::getpid();
const auto tid = FHU::Syscalls::gettid();
LOGMAN_THROW_A_FMT(ThreadObject->ThreadInfo.PID == pid && ThreadObject->ThreadInfo.TID == tid, "Can't delete TLS data from a different "
"thread!");
}
#endif
--IdleWaitRefCount;
IdleWaitCV.notify_all();
ThreadObject->ThreadSleeping = true;
// Go to sleep
ThreadObject->ThreadPaused.Wait();
++IdleWaitRefCount;
ThreadObject->ThreadSleeping = false;
IdleWaitCV.notify_all();
}
void ThreadManager::UnpauseThread(FEX::HLE::ThreadStateObject* Thread) {
Thread->ThreadPaused.NotifyOne();
}
void ThreadManager::LockBeforeFork() {
Stat.LockBeforeFork();
}
void ThreadManager::UnlockAfterFork(FEXCore::Core::InternalThreadState* LiveThread, bool Child) {
Stat.UnlockAfterFork(LiveThread, Child);
if (!Child) {
return;
}
// This function is called after fork
// We need to cleanup some of the thread data that is dead
for (auto& DeadThread : Threads) {
// The fork parent retains ownership of ThreadStats
DeadThread->Thread->ThreadStats = nullptr;
if (DeadThread->Thread == LiveThread) {
continue;
}
// Despite what google searches may susgest, glibc actually has special code to handle forks
// with multiple active threads.
// It cleans up the stacks of dead threads and marks them as terminated.
// It also cleans up a bunch of internal mutexes.
// FIXME: TLS is probally still alive. Investigate
// Deconstructing the Interneal thread state should clean up most of the state.
// But if anything on the now deleted stack is holding a refrence to the heap, it will be leaked
CTX->DestroyThread(DeadThread->Thread);
delete DeadThread;
// FIXME: Make sure sure nothing gets leaked via the heap. Ideas:
// * Make sure nothing is allocated on the heap without ref in InternalThreadState
// * Surround any code that heap allocates with a per-thread mutex.
// Before forking, the the forking thread can lock all thread mutexes.
}
// Remove all threads but the live thread from Threads
Threads.clear();
auto ThreadObject = FEX::HLE::ThreadManager::GetStateObjectFromCPUState(LiveThread->CurrentFrame);
Threads.push_back(ThreadObject);
// Clean up dead stacks
FEXCore::Threads::Thread::CleanupAfterFork();
// We now only have one thread.
IdleWaitRefCount = 1;
ThreadCreationMutex.StealAndDropActiveLocks();
}
void ThreadManager::WaitForIdle() {
std::unique_lock<std::mutex> lk(IdleWaitMutex);
IdleWaitCV.wait(lk, [this] { return IdleWaitRefCount.load() == 0; });
Running = false;
}
ThreadManager::~ThreadManager() {
std::lock_guard lk(ThreadCreationMutex);
for (auto& Thread : Threads) {
HandleThreadDeletion(Thread);
}
Threads.clear();
}
} // namespace FEX::HLE