The Pedigree Project 0.1
Thread-affinity.cc
1/*
2 * Copyright (c) 2026, Pedigree Developers
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted.
6 */
7
8#include <config.h>
9
10#if THREADS
11
12#include "pedigree/kernel/LockGuard.h"
13#include "pedigree/kernel/Log.h"
14#include "pedigree/kernel/process/PerProcessorScheduler.h"
15#include "pedigree/kernel/process/Process.h"
16#include "pedigree/kernel/process/Scheduler.h"
17#include "pedigree/kernel/process/SchedulingAlgorithm.h"
18#include "pedigree/kernel/process/TerminationDeferral.h"
19#include "pedigree/kernel/process/Thread.h"
20#include "pedigree/kernel/process/Uninterruptible.h"
21#include "pedigree/kernel/processor/Processor.h"
22#include "pedigree/kernel/processor/ProcessorInformation.h"
23
24#if X86_COMMON && MULTIPROCESSOR
25#include <machine/mach_pc/LocalApic.h>
26#include <machine/mach_pc/Pc.h>
27#endif
28
29#if PEDIGREE_AFFINITY_TESTS
30namespace {
31Thread* g_AffinityCommitTarget = nullptr;
32Thread::AffinityCommitHook g_AffinityCommitHook = nullptr;
33} // namespace
34
35void Thread::setAffinityCommitHookForTest(Thread* target, AffinityCommitHook hook) {
36 __atomic_store_n(&g_AffinityCommitTarget, target, __ATOMIC_RELEASE);
37 __atomic_store_n(&g_AffinityCommitHook, hook, __ATOMIC_RELEASE);
38}
39#endif
40
41ThreadPlacement ThreadPlacement::initialUser() {
42 ThreadPlacement placement;
43 placement.allowed = Scheduler::onlineAffinity();
44 placement.migratable = true;
45 return placement;
46}
47
48ThreadPlacement ThreadPlacement::inherit(Thread& creator) {
49 ThreadPlacement placement;
50 creator.snapshotPlacement(placement);
51 return placement.migratable ? placement : initialUser();
52}
53
54void Thread::initialisePlacement(const ThreadPlacement* placement) {
55 if (placement) {
56 m_Placement = *placement;
57 m_Placement.allowed.intersect(Scheduler::onlineAffinity());
58 if (m_Placement.allowed.empty())
59 FATAL("Thread construction has no allowed online processor.");
60 }
61}
62
63void Thread::snapshotPlacement(ThreadPlacement& placement) {
65 snapshotPlacementLocked(placement);
66}
67
69 placement = m_Placement;
70}
71
75 if (!owner || m_bShutdown || m_SignalFramesRequired || getUnwindState() != Continue ||
76 m_LegacyUserCallbackPins == ~size_t(0) ||
77 !m_Placement.allowed.contains(owner->logicalCpu()) ||
78 (m_AffinityPending && !m_RequestedAffinity.contains(owner->logicalCpu())))
79 return false;
80 ++m_LegacyUserCallbackPins;
81 return true;
82}
83
84void Thread::unpinLegacyUserCallbacks() {
86 assert(m_LegacyUserCallbackPins);
87 --m_LegacyUserCallbackPins;
88}
89
90AffinityResult Thread::requestAffinity(const CpuAffinityMask& requested, uint64_t& generation) {
91 CpuAffinityMask effective = requested;
92 effective.intersect(Scheduler::onlineAffinity());
93 generation = 0;
94 if (effective.empty())
95 return AffinityResult::Invalid;
96
97 PerProcessorScheduler* owner = nullptr;
98 Process* parent = m_pParent;
99 bool rejected = false;
100 bool parentPinned = false;
101 bool threadPinned = false;
102 {
103 auto progress = m_AffinityWaiters.acquire();
105 if (m_bShutdown || getUnwindState() != Continue || m_Status == AwaitingJoin ||
106 m_Status == Zombie)
107 return AffinityResult::Terminal;
108 if (!m_Placement.migratable)
109 return AffinityResult::Pinned;
110 if (m_AffinityPending) {
111 generation = m_AffinityGeneration;
112 return AffinityResult::Busy;
113 }
114 owner = getScheduler();
115 assert(owner);
116 if (m_LegacyUserCallbackPins && !effective.contains(owner->logicalCpu()))
117 return AffinityResult::Unsupported;
118 if (m_AffinityGeneration == ~uint64_t(0))
119 return AffinityResult::Invalid;
120
121 if (!m_AffinityWorkQueued) {
122 if (!parent->beginExternalLease())
123 return AffinityResult::Terminal;
124 parentPinned = true;
125 threadPinned = beginExternalLease();
126 rejected = !threadPinned;
127 }
128 if (!rejected) {
129 generation = ++m_AffinityGeneration;
130 m_RequestedAffinity = effective;
131 m_AffinityPending = true;
132 __atomic_store_n(&m_AffinityReturnPending, static_cast<size_t>(1), __ATOMIC_RELEASE);
133 if (!m_AffinityWorkQueued) {
134 m_AffinityWorkQueued = true;
135 if (!owner->enqueueAffinity(this)) {
136 m_AffinityWorkQueued = false;
137 m_AffinityPending = false;
138 __atomic_store_n(&m_AffinityReturnPending, static_cast<size_t>(0), __ATOMIC_RELEASE);
139 generation = 0;
140 rejected = true;
141 }
142 }
143 }
144 }
145 if (rejected) {
146 if (threadPinned)
148 if (parentPinned)
149 parent->endExternalLease();
150 return AffinityResult::Terminal;
151 }
152 owner->prompt();
153 return AffinityResult::Success;
154}
155
156AffinityResult Thread::waitAffinity(uint64_t generation) {
157 if (!generation)
158 return AffinityResult::Invalid;
159 TerminationDeferral lifetime;
160 while (true) {
161 auto progress = m_AffinityWaiters.acquire();
162 {
164 if (generation > m_AffinityGeneration)
165 return AffinityResult::Invalid;
166 if (generation <= m_AffinityCompleted)
167 return AffinityResult::Success;
168 if (!m_AffinityPending)
169 return AffinityResult::Terminal;
170 }
171 const auto reason = progress.waitForCompletion(WaitQueue::Channel(this), Thread::ProcessWait,
172 reinterpret_cast<uintptr_t>(this));
173 (void)reason;
174 }
175}
176
177AffinityResult Thread::completeAffinityAtSafePoint(bool* waited) {
178 if (!affinityWorkPending()) {
180 if (waited)
181 *waited = false;
182 return AffinityResult::Success;
183 }
185 if (waited)
186 *waited = false;
187 if (Processor::information().getCurrentThread() != this)
188 FATAL("Affinity gate entered for a non-current Thread.");
189
190 // Only this audited wait continuation may move. Suppress callbacks across
191 // its entire lifetime, including preemption before waiter publication.
192 Uninterruptible gateScope;
193 while (true) {
194 bool rejected = false;
195 bool parentPinned = false;
196 bool threadPinned = false;
197 bool finished = false;
198 AffinityResult result = AffinityResult::Success;
199 Process* parent = m_pParent;
200 PerProcessorScheduler* owner = nullptr;
201 {
202 auto progress = m_AffinityWaiters.acquire();
203 {
205 if (!m_AffinityGatePending) {
206 owner = getScheduler();
207 assert(owner);
208 if (m_bShutdown || getUnwindState() != Continue) {
209 result = AffinityResult::Terminal;
210 finished = true;
211 } else if (m_Placement.allowed.contains(owner->logicalCpu())) {
212 finished = true;
213 }
214 if (!finished)
215 assert(!m_LegacyUserCallbackPins);
216 if (!finished && !m_AffinityWorkQueued) {
217 parentPinned = parent->beginExternalLease();
218 if (parentPinned)
219 threadPinned = beginExternalLease();
220 rejected = !threadPinned;
221 }
222 if (!finished && !rejected) {
223 m_AffinityGatePending = true;
224 __atomic_store_n(&m_AffinityReturnPending, static_cast<size_t>(1), __ATOMIC_RELEASE);
225 if (!m_AffinityWorkQueued) {
226 m_AffinityWorkQueued = true;
227 if (!owner->enqueueAffinity(this)) {
228 m_AffinityWorkQueued = false;
229 m_AffinityGatePending = false;
230 rejected = true;
231 }
232 }
233 }
234 } else {
235 owner = getScheduler();
236 }
237 }
238 if (!finished && !rejected) {
239 owner->prompt();
240 // The guard prevents completion from racing enrollment. Terminal
241 // wakeups cannot abandon an admitted node; the source worker cancels
242 // it and owns the final lease release.
243 if (waited)
244 *waited = true;
245 const auto reason =
246 m_AffinityWaiters.wait(progress, nullptr, WaitQueue::Channel(this), ProcessWait,
247 reinterpret_cast<uintptr_t>(this), true, false);
248 (void)reason;
249 }
250 }
251 if (finished) {
253 return result;
254 }
255 if (rejected) {
256 if (threadPinned)
258 if (parentPinned)
259 parent->endExternalLease();
261 return AffinityResult::Terminal;
262 }
264 }
265}
266
267void Thread::publishReadyNotification() {
269 assert(m_ReadyPublicationPending);
270 __atomic_store_n(&m_ReadyPublicationPending, false, __ATOMIC_RELEASE);
271 if (m_Status == Ready) {
273 assert(owner);
274 owner->m_pSchedulingAlgorithm->threadStatusChanged(this);
275 }
276}
277
278bool PerProcessorScheduler::enqueueAffinity(Thread* thread, bool accepted) {
279 LockGuard<Spinlock> guard(m_AffinityQueueLock);
280 if (!m_AffinityAdmissionOpen && !accepted)
281 return false;
282 assert(!thread->m_AffinityNext);
283 if (m_AffinityTail)
284 m_AffinityTail->m_AffinityNext = thread;
285 else
286 m_AffinityHead = thread;
287 m_AffinityTail = thread;
288 m_AffinityRequests += 1;
289 ringIrqWorkDoorbell(m_TimeAccountingWorkerWake);
290 return true;
291}
292
293void PerProcessorScheduler::prompt() {
295#if X86_COMMON && MULTIPROCESSOR
296 if (this != &Processor::information().getScheduler()) {
297 ProcessorInformation* information = Processor::informationAt(m_LogicalCpu);
298 assert(information);
299 const uint8_t apicId = information->localApicId();
300 // Failed prompts leave accepted work visible to the periodic tick.
301 const bool submitted = Pc::instance().getLocalApic().interProcessorInterrupt(
302 apicId, IPI_RESCHEDULE_VECTOR, LocalApic::deliveryModeFixed, true, false);
303 (void)submitted;
304 }
305#endif
306}
307
308void PerProcessorScheduler::drainAffinityRequests() {
309 assert(this == &Processor::information().getScheduler());
310 // Both delayed and immediate constructors may still be waiting for their
311 // add worker to install the first context. Let that worker make progress.
312 const size_t batch = m_AffinityRequests.value();
313 for (size_t i = 0; i < batch; ++i) {
314 Thread* thread = nullptr;
315 {
316 LockGuard<Spinlock> guard(m_AffinityQueueLock);
317 thread = m_AffinityHead;
318 if (!thread)
319 break;
320 m_AffinityHead = thread->m_AffinityNext;
321 if (!m_AffinityHead)
322 m_AffinityTail = nullptr;
323 thread->m_AffinityNext = nullptr;
324 }
325#if PEDIGREE_AFFINITY_TESTS
326 const auto hook = __atomic_load_n(&g_AffinityCommitHook, __ATOMIC_ACQUIRE);
327 if (hook && thread == __atomic_load_n(&g_AffinityCommitTarget, __ATOMIC_ACQUIRE))
328 hook(thread);
329#endif
330 bool retry = false;
331 PerProcessorScheduler* destination = this;
332 {
333 auto progress = thread->m_AffinityWaiters.acquire();
334 {
335 LockGuard<Spinlock> guard(thread->m_Lock);
336 assert(thread->m_AffinityWorkQueued && thread->getScheduler() == this);
337 assert(thread != Processor::information().getCurrentThread());
338 const bool terminal = m_StopTimeAccountingWorker.value() || thread->m_bShutdown ||
339 thread->getUnwindState() != Thread::Continue ||
340 thread->m_Status == Thread::AwaitingJoin ||
341 thread->m_Status == Thread::Zombie;
342 if (!terminal && !thread->m_HasSchedulerContext) {
343 retry = true;
344 } else {
345 if (!terminal) {
346 assert(thread->m_Status != Thread::Running);
347 if (thread->m_AffinityPending) {
348 assert(!thread->m_LegacyUserCallbackPins ||
349 thread->m_RequestedAffinity.contains(m_LogicalCpu));
350 thread->m_Placement.allowed = thread->m_RequestedAffinity;
351 thread->m_AffinityCompleted = thread->m_AffinityGeneration;
352 }
353 if (thread->m_AffinityGatePending) {
354 if (!thread->m_Placement.allowed.contains(m_LogicalCpu)) {
355 assert(!thread->m_LegacyUserCallbackPins);
356 for (size_t cpu = 0; cpu < CpuAffinityMask::MaximumCpus; ++cpu) {
357 if (thread->m_Placement.allowed.contains(cpu)) {
358 destination = Scheduler::schedulerForCpu(cpu);
359 break;
360 }
361 }
362 }
363 assert(destination);
365 Scheduler::instance().rebindThread(thread, *destination);
366 destination->m_pSchedulingAlgorithm->threadStatusChanged(thread);
367 }
368 }
369 thread->m_AffinityPending = false;
370 thread->m_AffinityGatePending = false;
371 thread->m_AffinityWorkQueued = false;
372 __atomic_store_n(&thread->m_AffinityReturnPending, static_cast<size_t>(0),
373 __ATOMIC_RELEASE);
374 }
375 }
376 if (!retry)
377 progress.wakeAll(WaitQueue::WakeReason::Signalled, WaitQueue::Channel(thread));
378 }
379 m_AffinityRequests -= 1;
380 if (retry) {
381 const bool queued = enqueueAffinity(thread, true);
382 assert(queued);
383 } else {
384 destination->prompt();
385 // The node owns a lease independently of the syscall's caller. The
386 // final release may destroy a terminal detached target.
387 Process* parent = thread->m_pParent;
388 thread->endExternalLease();
389 parent->endExternalLease();
390 }
391 }
392}
393
394#endif
SchedulingAlgorithm * m_pSchedulingAlgorithm
void endExternalLease()
Definition Process.cc:1087
bool beginExternalLease()
Definition Process.cc:1077
static ProcessorInformation & information()
static ProcessorInformation * informationAt(size_t cpu)
Definition Processor.cc:39
static void setInterrupts(bool bEnable)
static Scheduler & instance()
Definition Scheduler.h:96
virtual void removeThread(Thread *pThread)=0
virtual void threadStatusChanged(Thread *pThread)=0
@ Continue
No unwind necessary, carry on as normal.
Definition Thread.h:513
bool tryPinLegacyUserCallbacks()
volatile Status m_Status
Definition Thread.h:1303
AffinityResult waitAffinity(uint64_t generation)
bool m_bShutdown
Definition Thread.h:1332
Process * m_pParent
Definition Thread.h:1176
void snapshotPlacementLocked(ThreadPlacement &placement) const
UnwindType getUnwindState()
Definition Thread.h:531
void endExternalLease()
Definition Thread.cc:2883
Spinlock m_Lock
Definition Thread.h:1257
class PerProcessorScheduler * getScheduler() const
Definition Thread.h:925
AffinityResult requestAffinity(const CpuAffinityMask &mask, uint64_t &generation)
bool beginExternalLease()
Definition Thread.cc:2873
AffinityResult completeAffinityAtSafePoint(bool *waited=nullptr)