The Pedigree Project 0.1
ThreadedIrqDispatcher.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 "pedigree/kernel/ActivityDiagnostics.h"
9#include "pedigree/kernel/Log.h"
10#include "pedigree/kernel/machine/IrqManager.h"
11#include "pedigree/kernel/machine/ThreadedIrqDispatcher.h"
12#include "pedigree/kernel/process/PerProcessorScheduler.h"
13#include "pedigree/kernel/process/Scheduler.h"
14#include "pedigree/kernel/process/TerminationDeferral.h"
15#include "pedigree/kernel/process/Thread.h"
16#include "pedigree/kernel/processor/Processor.h"
17#include "pedigree/kernel/processor/ProcessorInformation.h"
18#include "pedigree/kernel/time/Time.h"
19
20static_assert(__atomic_always_lock_free(sizeof(size_t), nullptr),
21 "IRQ doorbell words must be lock-free");
22static_assert(__atomic_always_lock_free(sizeof(uintptr_t), nullptr),
23 "IRQ diagnostic identities must be lock-free");
24static_assert(__atomic_always_lock_free(sizeof(Thread::DebugState), nullptr),
25 "IRQ worker debug state must be lock-free");
26
27namespace {
28size_t elapsedSince(size_t now, size_t then) {
29 return then && now >= then ? now - then : 0;
30}
31
32void updateMaximum(size_t& maximum, size_t value) {
33 if (value > __atomic_load_n(&maximum, __ATOMIC_RELAXED)) {
34 // Each physical line has exactly one worker updating its maxima.
35 __atomic_store_n(&maximum, value, __ATOMIC_RELEASE);
36 }
37}
38
39IrqWorkerDebugState workerDebugState(Thread::DebugState state) {
40 switch (state) {
41 case Thread::None:
42 return IrqWorkerDebugState::None;
43 case Thread::SemWait:
44 return IrqWorkerDebugState::SemaphoreWait;
45 case Thread::CondWait:
46 return IrqWorkerDebugState::ConditionWait;
47 case Thread::Joining:
48 return IrqWorkerDebugState::Joining;
49 case Thread::FutexWait:
50 return IrqWorkerDebugState::FutexWait;
51 case Thread::EventWait:
52 return IrqWorkerDebugState::EventWait;
53 case Thread::ProcessWait:
54 return IrqWorkerDebugState::ProcessWait;
55 case Thread::CallbackDrain:
56 return IrqWorkerDebugState::CallbackDrain;
57 }
58 return IrqWorkerDebugState::Unavailable;
59}
60
61IrqWorkerWaitReason workerWaitReason(WaitQueue::WakeReason reason) {
62 switch (reason) {
63 case WaitQueue::WakeReason::Waiting:
64 return IrqWorkerWaitReason::Waiting;
65 case WaitQueue::WakeReason::Signalled:
66 return IrqWorkerWaitReason::Signalled;
67 case WaitQueue::WakeReason::Event:
68 return IrqWorkerWaitReason::Event;
69 case WaitQueue::WakeReason::Unwinding:
70 return IrqWorkerWaitReason::Unwinding;
71 case WaitQueue::WakeReason::Terminating:
72 return IrqWorkerWaitReason::Terminating;
73 case WaitQueue::WakeReason::Spurious:
74 return IrqWorkerWaitReason::Spurious;
75 }
76 return IrqWorkerWaitReason::Unavailable;
77}
78} // namespace
79
80ThreadedIrqDispatcher::Line::Line()
81 : m_Owner(nullptr),
82 m_Callback(nullptr),
83 m_CallbackContext(nullptr),
84 m_Thread(nullptr),
85 m_Scheduler(nullptr),
86 m_WorkerWaiters(),
87 m_WorkerWake(),
88 m_WorkerProcessor(0),
89 m_Line(0),
90 m_PendingCookies(nullptr),
91 m_PendingCookieCount(0),
92 m_ActiveCookie(0),
93 m_CallbackActive(0),
94 m_PublicationState(PublicationClosed),
95 m_Started(0),
96 m_CompletedBatches(0),
97 m_CompletedCookie(0),
98 m_PendingSinceTimestamp(0),
99 m_ActiveCallbackStartedTimestamp(0),
100 m_LastWakeLatency(0),
101 m_MaximumWakeLatency(0),
102 m_LastCallbackRuntime(0),
103 m_MaximumCallbackRuntime(0) {}
104
105ThreadedIrqDispatcher::Line::~Line() {
106 if (__atomic_load_n(&m_Started, __ATOMIC_ACQUIRE) ||
107 __atomic_load_n(&m_Thread, __ATOMIC_ACQUIRE) || m_PendingCookies) {
108 FATAL("A threaded IRQ worker was destroyed while active.");
109 }
110}
111
112void ThreadedIrqDispatcher::Line::configure(ThreadedIrqDispatcher* owner, uint8_t line,
113 DispatchCallback callback, void* callbackContext) {
114 m_Owner = owner;
115 m_Line = line;
116 m_Callback = callback;
117 m_CallbackContext = callbackContext;
118}
119
120bool ThreadedIrqDispatcher::Line::start(size_t processor) {
121#if THREADS
122 if (__atomic_load_n(&m_Started, __ATOMIC_ACQUIRE)) {
123 return m_WorkerProcessor == processor && !publicationClosed();
124 }
125 if (__atomic_load_n(&m_Thread, __ATOMIC_ACQUIRE) || m_PendingCookies || !m_Owner || !m_Callback ||
126 !Scheduler::onlineAffinity().contains(processor)) {
127 return false;
128 }
129
130 size_t pendingCookieCount = Processor::getCount();
131#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
132 const size_t testPendingCookieCount =
133 __atomic_load_n(&m_Owner->m_PendingSlotCountForTest, __ATOMIC_ACQUIRE);
134 if (testPendingCookieCount) {
135 pendingCookieCount = testPendingCookieCount;
136 }
137#endif
138 if (!pendingCookieCount) {
139 return false;
140 }
141 size_t* pendingCookies = new size_t[pendingCookieCount];
142 for (size_t i = 0; i < pendingCookieCount; ++i) {
143 __atomic_store_n(&pendingCookies[i], static_cast<size_t>(0), __ATOMIC_RELAXED);
144 }
145 m_PendingCookies = pendingCookies;
146 m_PendingCookieCount = pendingCookieCount;
147 __atomic_store_n(&m_ActiveCookie, static_cast<size_t>(0), __ATOMIC_RELEASE);
148 __atomic_store_n(&m_CallbackActive, static_cast<size_t>(0), __ATOMIC_RELEASE);
149 __atomic_store_n(&m_PublicationState, static_cast<size_t>(0), __ATOMIC_RELEASE);
150 __atomic_store_n(&m_CompletedBatches, static_cast<size_t>(0), __ATOMIC_RELEASE);
151 __atomic_store_n(&m_CompletedCookie, static_cast<size_t>(0), __ATOMIC_RELEASE);
152 __atomic_store_n(&m_PendingSinceTimestamp, static_cast<size_t>(0), __ATOMIC_RELEASE);
153 __atomic_store_n(&m_ActiveCallbackStartedTimestamp, static_cast<size_t>(0), __ATOMIC_RELEASE);
154 __atomic_store_n(&m_LastWakeLatency, static_cast<size_t>(0), __ATOMIC_RELEASE);
155 __atomic_store_n(&m_MaximumWakeLatency, static_cast<size_t>(0), __ATOMIC_RELEASE);
156 __atomic_store_n(&m_LastCallbackRuntime, static_cast<size_t>(0), __ATOMIC_RELEASE);
157 __atomic_store_n(&m_MaximumCallbackRuntime, static_cast<size_t>(0), __ATOMIC_RELEASE);
158
159 ThreadPlacement placement;
160 placement.allowed.set(processor);
161 Thread* thread = new Thread(Scheduler::instance().getKernelProcess(), workerEntry, this, nullptr,
162 false, true, true, &placement);
163 m_Scheduler = thread->getScheduler();
164 m_WorkerProcessor = m_Scheduler->logicalCpu();
165 assert(m_WorkerProcessor == processor);
166 __atomic_store_n(&m_Thread, thread, __ATOMIC_RELEASE);
167 const String workerName(static_cast<const char*>(m_Owner->m_Name), m_Owner->m_Name.length());
168 thread->setName(workerName);
169 m_Scheduler->registerWorkerWake(m_WorkerWake, m_WorkerWaiters);
170
171 __atomic_store_n(&m_Started, static_cast<size_t>(1), __ATOMIC_RELEASE);
172 if (!thread->start()) {
173 // This can only fail if a freshly-created delayed Thread has already
174 // entered an impossible lifecycle state. Continuing would strand a
175 // registered kernel Thread which cannot be safely reclaimed here.
176 FATAL("A threaded IRQ worker could not be started.");
177 return false;
178 }
179 return true;
180#else
181 return false;
182#endif
183}
184
185void ThreadedIrqDispatcher::Line::beginStop() {
186 if (!__atomic_load_n(&m_Started, __ATOMIC_ACQUIRE)) {
187 return;
188 }
189 // One atomic word closes admission and counts publishers already inside
190 // publishFromInterrupt(). The worker does not exit until that count drains.
191 __atomic_fetch_or(&m_PublicationState, PublicationClosed, __ATOMIC_ACQ_REL);
192 m_Scheduler->ringIrqWorkDoorbell(m_WorkerWake);
193}
194
195bool ThreadedIrqDispatcher::Line::join() {
196 if (!__atomic_load_n(&m_Started, __ATOMIC_ACQUIRE)) {
197 return __atomic_load_n(&m_Thread, __ATOMIC_ACQUIRE) == nullptr;
198 }
199
200 Thread* thread = __atomic_load_n(&m_Thread, __ATOMIC_ACQUIRE);
201 if (!thread || !thread->joinForCompletion()) {
202 return false;
203 }
204
205 if (m_Scheduler) {
206 m_Scheduler->unregisterWorkerWake(m_WorkerWake);
207 }
208 __atomic_store_n(&m_Thread, static_cast<Thread*>(nullptr), __ATOMIC_RELEASE);
209 m_Scheduler = nullptr;
210 __atomic_store_n(&m_Started, static_cast<size_t>(0), __ATOMIC_RELEASE);
211 delete[] m_PendingCookies;
212 m_PendingCookies = nullptr;
213 m_PendingCookieCount = 0;
214 __atomic_store_n(&m_ActiveCookie, static_cast<size_t>(0), __ATOMIC_RELEASE);
215 __atomic_store_n(&m_CallbackActive, static_cast<size_t>(0), __ATOMIC_RELEASE);
216 return true;
217}
218
219bool ThreadedIrqDispatcher::Line::publishFromInterrupt(size_t cookie) {
220 if (!cookie || !__atomic_load_n(&m_Started, __ATOMIC_ACQUIRE)) {
221 return false;
222 }
223
224 size_t processor = Processor::index();
225#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
226 const size_t processorSlot =
227 __atomic_load_n(&m_Owner->m_PublicationSlotForTest, __ATOMIC_ACQUIRE);
228 if (processorSlot != static_cast<size_t>(-1)) {
229 processor = processorSlot;
230 }
231#endif
232
233 const bool remoteProducer = processor != m_WorkerProcessor;
234 if (remoteProducer && !m_Owner->m_RemoteWakeCallback) {
235#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
236 __atomic_add_fetch(&m_Owner->m_RemotePublicationRejectionsForTest, static_cast<size_t>(1),
237 __ATOMIC_RELAXED);
238#endif
239 // A local-only dispatcher cannot safely leave remote work dependent
240 // on a future unrelated timer tick. Reject before publication so the
241 // caller's fail-closed policy retains the terminal IRQ obligations.
242 return false;
243 }
244
245 const size_t admission =
246 __atomic_fetch_add(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_ACQ_REL);
247 if (admission & PublicationClosed) {
248 __atomic_fetch_sub(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_RELEASE);
249 return false;
250 }
251
252 if (!m_PendingCookies || processor >= m_PendingCookieCount) {
253 __atomic_fetch_sub(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_RELEASE);
254 // Processor topology is fixed before dispatcher initialisation. A
255 // failure here is therefore a lifecycle/configuration rejection, not
256 // a contention fallback which can lose an admitted edge.
257 return false;
258 }
259
260 // Maskable hard interrupts cannot run concurrently on one processor.
261 // Per-processor slots make publication one wait-free exchange. A nested
262 // publication happens after this store and may replace it with a later
263 // generation; the outer publisher never writes again when it resumes.
264 const size_t pending =
265 __atomic_exchange_n(&m_PendingCookies[processor], cookie, __ATOMIC_ACQ_REL);
266 if (!pending) {
267 __atomic_store_n(&m_PendingSinceTimestamp, static_cast<size_t>(Time::getTicks()),
268 __ATOMIC_RELEASE);
269 }
270
271#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
272 PublicationObservedHook hook =
273 __atomic_load_n(&m_Owner->m_PublicationObservedHook, __ATOMIC_ACQUIRE);
274 if (hook) {
275 hook(m_Owner, m_Line, cookie, pending);
276 }
277#endif
278
279 // The worker is pinned to this scheduler. Stage its local doorbell before
280 // issuing a directed prompt so a fast IPI observes the published cookie.
281 // A remote overwrite also prompts the scheduler: the first IPI may have
282 // already returned before this occurrence reached the pending slot.
283 m_Scheduler->ringIrqWorkDoorbell(m_WorkerWake, !remoteProducer || pending != 0);
284 if (remoteProducer && !pending) {
285 if (!m_Owner->m_RemoteWakeCallback(m_Owner->m_RemoteWakeCallbackContext, m_Line,
286 m_WorkerProcessor)) {
287 // The occurrence is already accepted and visible. Rolling it
288 // back would strand the owning controller's acknowledgement or
289 // mask state, so a failed directed wake is terminally explicit.
290 FATAL_NOLOCK("Threaded IRQ remote worker prompt failed after publication.");
291 }
292 }
293
294 __atomic_fetch_sub(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_RELEASE);
295 return true;
296}
297
298bool ThreadedIrqDispatcher::Line::hasPending() const {
299 return pendingCookie() != 0;
300}
301
302bool ThreadedIrqDispatcher::Line::isWorker(const Thread* thread) const {
303 return thread && thread == __atomic_load_n(&m_Thread, __ATOMIC_ACQUIRE);
304}
305
306size_t ThreadedIrqDispatcher::Line::pendingCookie() const {
307 // External diagnostics can race orderly dispatcher shutdown. Share the
308 // publisher admission word so join cannot release the slot array while a
309 // detached scan is in progress.
310 const size_t admission =
311 __atomic_fetch_add(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_ACQ_REL);
312 if (admission & PublicationClosed) {
313 __atomic_fetch_sub(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_RELEASE);
314 return 0;
315 }
316
317#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
318 PendingScanAdmittedHook hook =
319 __atomic_load_n(&m_Owner->m_PendingScanAdmittedHook, __ATOMIC_ACQUIRE);
320 if (hook) {
321 hook(m_Owner, m_Line);
322 }
323#endif
324
325 const size_t pending = pendingCookieForWorker();
326 __atomic_fetch_sub(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_RELEASE);
327 return pending;
328}
329
330size_t ThreadedIrqDispatcher::Line::pendingCookieForWorker() const {
331 size_t newest = 0;
332 for (size_t i = 0; i < m_PendingCookieCount; ++i) {
333 const size_t candidate = __atomic_load_n(&m_PendingCookies[i], __ATOMIC_ACQUIRE);
334 if (candidate && (!newest || generationReached(candidate, newest))) {
335 newest = candidate;
336 }
337 }
338 return newest;
339}
340
341size_t ThreadedIrqDispatcher::Line::activeCookie() const {
342 return __atomic_load_n(&m_ActiveCookie, __ATOMIC_ACQUIRE);
343}
344
345size_t ThreadedIrqDispatcher::Line::completedBatches() const {
346 return __atomic_load_n(&m_CompletedBatches, __ATOMIC_ACQUIRE);
347}
348
349size_t ThreadedIrqDispatcher::Line::completedCookie() const {
350 return __atomic_load_n(&m_CompletedCookie, __ATOMIC_ACQUIRE);
351}
352
353uintptr_t ThreadedIrqDispatcher::Line::workerIdentity() const {
354 return reinterpret_cast<uintptr_t>(__atomic_load_n(&m_Thread, __ATOMIC_ACQUIRE));
355}
356
357bool ThreadedIrqDispatcher::Line::callbackActive() const {
358 return __atomic_load_n(&m_CallbackActive, __ATOMIC_ACQUIRE) != 0;
359}
360
361bool ThreadedIrqDispatcher::Line::publicationClosed() const {
362 return (__atomic_load_n(&m_PublicationState, __ATOMIC_ACQUIRE) & PublicationClosed) != 0;
363}
364
365void ThreadedIrqDispatcher::Line::snapshotDiagnostics(IrqLineDiagnosticSnapshot& snapshot) const {
366 snapshot.workerDiagnosticAvailable = false;
367 snapshot.workerDebugState = IrqWorkerDebugState::Unavailable;
368 snapshot.workerDebugAddress = 0;
369 snapshot.workerWaitActive = false;
370 snapshot.workerWaitQueue = 0;
371 snapshot.workerWaitChannelOwner = 0;
372 snapshot.workerWaitChannelValue = 0;
373 snapshot.workerWaitReason = IrqWorkerWaitReason::Unavailable;
374 snapshot.workerWaitStateLevel = 0;
375 snapshot.workerWaitQueued = false;
376 snapshot.observationTimestamp = static_cast<size_t>(Time::getTicks());
377 snapshot.pendingSinceTimestamp = __atomic_load_n(&m_PendingSinceTimestamp, __ATOMIC_ACQUIRE);
378 snapshot.activeCallbackStartedTimestamp =
379 __atomic_load_n(&m_ActiveCallbackStartedTimestamp, __ATOMIC_ACQUIRE);
380 snapshot.lastWakeLatency = __atomic_load_n(&m_LastWakeLatency, __ATOMIC_ACQUIRE);
381 snapshot.maximumWakeLatency = __atomic_load_n(&m_MaximumWakeLatency, __ATOMIC_ACQUIRE);
382 snapshot.lastCallbackRuntime = __atomic_load_n(&m_LastCallbackRuntime, __ATOMIC_ACQUIRE);
383 snapshot.maximumCallbackRuntime = __atomic_load_n(&m_MaximumCallbackRuntime, __ATOMIC_ACQUIRE);
384
385 // Shutdown closes this shared admission word before joining the worker.
386 // A diagnostic reader admitted here therefore pins m_Thread until its
387 // detached copy is complete without taking a lock which the debugger may
388 // have interrupted another CPU while holding.
389 const size_t admission =
390 __atomic_fetch_add(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_ACQ_REL);
391 if (admission & PublicationClosed) {
392 __atomic_fetch_sub(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_RELEASE);
393 return;
394 }
395
396 Thread* thread = __atomic_load_n(&m_Thread, __ATOMIC_ACQUIRE);
397 if (thread) {
398 snapshot.workerDiagnosticAvailable = true;
399 uintptr_t debugAddress = 0;
400 snapshot.workerDebugState = workerDebugState(thread->getDebugState(debugAddress));
401 snapshot.workerDebugAddress = debugAddress;
402
403 Thread::WaitDebugInfo wait = {};
404 if (thread->getWaitDebugInfo(wait)) {
405 snapshot.workerWaitActive = true;
406 snapshot.workerWaitQueue = reinterpret_cast<uintptr_t>(wait.queue);
407 snapshot.workerWaitChannelOwner = reinterpret_cast<uintptr_t>(wait.channelOwner);
408 snapshot.workerWaitChannelValue = wait.channelValue;
409 snapshot.workerWaitReason = workerWaitReason(wait.reason);
410 snapshot.workerWaitStateLevel = wait.stateLevel;
411 snapshot.workerWaitQueued = wait.queued;
412 }
413 }
414
415 __atomic_fetch_sub(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_RELEASE);
416}
417
418int ThreadedIrqDispatcher::Line::workerEntry(void* context) {
419 return reinterpret_cast<Line*>(context)->run();
420}
421
422int ThreadedIrqDispatcher::Line::run() {
423 // Manager-owned workers are retired only by shutdown(). A terminal
424 // request must not strand a line which still accepts publications.
425 TerminationDeferral workerLifetime;
426 while (true) {
427 // Keep callback state visible for diagnostics while this worker owns a
428 // claimed batch. It is already Running/Ready in the scheduler, so no
429 // scheduler-side predicate is needed to protect this interval.
430 __atomic_store_n(&m_CallbackActive, static_cast<size_t>(1), __ATOMIC_RELEASE);
431 const size_t pendingSince = __atomic_load_n(&m_PendingSinceTimestamp, __ATOMIC_ACQUIRE);
432 const size_t cookie = takePendingCookie();
433 if (cookie) {
434 const size_t completedCookie = __atomic_load_n(&m_CompletedCookie, __ATOMIC_ACQUIRE);
435 if (completedCookie && !generationReached(cookie, completedCookie)) {
436 // A cross-CPU scan can claim an older slot after another
437 // batch has already completed. The delivered high-water
438 // suppresses that stale generation without suppressing
439 // equal-cookie work-bit publications.
440 __atomic_store_n(&m_CallbackActive, static_cast<size_t>(0), __ATOMIC_RELEASE);
442 continue;
443 }
444
445 const size_t started = static_cast<size_t>(Time::getTicks());
446 const size_t wakeLatency = elapsedSince(started, pendingSince);
447 __atomic_store_n(&m_LastWakeLatency, wakeLatency, __ATOMIC_RELEASE);
448 updateMaximum(m_MaximumWakeLatency, wakeLatency);
449 __atomic_store_n(&m_ActiveCallbackStartedTimestamp, started, __ATOMIC_RELEASE);
450 __atomic_store_n(&m_ActiveCookie, cookie, __ATOMIC_RELEASE);
451 m_Callback(m_CallbackContext, m_Line, cookie);
452 const size_t completed = static_cast<size_t>(Time::getTicks());
453 const size_t runtime = elapsedSince(completed, started);
454 ActivityDiagnostics::recordThreadedDispatch(m_Line, runtime);
455 __atomic_store_n(&m_LastCallbackRuntime, runtime, __ATOMIC_RELEASE);
456 updateMaximum(m_MaximumCallbackRuntime, runtime);
457 __atomic_add_fetch(&m_CompletedBatches, static_cast<size_t>(1), __ATOMIC_ACQ_REL);
458 __atomic_store_n(&m_CompletedCookie, cookie, __ATOMIC_RELEASE);
459 __atomic_store_n(&m_ActiveCookie, static_cast<size_t>(0), __ATOMIC_RELEASE);
460 __atomic_store_n(&m_ActiveCallbackStartedTimestamp, static_cast<size_t>(0), __ATOMIC_RELEASE);
461 __atomic_store_n(&m_CallbackActive, static_cast<size_t>(0), __ATOMIC_RELEASE);
462 // A continuously asserted source must not turn its threaded
463 // bottom half into an unbounded softirq loop. One completed batch
464 // is the scheduling budget before ordinary peers get a chance.
466 continue;
467 }
468
469 __atomic_store_n(&m_CallbackActive, static_cast<size_t>(0), __ATOMIC_RELEASE);
470 const size_t publicationState = __atomic_load_n(&m_PublicationState, __ATOMIC_ACQUIRE);
471 if (publicationState & PublicationClosed) {
472 if (publicationState & PublicationCountMask) {
474 continue;
475 }
476
477 // An admitted publisher stores its cookie before dropping the
478 // final count. Recheck after observing zero so close cannot race
479 // the worker past an already-accepted occurrence.
480 if (!hasPendingForWorker()) {
481 break;
482 }
483 continue;
484 }
485
486 // Recheck the publication state while holding the wait queue's lock. A
487 // producer which races this check either finds a waiter to wake or leaves
488 // its published cookie visible for this second check.
489 auto guard = m_WorkerWaiters.acquire();
490 const size_t currentPublicationState = __atomic_load_n(&m_PublicationState, __ATOMIC_ACQUIRE);
491 if (!hasPendingForWorker() && !(currentPublicationState & PublicationClosed)) {
492 const WaitQueue::WakeReason reason =
493 guard.wait(WaitQueue::Channel(), Thread::CondWait, reinterpret_cast<uintptr_t>(this));
494 (void)reason;
495 }
496 }
497
498 return 0;
499}
500
501bool ThreadedIrqDispatcher::Line::hasPendingForWorker() const {
502 return pendingCookieForWorker() != 0;
503}
504
505size_t ThreadedIrqDispatcher::Line::takePendingCookie() {
506 size_t newest = 0;
507 for (size_t i = 0; i < m_PendingCookieCount; ++i) {
508 const size_t candidate =
509 __atomic_exchange_n(&m_PendingCookies[i], static_cast<size_t>(0), __ATOMIC_ACQ_REL);
510 if (candidate && (!newest || generationReached(candidate, newest))) {
511 newest = candidate;
512 }
513 }
514 return newest;
515}
516
517bool ThreadedIrqDispatcher::Line::generationReached(size_t current, size_t target) {
518 // Cookies advance monotonically and no live publication can span half of
519 // the size_t range, so signed modular distance preserves wrap ordering.
520 return static_cast<intptr_t>(current - target) >= 0;
521}
522
523ThreadedIrqDispatcher::ThreadedIrqDispatcher(const String& name, size_t lineCount,
524 DispatchCallback callback, void* callbackContext)
525 : m_Lines(nullptr),
526 m_Name(name),
527 m_LineCount(lineCount),
528 m_Callback(callback),
529 m_CallbackContext(callbackContext),
530 m_ConfigurationLock(false),
531 m_RemoteWakeCallback(nullptr),
532 m_RemoteWakeCallbackContext(nullptr),
534 m_Initialised(false),
535 m_Stopping(0),
537#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
538 ,
539 m_PublicationObservedHook(nullptr),
540 m_PendingScanAdmittedHook(nullptr),
541 m_PendingSlotCountForTest(0),
542 m_PublicationSlotForTest(static_cast<size_t>(-1)),
543 m_RejectNextPublicationForTest(0),
544 m_RemotePublicationRejectionsForTest(0)
545#endif
546{
547 if (m_LineCount > MaxLines) {
548 m_LineCount = MaxLines;
549 }
550}
551
553 void* callbackContext) {
555 if (__atomic_load_n(&m_ConfigurationClosed, __ATOMIC_ACQUIRE)) {
557 return false;
558 }
559
560 // This pair is immutable once any worker begins, so hard publication can
561 // read it without another lock or an allocation-dependent indirection.
562 m_RemoteWakeCallback = callback;
563 m_RemoteWakeCallbackContext = callbackContext;
565 return true;
566}
567
568#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
569void ThreadedIrqDispatcher::setPublicationObservedHookForTest(PublicationObservedHook hook) {
570 __atomic_store_n(&m_PublicationObservedHook, hook, __ATOMIC_RELEASE);
571}
572
573bool ThreadedIrqDispatcher::setPendingSlotCountForTest(size_t slotCount) {
574 if (isInitialised() || !slotCount) {
575 return false;
576 }
577
578 __atomic_store_n(&m_PendingSlotCountForTest, slotCount, __ATOMIC_RELEASE);
579 return true;
580}
581
582bool ThreadedIrqDispatcher::publishFromSlotForTest(uint8_t line, size_t slot, size_t cookie) {
583 if (!isInitialised() || line >= m_LineCount) {
584 return false;
585 }
586
587 size_t expected = static_cast<size_t>(-1);
588 if (!__atomic_compare_exchange_n(&m_PublicationSlotForTest, &expected, slot, false,
589 __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
590 return false;
591 }
592 const bool published = m_Lines[line].publishFromInterrupt(cookie);
593 __atomic_store_n(&m_PublicationSlotForTest, static_cast<size_t>(-1), __ATOMIC_RELEASE);
594 return published;
595}
596
597void ThreadedIrqDispatcher::rejectNextPublicationForTest() {
598 __atomic_store_n(&m_RejectNextPublicationForTest, static_cast<size_t>(1), __ATOMIC_RELEASE);
599}
600
601void ThreadedIrqDispatcher::setPendingScanAdmittedHookForTest(PendingScanAdmittedHook hook) {
602 __atomic_store_n(&m_PendingScanAdmittedHook, hook, __ATOMIC_RELEASE);
603}
604
605#endif
606
607ThreadedIrqDispatcher::~ThreadedIrqDispatcher() {
608 if (__atomic_load_n(&m_Initialised, __ATOMIC_ACQUIRE)) {
609 FATAL("Threaded IRQ dispatcher was destroyed before shutdown.");
610 }
611 delete[] m_Lines;
612}
613
615#if THREADS
616 if (__atomic_load_n(&m_Initialised, __ATOMIC_ACQUIRE) || !m_LineCount || !m_Callback) {
617 return false;
618 }
619
621 size_t configurationExpected = 0;
622 if (!__atomic_compare_exchange_n(&m_ConfigurationClosed, &configurationExpected,
623 static_cast<size_t>(1), false, __ATOMIC_ACQ_REL,
624 __ATOMIC_ACQUIRE)) {
626 return false;
627 }
629
630#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
631 __atomic_store_n(&m_RejectNextPublicationForTest, static_cast<size_t>(0), __ATOMIC_RELEASE);
632 __atomic_store_n(&m_RemotePublicationRejectionsForTest, static_cast<size_t>(0), __ATOMIC_RELEASE);
633#endif
634 __atomic_store_n(&m_ShutdownClaimed, static_cast<size_t>(0), __ATOMIC_RELEASE);
635 __atomic_store_n(&m_Stopping, static_cast<size_t>(0), __ATOMIC_RELEASE);
636
637 if (!m_Lines) {
638 Line* lines = new Line[m_LineCount];
639 for (size_t i = 0; i < m_LineCount; ++i) {
640 lines[i].configure(this, static_cast<uint8_t>(i), m_Callback, m_CallbackContext);
641 }
642 __atomic_store_n(&m_Lines, lines, __ATOMIC_RELEASE);
643 }
644
645 for (size_t i = 0; !lazy && i < m_LineCount; ++i) {
646 if (!m_Lines[i].start(Processor::index())) {
647 for (size_t j = 0; j < i; ++j) {
648 m_Lines[j].beginStop();
649 }
650 Processor::information().getScheduler().serviceIrqWorkDoorbell();
651 for (size_t j = 0; j < i; ++j) {
652 m_Lines[j].join();
653 }
654 __atomic_store_n(&m_ConfigurationClosed, static_cast<size_t>(0), __ATOMIC_RELEASE);
655 return false;
656 }
657 }
658
659 __atomic_store_n(&m_Initialised, static_cast<size_t>(1), __ATOMIC_RELEASE);
660 return true;
661#else
662 return true;
663#endif
664}
665
666bool ThreadedIrqDispatcher::prepareLine(uint8_t line, size_t processor) {
667#if THREADS
668 if (!isInitialised() || line >= m_LineCount || __atomic_load_n(&m_Stopping, __ATOMIC_ACQUIRE) ||
669 Processor::executionContext() != ExecutionContext::WaitableThread) {
670 return false;
671 }
672 return m_Lines[line].start(processor);
673#else
674 return false;
675#endif
676}
677
679#if THREADS
680 if (!__atomic_load_n(&m_Initialised, __ATOMIC_ACQUIRE)) {
681 return true;
682 }
683 if (!canShutdown()) {
684 return false;
685 }
686 size_t expected = 0;
687 if (!__atomic_compare_exchange_n(&m_ShutdownClaimed, &expected, static_cast<size_t>(1), false,
688 __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
689 // A successful claimant owns every join and slot release. A racing
690 // caller must not join the same workers or free the same arrays.
691 return !__atomic_load_n(&m_Initialised, __ATOMIC_ACQUIRE);
692 }
693 __atomic_store_n(&m_Stopping, static_cast<size_t>(1), __ATOMIC_RELEASE);
694
695 for (size_t i = 0; i < m_LineCount; ++i) {
696 m_Lines[i].beginStop();
697 }
698
699 Processor::information().getScheduler().serviceIrqWorkDoorbell();
700
701 bool joined = true;
702 for (size_t i = 0; i < m_LineCount; ++i) {
703 joined &= m_Lines[i].join();
704 }
705 if (joined) {
706 __atomic_store_n(&m_Initialised, static_cast<size_t>(0), __ATOMIC_RELEASE);
707 __atomic_store_n(&m_ConfigurationClosed, static_cast<size_t>(0), __ATOMIC_RELEASE);
708 } else {
709 // Successfully joined lines are already inert. Let a later ordinary
710 // thread retry only the workers which did not complete this drain.
711 __atomic_store_n(&m_ShutdownClaimed, static_cast<size_t>(0), __ATOMIC_RELEASE);
712 }
713 return joined;
714#else
715 return true;
716#endif
717}
718
720 Thread* current = Processor::information().getCurrentThread();
721 bool safe = current && Processor::executionContext() == ExecutionContext::WaitableThread &&
723#if HOSTED
724 safe = safe && !current->getHostedSignalDepth();
725#endif
726 return safe;
727}
728
729bool ThreadedIrqDispatcher::isInitialised() const {
730 return __atomic_load_n(&m_Initialised, __ATOMIC_ACQUIRE) != 0;
731}
732
734#if THREADS
735 const Line* lines = __atomic_load_n(&m_Lines, __ATOMIC_ACQUIRE);
736 if (!lines) {
737 return false;
738 }
739 const Thread* current = Processor::information().getCurrentThread();
740 for (size_t i = 0; i < m_LineCount; ++i) {
741 if (lines[i].isWorker(current)) {
742 return true;
743 }
744 }
745#endif
746 return false;
747}
748
749bool ThreadedIrqDispatcher::publishFromInterrupt(uint8_t line, size_t cookie) {
750 if (!isInitialised() || line >= m_LineCount || !cookie) {
751 return false;
752 }
753#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
754 if (__atomic_exchange_n(&m_RejectNextPublicationForTest, static_cast<size_t>(0),
755 __ATOMIC_ACQ_REL)) {
756 return false;
757 }
758#endif
759 return m_Lines[line].publishFromInterrupt(cookie);
760}
761
762bool ThreadedIrqDispatcher::hasPending(uint8_t line) const {
763 return isInitialised() && line < m_LineCount && m_Lines[line].hasPending();
764}
765
766size_t ThreadedIrqDispatcher::pendingCookie(uint8_t line) const {
767 const Line* lines = __atomic_load_n(&m_Lines, __ATOMIC_ACQUIRE);
768 return lines && line < m_LineCount ? lines[line].pendingCookie() : 0;
769}
770
771size_t ThreadedIrqDispatcher::activeCookie(uint8_t line) const {
772 const Line* lines = __atomic_load_n(&m_Lines, __ATOMIC_ACQUIRE);
773 return lines && line < m_LineCount ? lines[line].activeCookie() : 0;
774}
775
776size_t ThreadedIrqDispatcher::completedBatches(uint8_t line) const {
777 const Line* lines = __atomic_load_n(&m_Lines, __ATOMIC_ACQUIRE);
778 return lines && line < m_LineCount ? lines[line].completedBatches() : 0;
779}
780
781size_t ThreadedIrqDispatcher::completedCookie(uint8_t line) const {
782 const Line* lines = __atomic_load_n(&m_Lines, __ATOMIC_ACQUIRE);
783 return lines && line < m_LineCount ? lines[line].completedCookie() : 0;
784}
785
786uintptr_t ThreadedIrqDispatcher::workerIdentity(uint8_t line) const {
787 const Line* lines = __atomic_load_n(&m_Lines, __ATOMIC_ACQUIRE);
788 return lines && line < m_LineCount ? lines[line].workerIdentity() : 0;
789}
790
791bool ThreadedIrqDispatcher::callbackActive(uint8_t line) const {
792 const Line* lines = __atomic_load_n(&m_Lines, __ATOMIC_ACQUIRE);
793 return lines && line < m_LineCount && lines[line].callbackActive();
794}
795
796bool ThreadedIrqDispatcher::publicationClosed(uint8_t line) const {
797 const Line* lines = __atomic_load_n(&m_Lines, __ATOMIC_ACQUIRE);
798 return line < m_LineCount && (!lines || lines[line].publicationClosed());
799}
800
802 IrqLineDiagnosticSnapshot& snapshot) const {
803 const Line* lines = __atomic_load_n(&m_Lines, __ATOMIC_ACQUIRE);
804 if (lines && line < m_LineCount) {
805 lines[line].snapshotDiagnostics(snapshot);
806 }
807}
static ProcessorInformation & information()
static size_t getCount()
static ExecutionContext executionContext()
Definition Processor.cc:109
static size_t index()
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:236
void release()
Definition Spinlock.cc:168
bool acquire(bool recurse=false, bool safe=true)
Definition Spinlock.cc:36
DebugState
Definition Thread.h:177
bool getWaitDebugInfo(WaitDebugInfo &info)
Definition Thread.cc:3163
bool joinForCompletion()
Definition Thread.cc:2750
DebugState getDebugState(uintptr_t &address)
Definition Thread.h:574
class PerProcessorScheduler * getScheduler() const
Definition Thread.h:929
bool start()
Definition Thread.cc:751
bool hasPending(uint8_t line) const
size_t pendingCookie(uint8_t line) const
bool prepareLine(uint8_t line, size_t processor)
RemoteWakeCallback m_RemoteWakeCallback
bool(*)(void *, uint8_t, size_t) RemoteWakeCallback
bool initialise(bool lazy=false)
bool publishFromInterrupt(uint8_t line, size_t cookie)
MUST_USE_RESULT bool setRemoteWakeCallback(RemoteWakeCallback callback, void *callbackContext)
void snapshotDiagnostics(uint8_t line, IrqLineDiagnosticSnapshot &snapshot) const