710 lines
18 KiB
C++
710 lines
18 KiB
C++
/***
|
|
Copyright (C) 2022 J Reece Wilson (a/k/a "Reece"). All rights reserved.
|
|
|
|
File: AuIPCMutexFutex.Linux.cpp
|
|
Date: 2022-
|
|
Author: Reece
|
|
Note:
|
|
***/
|
|
#include <Source/RuntimeInternal.hpp>
|
|
#include "IPC.hpp"
|
|
#include "AuIPCHandle.hpp"
|
|
#include "AuIPCMutexFutex.Linux.hpp"
|
|
#include "AuIPCPrimitives.Linux.hpp"
|
|
#include "AuIPCMemory.Unix.hpp"
|
|
|
|
// LINUX SYSCALL APIS
|
|
#include <linux/futex.h>
|
|
#include <syscall.h>
|
|
|
|
// INTERNAL UTILS
|
|
// ...IO / FD SHARING
|
|
#include <Source/IO/UNIX/FDIpcServer.hpp>
|
|
// ...TIME UTILS
|
|
#include <Source/Time/Time.hpp>
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////
|
|
// CONSTANTS
|
|
////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static const AuUInt8 kMaxFutexes = 128;
|
|
static const AuUInt32 kFutexArraySize = AuUInt32(kMaxFutexes) * (sizeof(AuUInt64) * 3);
|
|
|
|
static const AuUInt32 kFutexIsValid = 0x80000000;
|
|
static const AuUInt32 kFutexIsDead = 0x40000000;
|
|
static const AuUInt32 kFutexIsHasOwner = 0x20000000;
|
|
|
|
//static const AuUInt32 kFutexValueLocked = kFutexIsHasOwner | kFutexIsValid | 1;
|
|
static const AuUInt32 kFutexValueUnlocked = kFutexIsHasOwner;
|
|
static const AuUInt32 kFutexValueNULL = 0;
|
|
|
|
static AuUInt32 gConstNull = 0;
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////
|
|
// VARIABLES
|
|
////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
struct FutexObject
|
|
{
|
|
union
|
|
{
|
|
AuUInt64 maxWord;
|
|
void *nextPtr;
|
|
};
|
|
|
|
union
|
|
{
|
|
AuUInt32 futex;
|
|
AuUInt64 futexPadded;
|
|
};
|
|
};
|
|
|
|
static_assert(sizeof(FutexObject) == 16);
|
|
|
|
static AuThreadPrimitives::SpinLock gLock;
|
|
static AuSPtr<AuIPC::IPCSharedMemory> gFutexSharedMemory;
|
|
static FutexObject *gFutexArray;
|
|
static bool gFutexInit {};
|
|
|
|
static AuIOIPC::IMutexClosedHook * gFutexCallbacks[kMaxFutexes];
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////
|
|
// IMPLEMENTATION
|
|
////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static void InitFutexAPI()
|
|
{
|
|
AU_LOCK_GUARD(gLock);
|
|
|
|
if (AuExchange(gFutexInit, true))
|
|
{
|
|
return;
|
|
}
|
|
|
|
gFutexSharedMemory = AuIOIPC::NewSharedMemory(kFutexArraySize);
|
|
SysAssert(gFutexSharedMemory);
|
|
|
|
gFutexArray = gFutexSharedMemory->GetMemory().Begin<FutexObject>();
|
|
}
|
|
|
|
struct MagicFutexLinkHeader : robust_list_head
|
|
{
|
|
AuUInt32 linkCount {};
|
|
};
|
|
|
|
struct FutexContext
|
|
{
|
|
bool bInit {};
|
|
AuThreadPrimitives::SpinLock lock;
|
|
AuUInt32 tid {};
|
|
|
|
bool Init();
|
|
|
|
bool SetGrugSelf();
|
|
|
|
bool Link(AuUInt32 *ptr);
|
|
bool Unlink(AuUInt32 *ptr);
|
|
|
|
MagicFutexLinkHeader futexArrayHeader {};
|
|
};
|
|
|
|
|
|
static FutexContext gFutexContext;
|
|
void LinuxSuperSecretFuckGlibc();
|
|
static void LinuxLockFutex(AuUInt32 *futex);
|
|
|
|
void LinuxSuperSecretFuckGlibc()
|
|
{
|
|
// Fun: You have to define an entry-relative-offset within a linked list header, hard coding an ABI, for every user
|
|
// program in the tasks/processes address space. Low-iq linux kernel comments state "tehe" check with the glibc folk.
|
|
//
|
|
// Counter, they aren't "folk," they aren't even real people.
|
|
// Fuck you, and fuck your buzzword mutex that hasn't evolved into a stable API since its initial hack of an
|
|
// implementation 20 years ago. I don't want to make a fucking array of pthreads (40+ bytes for a single atomic).
|
|
// I don't want to fucking define a system-wide abi, matching ONE OF MANY fucking glibc ABIs, any given process
|
|
// could be arbitrarily linked against.
|
|
//
|
|
// As a wise man once said, "Linus says fuck you Nvidia, I say f............! That's what I say."
|
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
// Unrelated, here's what FreeBSD says about the UMUTEX_ROBUST list set operation:
|
|
// Note that if any 32-bit ABI compatibility is being requested, then care
|
|
// must be taken with robust lists. A single thread may not mix 32-bit com-
|
|
// patible robust lists with native robust lists. The first
|
|
// UMTX_OP_ROBUST_LISTS call in a given thread determines which ABI that
|
|
// thread will use for robust lists going forward.
|
|
//
|
|
// Code: https://github.com/freebsd/freebsd-src/blob/27a9392d543933f1aaa4e4ddae2a1585a72db1b2/sys/kern/kern_umtx.c#L4150
|
|
//
|
|
// ...FreeBSD just copied the fucking glibc tards between 2012-present didn't they? I bet they were "inspired" by glibcs
|
|
// nptl implementation on Linux, and thought hey, let's just copy this so we can have a functional CRT with shared pt-mutexes
|
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
// I'll just have to use a dedicated watcher process that'll teardown a robust list of only standard
|
|
// entries limited to the Aurora IPC origin. Will use the grug thread for this bc i dont foresee user
|
|
// code locking shared pthread-mutexs under the few user-interfaces grug may call out to.
|
|
//
|
|
SysAssert(gFutexContext.SetGrugSelf());
|
|
}
|
|
|
|
bool FutexContext::SetGrugSelf()
|
|
{
|
|
// Specify the magic word to use...
|
|
this->tid = gettid();
|
|
|
|
// Update the header
|
|
this->futexArrayHeader.list.next = &this->futexArrayHeader.list;
|
|
this->futexArrayHeader.futex_offset = 8;
|
|
this->futexArrayHeader.list_op_pending = NULL;
|
|
|
|
// Update TLS
|
|
if (Aurora::set_robust_list(AuReinterpretCast<robust_list_head *>(&this->futexArrayHeader.list), sizeof(robust_list_head)) != 0)
|
|
{
|
|
SysPushErrorIO("Set robust list failed");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FutexContext::Init()
|
|
{
|
|
AU_LOCK_GUARD(this->lock);
|
|
|
|
if (AuExchange(this->bInit, true))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
InitFutexAPI();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FutexContext::Link(AuUInt32 *ptr)
|
|
{
|
|
if (!ptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!Init())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AU_LOCK_GUARD(this->lock);
|
|
auto temp = AuReinterpretCast<FutexObject *>(AuUInt(ptr) - offsetof(FutexObject, futex));
|
|
|
|
auto firstElement = futexArrayHeader.list.next;
|
|
temp->nextPtr = firstElement;
|
|
|
|
futexArrayHeader.list_op_pending = nullptr;
|
|
futexArrayHeader.list.next = (robust_list *)temp;
|
|
futexArrayHeader.linkCount++;
|
|
return true;
|
|
}
|
|
|
|
bool FutexContext::Unlink(AuUInt32 *ptr)
|
|
{
|
|
if (!ptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!Init())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AU_LOCK_GUARD(this->lock);
|
|
|
|
FutexObject *cur { (FutexObject *)this->futexArrayHeader.list.next };
|
|
FutexObject *prevLink { cur };
|
|
|
|
for (int i = 0; i < this->futexArrayHeader.linkCount; i++)
|
|
{
|
|
if (&cur->futex != ptr)
|
|
{
|
|
prevLink = cur;
|
|
cur = (FutexObject *)cur->nextPtr;
|
|
continue;
|
|
}
|
|
|
|
prevLink->nextPtr = cur->nextPtr;
|
|
this->futexArrayHeader.linkCount--;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static AuUInt8 AllocateFutex()
|
|
{
|
|
gFutexContext.Init();
|
|
if (!gFutexArray)
|
|
{
|
|
return 255;
|
|
}
|
|
|
|
for (AuUInt32 i = 0;
|
|
i < kMaxFutexes;
|
|
i++)
|
|
{
|
|
if (AuAtomicCompareExchange<AuUInt32>(&gFutexArray[i].futex, kFutexValueUnlocked, kFutexValueNULL) == kFutexValueNULL)
|
|
{
|
|
if (gFutexCallbacks[i])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return 255;
|
|
}
|
|
|
|
static bool TryReleaseFutex(AuUInt8 index)
|
|
{
|
|
auto old = gFutexCallbacks[index];
|
|
auto oldState = gFutexArray[index].futexPadded;
|
|
|
|
if ((old) &&
|
|
(!old->OnClosed()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (AuAtomicCompareExchange<AuUInt32>(&gFutexArray[index].futex, kFutexValueUnlocked, oldState) != oldState)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void FreeFutex(AuUInt8 index)
|
|
{
|
|
gFutexArray[index].futexPadded = 0;
|
|
gFutexCallbacks[index] = nullptr;
|
|
}
|
|
|
|
static void LinuxAddRobustFutexSlow(AuUInt32 *futex)
|
|
{
|
|
SysAssert(gFutexContext.Init());
|
|
SysAssert(gFutexContext.Link(futex));
|
|
}
|
|
|
|
static void LinuxRemoveRobustFutexSlow(AuUInt32 *futex)
|
|
{
|
|
SysAssert(gFutexContext.Init());
|
|
if (!gFutexContext.Unlink(futex))
|
|
{
|
|
SysPushErrorIO("Unlink futex error: {}", fmt::ptr(futex));
|
|
}
|
|
}
|
|
|
|
static bool LinuxLockFutex(AuUInt32 *futex, AuUInt32 timeout)
|
|
{
|
|
bool bContended;
|
|
struct timespec tspec;
|
|
AuUInt32 value = gFutexContext.tid;
|
|
|
|
if (timeout)
|
|
{
|
|
AuTime::ms2tsabs(&tspec, timeout);
|
|
}
|
|
|
|
do
|
|
{
|
|
auto old = AuAtomicCompareExchange<AuUInt32>(futex, value, kFutexValueUnlocked);
|
|
bContended = old != kFutexValueUnlocked;
|
|
if (bContended)
|
|
{
|
|
int res = Aurora::futex_wait_shared(futex, old, timeout ? &tspec : nullptr);
|
|
if (res < 0)
|
|
{
|
|
if (res == ETIMEDOUT || errno == ETIMEDOUT)
|
|
{
|
|
return false;
|
|
}
|
|
else if (res == -EAGAIN || errno == EAGAIN)
|
|
{
|
|
bContended = true;
|
|
}
|
|
else
|
|
{
|
|
SysPushErrorIO("FUTEX ERROR: {}", res);
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bContended = false;
|
|
}
|
|
}
|
|
}
|
|
while (bContended);
|
|
|
|
::LinuxAddRobustFutexSlow(futex);
|
|
return true;
|
|
}
|
|
|
|
static bool LinuxTryLockFutex(AuUInt32 *futex)
|
|
{
|
|
if (AuAtomicCompareExchange<AuUInt32>(futex,
|
|
gFutexContext.tid,
|
|
kFutexValueUnlocked) != kFutexValueUnlocked)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
LinuxAddRobustFutexSlow(futex);
|
|
return true;
|
|
}
|
|
|
|
static bool LinuxUnlockFutex(AuUInt32 *futex)
|
|
{
|
|
LinuxRemoveRobustFutexSlow(futex);
|
|
|
|
if (AuAtomicCompareExchange<AuUInt32>(futex,
|
|
kFutexValueUnlocked,
|
|
gFutexContext.tid) != gFutexContext.tid)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Aurora::futex_wake_shared(futex, 1);
|
|
|
|
return true;
|
|
}
|
|
|
|
void LinuxSuperSecretIOTick()
|
|
{
|
|
if (!gFutexInit)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!gFutexArray)
|
|
{
|
|
return;
|
|
}
|
|
|
|
static Aurora::Utility::RateLimiter gLimiter;
|
|
|
|
if (!gLimiter.nextTriggerTime)
|
|
{
|
|
gLimiter.noCatchUp = true;
|
|
gLimiter.SetNextStep(AuMSToNS<AuUInt64>(3'000));
|
|
}
|
|
|
|
if (!gLimiter.CheckExchangePass())
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (AuUInt32 i = 0;
|
|
i < kMaxFutexes;
|
|
i++)
|
|
{
|
|
auto val = gFutexArray[i].futex;
|
|
val &= ~(0x3fffffff);
|
|
if ((val & kFutexIsDead) != 0)
|
|
{
|
|
::TryReleaseFutex(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace Aurora::IO::IPC
|
|
{
|
|
static AuThreadPrimitives::SpinLock gLock;
|
|
static AuBST<AuPair<AuUInt64 /*cookie*/, AuUInt32 /*pid*/>, AuWPtr<IPCSharedMemory>> gSharedViewCache;
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Mutexes
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
IPCMutexProxy::IPCMutexProxy(AuUInt32 index) :
|
|
mutex_(),
|
|
bOwned(true),
|
|
index_(index)
|
|
{
|
|
if (this->mutex_.HasValidHandle())
|
|
{
|
|
if (IO::UNIX::FDServe(this->GetHandle(), this->token_))
|
|
{
|
|
//this->handle_.PushId(EIPCHandleType::eIPCPrimitiveMutex, token);
|
|
}
|
|
else
|
|
{
|
|
this->mutex_.~LSMutex();
|
|
}
|
|
}
|
|
|
|
this->mem_ = gFutexSharedMemory;
|
|
this->mutex_.bNoAutoRel = true;
|
|
|
|
gFutexCallbacks[index] = this;
|
|
}
|
|
|
|
IPCMutexProxy::IPCMutexProxy(int handle, AuSPtr<IPCSharedMemory> mem, AuUInt32 index) :
|
|
mutex_(handle),
|
|
mem_(mem),
|
|
index_(index)
|
|
{
|
|
if (this->mutex_.HasValidHandle())
|
|
{
|
|
if (IO::UNIX::FDServe(this->GetHandle(), this->token_))
|
|
{
|
|
//this->handle_.PushId(EIPCHandleType::eIPCPrimitiveMutex, token);
|
|
}
|
|
else
|
|
{
|
|
this->mutex_.~LSMutex();
|
|
}
|
|
}
|
|
|
|
this->mutex_.bNoAutoRel = true;
|
|
}
|
|
|
|
IPCMutexProxy::~IPCMutexProxy()
|
|
{
|
|
IO::UNIX::FDServeEnd(this->token_);
|
|
|
|
if (this->bOwned)
|
|
{
|
|
::FreeFutex(this->index_);
|
|
}
|
|
}
|
|
|
|
AuUInt32 *IPCMutexProxy::GetFutex()
|
|
{
|
|
return &this->mem_->GetMemory().Begin<FutexObject>()[this->index_].futex;
|
|
}
|
|
|
|
bool IPCMutexProxy::Unlock()
|
|
{
|
|
auto futex = this->GetFutex();
|
|
if (!futex)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!::LinuxUnlockFutex(futex))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
SysAssert(this->mutex_.Unlock());
|
|
this->leakSelf_.reset();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IPCMutexProxy::IsSignaled()
|
|
{
|
|
auto futex = this->GetFutex();
|
|
if (!futex)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!::LinuxTryLockFutex(futex))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!this->mutex_.IsSignaled())
|
|
{
|
|
::LinuxUnlockFutex(futex);
|
|
return false;
|
|
}
|
|
|
|
this->leakSelf_ = AuSharedFromThis();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IPCMutexProxy::WaitOn(AuUInt32 timeout)
|
|
{
|
|
auto futex = this->GetFutex();
|
|
if (!futex)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!::LinuxLockFutex(futex, timeout))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!this->mutex_.IsSignaled())
|
|
{
|
|
::LinuxUnlockFutex(futex);
|
|
return false;
|
|
}
|
|
|
|
this->leakSelf_ = AuSharedFromThis();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IPCMutexProxy::OnClosed()
|
|
{
|
|
auto futex = this->GetFutex();
|
|
|
|
if (this->pMutexClosedHook)
|
|
{
|
|
if (!this->pMutexClosedHook->OnClosed())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (futex)
|
|
{
|
|
auto a = *futex;
|
|
if (*futex == kFutexIsDead)
|
|
{
|
|
this->mutex_.Unlock();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Loop::ELoopSource IPCMutexProxy::GetType()
|
|
{
|
|
return this->mutex_.GetType();
|
|
}
|
|
|
|
AuString IPCMutexProxy::ExportToString()
|
|
{
|
|
IPC::IPCHandle handle;
|
|
|
|
handle.PushId(EIPCHandleType::eIPCPrimitiveMutex, this->token_);
|
|
SysAssert(this->mem_);
|
|
handle.PushId(EIPCHandleType::eIPCMemory, AuStaticCast<IPCSharedMemoryImpl>(this->mem_)->handle_.values[0].token);
|
|
handle.values[0].token.word = this->index_;
|
|
|
|
return handle.ToString();
|
|
}
|
|
|
|
AUKN_SYM AuSPtr<IPCMutex> NewMutex()
|
|
{
|
|
auto futex = ::AllocateFutex();
|
|
if (futex == 255)
|
|
{
|
|
return {};
|
|
}
|
|
|
|
auto object = AuMakeShared<IPCMutexProxy>(futex);
|
|
if (!object)
|
|
{
|
|
SysPushErrorMem();
|
|
return {};
|
|
}
|
|
|
|
if (!object->HasValidHandle())
|
|
{
|
|
SysPushErrorIO();
|
|
return {};
|
|
}
|
|
|
|
return object;
|
|
}
|
|
|
|
static AuSPtr<IPCSharedMemory> GetFutexPagesFromCacheOrImport(const IPCToken &mem)
|
|
{
|
|
AU_LOCK_GUARD(gLock);
|
|
|
|
auto id = AuMakePair(mem.cookie, mem.pid);
|
|
auto itr = gSharedViewCache.find(id);
|
|
if (itr != gSharedViewCache.end())
|
|
{
|
|
auto test = itr->second.lock();
|
|
if (test)
|
|
{
|
|
return test;
|
|
}
|
|
}
|
|
|
|
auto shared = ImportSharedMemoryEx(mem);
|
|
if (!shared)
|
|
{
|
|
SysPushErrorNested();
|
|
return {};
|
|
}
|
|
|
|
if (!AuTryInsert(gSharedViewCache, id, shared))
|
|
{
|
|
SysPushErrorMem();
|
|
// We don't need to fail. We can leak next time. It'll be fine.
|
|
}
|
|
|
|
return shared;
|
|
}
|
|
|
|
AuSPtr<IPCMutexProxy> ImportMutexEx(const IPCToken &handle, const IPCToken &mem, AuUInt32 index)
|
|
{
|
|
int fd {-1};
|
|
if (!IO::UNIX::FDAccept(handle, fd))
|
|
{
|
|
SysPushErrorNested();
|
|
return {};
|
|
}
|
|
|
|
auto view = GetFutexPagesFromCacheOrImport(mem);
|
|
if (!view)
|
|
{
|
|
SysPushErrorNested();
|
|
return {};
|
|
}
|
|
|
|
auto object = AuMakeShared<IPCMutexProxy>(fd, view, index);
|
|
if (!object)
|
|
{
|
|
SysPushErrorMem();
|
|
::close(fd);
|
|
return {};
|
|
}
|
|
|
|
if (!object->HasValidHandle())
|
|
{
|
|
SysPushErrorIO();
|
|
return {};
|
|
}
|
|
|
|
return object;
|
|
}
|
|
|
|
AUKN_SYM AuSPtr<IPCMutex> ImportMutex(const AuString &handle)
|
|
{
|
|
IPC::IPCHandle decodedHandle;
|
|
|
|
if (!decodedHandle.FromString(handle))
|
|
{
|
|
SysPushErrorParseError("Invalid handle: {}", handle);
|
|
return {};
|
|
}
|
|
|
|
auto val = decodedHandle.GetToken(IPC::EIPCHandleType::eIPCPrimitiveMutex, 0);
|
|
if (!val)
|
|
{
|
|
SysPushErrorParseError("Invalid handle: {}", handle);
|
|
return {};
|
|
}
|
|
|
|
auto mem = decodedHandle.GetToken(IPC::EIPCHandleType::eIPCMemory, 1);
|
|
if (!mem)
|
|
{
|
|
SysPushErrorParseError("Invalid handle: {}", handle);
|
|
return {};
|
|
}
|
|
|
|
return ImportMutexEx(val->token, mem->token, val->token.word);
|
|
}
|
|
} |