fix: kernel concurrency, interrupt context, and user VA safety

This commit is contained in:
daniel committed 2026-08-01 15:43:38 +02:00
1 parent f222bf1f04
commit b1c55073c7
71 files changed
+2654 -1092

No files matched your search

+386 -47
View File
@@ -11,6 +11,7 @@
#include <Memory/HHDM.hpp>
#include <Libraries/Memory.hpp>
#include <Libraries/String.hpp>
#include <Common/Panic.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <CppLib/Spinlock.hpp>
@@ -71,21 +72,105 @@ namespace Sched {
// It is held ACROSS context switches to prevent the race where
// another CPU picks up a process whose RSP hasn't been saved yet.
// The resumed process releases it.
static kcp::Spinlock schedLock;
// schedLock is a baton: the context that acquires it switches stacks and
// the context being resumed releases it. A Spinlock's ordinary shared
// savedFlags field cannot represent that -- it would restore the switching
// context's IF state into the resumed stack. Keep the flags with each saved
// process/idle context instead, so timer frames resume with IF=0 and syscall
// frames resume with their original IF=1 even after CPU migration.
class SchedulerSpinlock {
kcp::Spinlock lock;
uint64_t processFlags[MaxProcesses] = {};
uint64_t idleFlags[Smp::MaxCPUs] = {};
public:
void Acquire() {
uint64_t flags = lock.AcquireIrqSave();
auto* cpu = Smp::GetCurrentCpuData();
if (cpu != nullptr && cpu->currentSlot >= 0 && cpu->currentSlot < MaxProcesses) {
processFlags[cpu->currentSlot] = flags;
} else if (cpu != nullptr && cpu->cpuIndex >= 0 && cpu->cpuIndex < Smp::MaxCPUs) {
idleFlags[cpu->cpuIndex] = flags;
}
}
void Release() {
uint64_t flags = 0; // Fresh startup remains IF=0 until JumpToUserMode's IRETQ.
auto* cpu = Smp::GetCurrentCpuData();
if (cpu != nullptr && cpu->currentSlot >= 0 && cpu->currentSlot < MaxProcesses) {
flags = processFlags[cpu->currentSlot];
} else if (cpu != nullptr && cpu->cpuIndex >= 0 && cpu->cpuIndex < Smp::MaxCPUs) {
flags = idleFlags[cpu->cpuIndex];
}
lock.ReleaseIrqRestore(flags);
}
void ReleaseForStartup() {
lock.ReleaseIrqNoRestore();
}
};
static SchedulerSpinlock schedLock;
// Approximate count of Ready processes. Incremented/decremented
// under schedLock. Idle CPUs check this to avoid scanning all 256
// process slots on every timer tick.
static volatile int readyCount = 0;
// Updated while schedLock is held, but sampled locklessly by idle CPUs and
// reschedule-IPI paths. Atomic loads make those wakeup decisions coherent
// across cores instead of relying on volatile's compiler-only semantics.
static std::atomic<int> readyCount{0};
// Per-object wake generations close the readiness-check/enrollment gap.
// Each bucket packs [63:32] = a tag identifying the last object to signal
// it and [31:0] = a signal counter, published together in one atomic word.
// Publishing both halves atomically lets a waiter distinguish "exactly one
// signal landed here and it carried someone else's tag" (safe to keep
// blocking) from "something ambiguous happened" (must treat as a wake).
// Without the tag, an unrelated hot object sharing the bucket forces the
// waiter to re-run its condition on every signal. Tag collisions only cost
// a spurious wake; they can never lose one, which is the only direction
// that would reintroduce the lost-wakeup bug this table exists to prevent.
static constexpr uint32_t ObjectWakeBuckets = 1024;
static std::atomic<uint64_t> objectWakeEpochs[ObjectWakeBuckets] = {};
static uint32_t ObjectWakeBucket(void* object) {
uint64_t value = (uint64_t)object;
return (uint32_t)(((value >> 4) ^ (value >> 12)) & (ObjectWakeBuckets - 1));
}
static uint32_t ObjectWakeTag(void* object) {
uint64_t value = (uint64_t)object;
value ^= value >> 32;
value *= 0x9E3779B97F4A7C15ULL;
return (uint32_t)(value >> 32);
}
// The idle loop runs in the kernel PML4
static uint64_t GetKernelCR3() {
return (uint64_t)Memory::VMM::g_paging->PML4;
}
static void RescheduleIpiHandler(uint8_t) {
static void RescheduleIpiHandler(uint8_t, bool interruptedUser) {
auto* cpu = Smp::GetCurrentCpuData();
if (cpu == nullptr || cpu->currentSlot >= 0 || readyCount <= 0) {
if (cpu == nullptr) {
return;
}
// Process teardown uses this IPI to promptly stop sibling threads on
// other CPUs. A user-mode sibling can exit immediately. If the IPI
// interrupted a syscall, merely expire its slice: teardown on that
// suspended kernel stack could reacquire a mutex it already owns.
if (cpu->currentSlot >= 0 &&
processTable[cpu->currentSlot].killPending.load(std::memory_order_acquire)) {
if (interruptedUser) {
ExitProcess();
return;
}
processTable[cpu->currentSlot].sliceRemaining = 0;
return;
}
if (cpu->currentSlot >= 0 || readyCount <= 0 || cpu->reservedForKernelWork) {
return;
}
@@ -101,10 +186,10 @@ namespace Sched {
if (target == nullptr || !target->started) return false;
if (target->cpuIndex == sourceCpuIndex) return false;
if (target->currentSlot >= 0) return false;
if (target->reservedForKernelWork) return false;
Hal::LocalApic::SendFixedIpi(target->lapicId,
return Hal::LocalApic::SendFixedIpi(target->lapicId,
Hal::IRQ_VECTOR_BASE + Hal::IRQ_RESCHEDULE);
return true;
};
if (tryKick(Smp::GetCpuData(0))) {
@@ -163,7 +248,9 @@ namespace Sched {
// The schedLock is held (acquired by the switching-from CPU's Schedule).
static void ProcessStartup() {
// Release the schedLock that the switching-from CPU held
schedLock.Release();
// without consulting this brand-new slot's (intentionally empty)
// saved interrupt state. JumpToUserMode's IRETQ enables interrupts.
schedLock.ReleaseForStartup();
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
@@ -526,10 +613,11 @@ namespace Sched {
// Give the new thread its own TLS block when the image has one.
// We are executing inside the process's address space, so the
// freshly mapped pages and the template are directly addressable.
// The block is process-heap memory; it is reclaimed with the rest
// of the user half at process teardown (a thread that exits before
// its process leaks its block until then - acceptable for now).
// The block uses process-heap virtual space. Its physical pages are
// reclaimed when the sibling exits (or if creation fails).
uint64_t threadFsBase = 0;
uint64_t threadTlsBase = 0;
uint64_t threadTlsPages = 0;
Process& primary = processTable[primarySlot_];
if (primary.tlsMemSize > 0) {
uint64_t align = primary.tlsAlign < 16 ? 16 : primary.tlsAlign;
@@ -537,12 +625,15 @@ namespace Sched {
uint64_t total = S + 16;
uint64_t numPages = (total + 0xFFF) / 0x1000;
schedLock.Acquire();
uint64_t base = primary.heapNext;
primary.heapNext += numPages * 0x1000;
schedLock.Release();
uint64_t base = 0;
if (!ReserveUserHeapRange(primarySlot_, numPages * 0x1000, base)) {
Kt::KernelLogStream(Kt::ERROR, "Sched")
<< "Thread TLS exceeds user heap range";
return -1;
}
bool ok = true;
uint64_t mappedPages = 0;
for (uint64_t p = 0; p < numPages; p++) {
void* page = Memory::g_pfa->AllocateZeroed();
if (page == nullptr) {
@@ -552,11 +643,14 @@ namespace Sched {
uint64_t physAddr = Memory::SubHHDM((uint64_t)page);
if (!Memory::VMM::Paging::MapUserIn(sharedPml4, physAddr,
base + p * 0x1000)) {
Memory::g_pfa->Free(page);
ok = false;
break;
}
mappedPages++;
}
if (!ok) {
Ipc::UnmapAndFreeUserRange(sharedPml4, base, mappedPages);
Kt::KernelLogStream(Kt::ERROR, "Sched")
<< "Thread TLS allocation failed";
return -1;
@@ -567,6 +661,8 @@ namespace Sched {
primary.tlsFileSize);
*(uint64_t*)tp = tp; /* ABI self-pointer */
threadFsBase = tp;
threadTlsBase = base;
threadTlsPages = numPages;
}
// We do not write to the user stack from kernel mode (that would
@@ -578,7 +674,11 @@ namespace Sched {
// Allocate kernel stack.
void* stackMem = Memory::g_pfa->ReallocConsecutive(nullptr, StackPages);
if (stackMem == nullptr) return -1;
if (stackMem == nullptr) {
if (threadTlsPages != 0)
Ipc::UnmapAndFreeUserRange(sharedPml4, threadTlsBase, threadTlsPages);
return -1;
}
memset(stackMem, 0, StackSize);
uint8_t* kernelStackBase = (uint8_t*)stackMem;
uint64_t kernelStackTop = (uint64_t)kernelStackBase + StackSize;
@@ -598,6 +698,8 @@ namespace Sched {
if (slot < 0) {
schedLock.Release();
Memory::g_pfa->Free(stackMem, StackPages);
if (threadTlsPages != 0)
Ipc::UnmapAndFreeUserRange(sharedPml4, threadTlsBase, threadTlsPages);
return -1;
}
@@ -663,6 +765,54 @@ namespace Sched {
return tid;
}
bool ReserveUserHeapRange(int primarySlot_, uint64_t size, uint64_t& outVa) {
outVa = 0;
if (primarySlot_ < 0 || primarySlot_ >= MaxProcesses || size == 0 ||
(size & 0xFFFULL) != 0) {
return false;
}
schedLock.Acquire();
Process& primary = processTable[primarySlot_];
uint64_t base = primary.heapNext;
bool validOwner = primary.primarySlot == primarySlot_ &&
primary.state != ProcessState::Free &&
primary.state != ProcessState::Terminated;
bool fits = validOwner && base >= UserHeapBase &&
base <= UserHeapLimit && size <= UserHeapLimit - base;
if (fits) {
primary.heapNext = base + size;
outVa = base;
}
schedLock.Release();
return fits;
}
static void FreeSiblingThreadTls(Process& thr) {
int primarySlot_ = thr.primarySlot;
if (primarySlot_ < 0 || primarySlot_ >= MaxProcesses ||
primarySlot_ == (int)(&thr - processTable) || thr.fsBase == 0) {
return;
}
Process& primary = processTable[primarySlot_];
if (primary.tlsMemSize == 0 || primary.pml4Phys == 0) {
thr.fsBase = 0;
return;
}
uint64_t align = primary.tlsAlign < 16 ? 16 : primary.tlsAlign;
uint64_t blockSize = (primary.tlsMemSize + align - 1) & ~(align - 1);
if (blockSize > thr.fsBase || blockSize > UINT64_MAX - 16) {
Panic("Invalid sibling TLS metadata during thread teardown", nullptr);
}
uint64_t base = thr.fsBase - blockSize;
uint64_t pages = (blockSize + 16 + 0xFFF) / 0x1000;
thr.fsBase = 0;
Ipc::UnmapAndFreeUserRange(primary.pml4Phys, base, pages);
}
// Switch away from a slot we have just marked non-runnable while holding
// schedLock. Mirrors SwitchAwayFromBlockedCurrentLocked but does NOT
// assume the caller will be resumed -- used by ExitCurrentThread.
@@ -714,10 +864,20 @@ namespace Sched {
__builtin_unreachable();
}
thr.killPending = false;
bool processExitKill =
thr.killPending.exchange(false, std::memory_order_acq_rel);
thr.startPending = false;
thr.exitCode = exitCode;
// TLS belongs only to this sibling. Reclaim it before publishing the
// Terminated state; otherwise a long-lived multithreaded GUI process
// leaks these pages until the entire process exits. When the primary
// is already coordinating full teardown, defer this to its final
// sibling sweep: the primary may be waiting from an IF=0 timer frame
// and therefore cannot acknowledge our TLB-shootdown IPI here.
if (!processExitKill)
FreeSiblingThreadTls(thr);
schedLock.Acquire();
thr.state = ProcessState::Terminated;
thr.runningOnCpu = -1;
@@ -847,10 +1007,16 @@ namespace Sched {
// New processes release it in ProcessStartup().
// ====================================================================
// Reclaim terminated process slots. Called from BSP's Tick only,
// NOT from every Schedule() call on every CPU. This avoids holding
// schedLock (with interrupts disabled) during PFA::Free on the
// hot scheduling path.
// BSP maintenance that may allocate/free memory or touch policy MSRs must
// run from the idle kernel context, never from the timer interrupt. The
// pending flag is set by process teardown on any CPU and periodically by
// the BSP timer. It remains set until an idle maintenance pass consumes it.
static std::atomic<bool> bspMaintenancePending{false};
static uint64_t nextBspPolicyTick = 0; // BSP timer only
// Reclaim terminated process slots from BSP idle context. This avoids
// holding schedLock (with interrupts disabled) during PFA::Free on the
// hot scheduling path or inside a hard interrupt.
static void ReclaimTerminated() {
schedLock.Acquire();
for (int i = 0; i < MaxProcesses; i++) {
@@ -880,7 +1046,17 @@ namespace Sched {
return readyCount > 0;
}
void RunBspMaintenance() {
void RequestKernelWork() {
auto* cpu = Smp::GetCurrentCpuData();
if (cpu != nullptr && cpu->currentSlot >= 0) {
cpu->kernelWorkPending = true;
}
}
// Bounded, IRQ-safe portion of BSP maintenance. Keeping deadline expiry
// on every 1 ms BSP tick prevents timed input/object waits from depending
// on the BSP becoming idle under sustained desktop load.
static void WakeExpiredProcesses() {
bool wokeProcesses = false;
schedLock.Acquire();
@@ -902,12 +1078,19 @@ namespace Sched {
if (wokeProcesses) {
KickOneIdleCpu(0);
}
}
void RunBspMaintenance() {
// Clear before scanning. A producer that races this pass sets the flag
// again and therefore cannot have its request acknowledged by mistake.
if (!bspMaintenancePending.exchange(false, std::memory_order_acq_rel)) {
return;
}
ReclaimTerminated();
// Thermal governor step (rate-limited internally to 500 ms).
// Runs here because BSP maintenance is reached from both the idle
// loop and the timer tick, so it keeps running under full load.
// The timer requests this idle-context pass even under full load.
Hal::CpuPower::ThermalTick(Timekeeping::GetMilliseconds());
}
@@ -933,21 +1116,41 @@ namespace Sched {
schedLock.Acquire();
// Find the next Ready process (round-robin from after current slot)
// Normally select the next Ready process. A pending bottom half gets
// one deliberate trip through the idle kernel context first; that
// context drains bounded work and immediately schedules user work
// again. This avoids both hard-IRQ callbacks and bottom-half starvation
// when there is always at least one runnable desktop process.
int next = -1;
bool yieldToKernel = cpu->currentSlot >= 0 && cpu->kernelWorkPending;
if (yieldToKernel) cpu->kernelWorkPending = false;
int start = (cpu->currentSlot >= 0) ? cpu->currentSlot + 1 : 0;
for (int i = 0; i < MaxProcesses; i++) {
int idx = (start + i) % MaxProcesses;
if (processTable[idx].state == ProcessState::Ready) {
next = idx;
break;
if (!yieldToKernel) {
for (int i = 0; i < MaxProcesses; i++) {
int idx = (start + i) % MaxProcesses;
if (processTable[idx].state == ProcessState::Ready) {
next = idx;
break;
}
}
}
if (next < 0) {
// No ready processes. If we were running one, return to idle.
// No other process is ready. A normal time-slice expiry must keep
// the sole Running process on this CPU: bouncing it through idle
// can suspend a syscall while it owns a process-context Mutex, then
// let ServiceDeferredWork spin on that same mutex with the idle
// context reserved against scheduling. That is a permanent
// self-deadlock. Only an explicit, ring-3-safe bottom-half request
// is allowed to yield a running process to idle.
if (cpu->currentSlot >= 0) {
if (!yieldToKernel) {
processTable[cpu->currentSlot].sliceRemaining = TimeSliceMs;
schedLock.Release();
return;
}
int oldSlot = cpu->currentSlot;
processTable[oldSlot].state = ProcessState::Ready;
readyCount++;
@@ -1019,7 +1222,7 @@ namespace Sched {
schedLock.Release();
}
void Tick(uint32_t elapsedMs) {
void Tick(uint32_t elapsedMs, bool interruptedUser) {
auto* cpu = Smp::GetCurrentCpuData();
// Pick up thermal-governor frequency changes (HWP requests are
@@ -1027,9 +1230,21 @@ namespace Sched {
// the governor bumped the policy epoch since our last tick.
Hal::CpuPower::ApplyPolicyIfChanged();
// BSP: wake sleeping processes and reclaim terminated slots
// BSP: expire timed waits in the bounded IRQ-safe pass. Queue heavier
// reclamation/policy work for the idle kernel context.
if (cpu->cpuIndex == 0) {
RunBspMaintenance();
WakeExpiredProcesses();
uint64_t now = Timekeeping::GetTicks();
if (nextBspPolicyTick == 0 || now >= nextBspPolicyTick) {
nextBspPolicyTick = now + 500;
bspMaintenancePending.store(true, std::memory_order_release);
}
if (interruptedUser &&
bspMaintenancePending.load(std::memory_order_acquire)) {
RequestKernelWork();
}
}
int slot = cpu->currentSlot;
@@ -1052,12 +1267,21 @@ namespace Sched {
processTable[slot].cpuTimeMs += elapsedMs;
// Check if another CPU requested this process be killed.
// We are on the CPU running it, so ExitProcess is safe here.
if (cpu->kernelWorkPending) {
processTable[slot].sliceRemaining = 0;
}
// Check if another CPU requested this process be killed. Full teardown
// takes process-context mutexes. It is safe here only when the timer
// interrupted ring 3; if it interrupted a syscall, teardown could try
// to reacquire a lock already held by this same suspended kernel stack.
// In that case force a reschedule and finish at syscall-return instead.
if (processTable[slot].killPending) {
processTable[slot].killPending = false;
ExitProcess();
return;
if (interruptedUser) {
ExitProcess();
return;
}
processTable[slot].sliceRemaining = 0;
}
if (processTable[slot].sliceRemaining > elapsedMs) {
@@ -1066,7 +1290,13 @@ namespace Sched {
processTable[slot].sliceRemaining = 0;
}
if (processTable[slot].sliceRemaining == 0) {
// Never context-switch away from an arbitrary kernel stack. Syscalls
// routinely own non-IRQ-disabling subsystem mutexes; suspending that
// stack and dispatching a process that wants the same mutex leaves the
// new process spinning in ring 0, where the timer deliberately cannot
// preempt it to resume the owner. Preserve the expired slice and take
// the switch on the first timer interrupt after SYSRET reaches ring 3.
if (processTable[slot].sliceRemaining == 0 && interruptedUser) {
Schedule();
}
}
@@ -1080,6 +1310,14 @@ namespace Sched {
return processTable[primary].pid;
}
void ExitIfKilled() {
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu ? cpu->currentSlot : -1;
if (slot >= 0 && processTable[slot].killPending) {
ExitProcess();
}
}
int GetCurrentTid() {
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
@@ -1173,8 +1411,17 @@ namespace Sched {
// terminated immediately; siblings running on other CPUs are tagged
// killPending and we spin until their tick handler routes them
// through ExitCurrentThread.
// Several device syscalls have bounded waits up to five seconds. Give
// an interrupted kernel-mode sibling room to finish that recovery
// path before declaring it wedged.
static constexpr uint64_t SiblingDrainTimeoutMs = 10000;
static constexpr uint32_t SiblingDrainMaxPasses = 262144;
uint64_t siblingDrainDeadline = Timekeeping::GetTicks() + SiblingDrainTimeoutMs;
uint32_t siblingDrainPasses = 0;
for (;;) {
bool anyRunning = false;
bool targetCpus[Smp::MaxCPUs] = {};
schedLock.Acquire();
for (int i = 0; i < MaxProcesses; i++) {
if (i == primarySlot_) continue;
@@ -1184,7 +1431,10 @@ namespace Sched {
continue;
}
if (st == ProcessState::Running) {
processTable[i].killPending = true;
processTable[i].killPending.store(true, std::memory_order_release);
int targetCpu = processTable[i].runningOnCpu;
if (targetCpu >= 0 && targetCpu < Smp::MaxCPUs)
targetCpus[targetCpu] = true;
anyRunning = true;
continue;
}
@@ -1211,6 +1461,30 @@ namespace Sched {
}
schedLock.Release();
if (!anyRunning) break;
// Do not depend solely on a remote CPU's next periodic timer.
// Laptop firmware/IRQ pathologies can delay that tick, and the
// old unbounded wait then trapped the exiting CPU forever.
if ((siblingDrainPasses & 0x3FF) == 0) {
for (int i = 0; i < Smp::GetCpuCount(); i++) {
if (!targetCpus[i] || i == cpu->cpuIndex) continue;
Smp::CpuData* target = Smp::GetCpuData(i);
if (target != nullptr && target->started) {
(void)Hal::LocalApic::SendFixedIpi(target->lapicId,
Hal::IRQ_VECTOR_BASE + Hal::IRQ_RESCHEDULE);
}
}
}
siblingDrainPasses++;
if (siblingDrainPasses >= SiblingDrainMaxPasses ||
Timekeeping::GetTicks() >= siblingDrainDeadline) {
// It is unsafe to free the shared address space while another
// CPU may still execute in it. A visible panic is preferable
// to silent lockup or use-after-free corruption.
Panic("Timed out draining sibling threads during process exit", nullptr);
}
// Wait for the sibling CPU(s) to observe killPending on their
// next timer tick. Briefly busy-wait; this path is rare.
for (int spin = 0; spin < 1024; spin++) {
@@ -1224,6 +1498,10 @@ namespace Sched {
if (i == primarySlot_) continue;
if (processTable[i].primarySlot != primarySlot_) continue;
if (processTable[i].state != ProcessState::Terminated) continue;
// Ready/blocked siblings terminated inline above never ran their
// ordinary ExitCurrentThread cleanup. Running siblings normally
// arrive here with fsBase already cleared, making this a no-op.
FreeSiblingThreadTls(processTable[i]);
void* stackBase = (void*)processTable[i].stackBase;
processTable[i].stackBase = 0;
processTable[i].pml4Phys = 0;
@@ -1280,6 +1558,7 @@ namespace Sched {
proc.state = ProcessState::Terminated;
proc.runningOnCpu = -1;
proc.reapReady = true;
bspMaintenancePending.store(true, std::memory_order_release);
// Publish the exit code before waking waiters so SYS_WAITPID
// observes it the moment it unblocks.
@@ -1429,6 +1708,8 @@ namespace Sched {
}
void BlockOnPid(int pid) {
ExitIfKilled();
// If the target is already dead, return immediately
if (!IsAlive(pid)) return;
@@ -1464,11 +1745,14 @@ namespace Sched {
processTable[slot].sleepUntilTick = 0;
processTable[slot].runningOnCpu = -1;
SwitchAwayFromBlockedCurrentLocked();
ExitIfKilled();
}
void BlockForSleep(uint64_t ms) {
if (ms == 0) return;
ExitIfKilled();
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
if (slot < 0) return;
@@ -1481,11 +1765,14 @@ namespace Sched {
processTable[slot].sleepUntilTick = Timekeeping::GetTicks() + ms;
processTable[slot].runningOnCpu = -1;
SwitchAwayFromBlockedCurrentLocked();
ExitIfKilled();
}
void BlockOnObject(void* object, uint64_t timeoutMs) {
if (object == nullptr) return;
ExitIfKilled();
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
if (slot < 0) return;
@@ -1499,12 +1786,15 @@ namespace Sched {
: 0;
processTable[slot].runningOnCpu = -1;
SwitchAwayFromBlockedCurrentLocked();
ExitIfKilled();
}
bool BlockOnObjectIf(void* object, uint64_t timeoutMs,
bool (*shouldBlock)(void*), void* context) {
if (object == nullptr || shouldBlock == nullptr) return false;
ExitIfKilled();
auto* cpu = Smp::GetCurrentCpuData();
if (cpu == nullptr) return false;
@@ -1526,12 +1816,58 @@ namespace Sched {
: 0;
processTable[slot].runningOnCpu = -1;
SwitchAwayFromBlockedCurrentLocked();
ExitIfKilled();
return true;
}
uint64_t ObserveObjectWake(void* object) {
if (object == nullptr) return 0;
return objectWakeEpochs[ObjectWakeBucket(object)].load(std::memory_order_acquire);
}
struct ObjectWakeObservation {
void* object;
uint64_t epoch;
};
static bool ObjectWakeIsUnchanged(void* opaque) {
auto* observation = (ObjectWakeObservation*)opaque;
uint64_t now = ObserveObjectWake(observation->object);
uint64_t was = observation->epoch;
if (now == was) return true; // nothing signalled this bucket at all
// Exactly one signal landed here since the observation, and it carried
// a different object's tag, so it cannot have been ours: the caller may
// still block. Any larger or wrapped delta is ambiguous -- more than
// one object may have signalled -- and must be reported as a wake so a
// real edge is never dropped.
if ((uint32_t)now == (uint32_t)was + 1 &&
(uint32_t)(now >> 32) != ObjectWakeTag(observation->object)) {
return true;
}
return false;
}
bool BlockOnObjectSince(void* object, uint64_t timeoutMs, uint64_t observedWake) {
ObjectWakeObservation observation{object, observedWake};
return BlockOnObjectIf(object, timeoutMs, ObjectWakeIsUnchanged, &observation);
}
void WakeObjectWaiters(void* object) {
if (object == nullptr) return;
// Publish this object's tag and the bumped counter as one word. A CAS
// loop (rather than fetch_add) keeps the pair consistent and, unlike a
// plain store, cannot drop a concurrent waker's increment on the same
// bucket -- a dropped increment would be an unobserved wake edge.
auto& bucket = objectWakeEpochs[ObjectWakeBucket(object)];
uint64_t tagBits = (uint64_t)ObjectWakeTag(object) << 32;
uint64_t observed = bucket.load(std::memory_order_relaxed);
while (!bucket.compare_exchange_weak(
observed, tagBits | (uint32_t)((uint32_t)observed + 1),
std::memory_order_release, std::memory_order_relaxed)) {
}
bool wokeAny = false;
schedLock.Acquire();
for (int i = 0; i < MaxProcesses; i++) {
@@ -1608,13 +1944,10 @@ namespace Sched {
bool inheritRedir = crasher && crasher->redirected &&
crasher->ioOutHandle >= 0 && crasherSlot >= 0;
Ipc::Object* outObj = nullptr;
Ipc::HandleType outType = Ipc::HandleType::None;
uint32_t outRights = 0;
Ipc::HandleSnapshot outSnapshot;
if (inheritRedir) {
if (!Ipc::SnapshotHandleForSlot(crasherSlot, crasher->ioOutHandle,
outType, outObj, outRights) ||
outType != Ipc::HandleType::Stream || outObj == nullptr) {
if (!outSnapshot.Capture(crasherSlot, crasher->ioOutHandle) ||
outSnapshot.type != Ipc::HandleType::Stream || outSnapshot.object == nullptr) {
inheritRedir = false;
}
}
@@ -1630,7 +1963,7 @@ namespace Sched {
// so it outlives the crasher's imminent ExitProcess teardown. The
// snapshot above and this install run back-to-back with no blocking
// call between, while the crasher's handle still pins the stream.
int h = Ipc::InstallHandleForSlot(childSlot, outObj, Ipc::HandleType::Stream,
int h = Ipc::InstallHandleForSlot(childSlot, outSnapshot.object, Ipc::HandleType::Stream,
Ipc::RightWrite | Ipc::RightWait | Ipc::RightDup);
if (h >= 0) {
child->ioOutHandle = h;
@@ -1647,3 +1980,9 @@ namespace Sched {
}
}
// Called by the syscall entry assembly after the syscall implementation has
// released all of its subsystem locks but before returning to ring 3.
extern "C" void SchedExitIfKilled() {
Sched::ExitIfKilled();
}