ax_task/thread/spec.rs
1//! Thread construction data kept independent from an operating system.
2
3use alloc::{sync::Arc, vec, vec::Vec};
4
5use crate::{
6 runtime::{
7 resource::{
8 AddressSpaceHandle, AddressSpaceToken, ExecutionContextHandle, StackHandle, TlsHandle,
9 },
10 service::{
11 SchedulerTickCpuTime, SchedulerTickGate, SchedulerTickTaskWork,
12 SchedulerTickWorkDisposition,
13 },
14 task_runtime,
15 },
16 sched::{CpuId, SchedulePolicy},
17 thread::{SchedulerTickWork, TaskError, ThreadHandle, ThreadId},
18};
19
20/// Runtime-owned resources whose lifetime follows one thread.
21#[repr(C)]
22#[derive(Debug, Eq, PartialEq)]
23pub struct ThreadResources {
24 context: ExecutionContextHandle,
25 stack: StackHandle,
26 tls: TlsHandle,
27 address_space: AddressSpaceToken,
28}
29
30impl ThreadResources {
31 /// Empty resources for pure scheduler models.
32 pub const NONE: Self = Self {
33 context: ExecutionContextHandle::NONE,
34 stack: StackHandle::NONE,
35 tls: TlsHandle::NONE,
36 address_space: AddressSpaceToken::NONE,
37 };
38
39 /// Creates a complete runtime resource bundle from uniquely owned handles.
40 ///
41 /// # Safety
42 ///
43 /// Every non-empty handle must be live, belong to the currently installed
44 /// [`crate::runtime::TaskRuntime`], and have its unique destruction right
45 /// transferred into this bundle. The caller must not construct another
46 /// owning bundle from the same scalar handles.
47 pub const unsafe fn new(
48 context: ExecutionContextHandle,
49 stack: StackHandle,
50 tls: TlsHandle,
51 address_space: AddressSpaceToken,
52 ) -> Self {
53 Self {
54 context,
55 stack,
56 tls,
57 address_space,
58 }
59 }
60
61 /// Returns the execution context.
62 pub const fn context(&self) -> ExecutionContextHandle {
63 self.context
64 }
65 /// Returns the guarded stack allocation.
66 pub const fn stack(&self) -> StackHandle {
67 self.stack
68 }
69 /// Returns the TLS allocation.
70 pub const fn tls(&self) -> TlsHandle {
71 self.tls
72 }
73 /// Returns the address-space handle.
74 pub const fn address_space(&self) -> AddressSpaceHandle {
75 self.address_space.handle()
76 }
77
78 pub(crate) fn replace_address_space(
79 &mut self,
80 address_space: AddressSpaceToken,
81 ) -> AddressSpaceToken {
82 core::mem::replace(&mut self.address_space, address_space)
83 }
84
85 pub(crate) fn take_address_space(&mut self) -> AddressSpaceToken {
86 core::mem::replace(&mut self.address_space, AddressSpaceToken::NONE)
87 }
88
89 /// Releases thread-private resources and returns the independent active-mm
90 /// ownership token.
91 ///
92 /// The registry calls this only after switch tail has cleared physical CPU
93 /// ownership. Context, TLS, and stack destruction are consequently
94 /// one-way operations with no retry state. The address-space token has a
95 /// separate active-CPU lifetime and is handed to that reclaim protocol
96 /// instead of retaining already-dead thread resources.
97 pub(crate) fn release(mut self) -> AddressSpaceToken {
98 if !self.context.is_none() {
99 task_runtime::destroy_context(self.context);
100 self.context = ExecutionContextHandle::NONE;
101 }
102
103 if !self.tls.is_none() {
104 task_runtime::deallocate_tls(self.tls);
105 self.tls = TlsHandle::NONE;
106 }
107
108 if !self.stack.is_none() {
109 task_runtime::deallocate_stack(self.stack);
110 self.stack = StackHandle::NONE;
111 }
112
113 core::mem::replace(&mut self.address_space, AddressSpaceToken::NONE)
114 }
115}
116
117/// Why a running thread relinquished its execution context.
118///
119/// The value crosses the OS extension callback boundary, so its numeric layout
120/// is stable and may also be written directly to allocation-free trace records.
121#[repr(u32)]
122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
123pub enum SwitchReason {
124 /// A scheduler request selected a more urgent or otherwise eligible thread.
125 Preempted = 1,
126 /// The thread voluntarily yielded its current service position.
127 Yield = 2,
128 /// The thread committed a park or another blocking operation.
129 Blocked = 3,
130 /// The thread terminated and will never become runnable again.
131 Exited = 4,
132 /// CPU affinity or balancing moved the thread away from this CPU.
133 Migrated = 5,
134}
135
136/// CPU affinity expressed against one [`crate::runtime::TaskSystem`] topology.
137#[derive(Clone, Debug, Eq, PartialEq)]
138pub struct CpuSet {
139 words: Vec<usize>,
140 topology_len: usize,
141 // Mirrors Linux task_struct::nr_cpus_allowed so scheduler class decisions
142 // do not repeatedly derive affinity cardinality from the mask.
143 allowed_count: usize,
144}
145
146impl CpuSet {
147 const BITS_PER_WORD: usize = usize::BITS as usize;
148
149 /// Creates a set that permits every CPU in a topology.
150 pub fn all(cpu_count: usize) -> Self {
151 let mut words = vec![usize::MAX; cpu_count.div_ceil(Self::BITS_PER_WORD)];
152 if let Some(last) = words.last_mut()
153 && !cpu_count.is_multiple_of(Self::BITS_PER_WORD)
154 {
155 *last = (1usize << (cpu_count % Self::BITS_PER_WORD)) - 1;
156 }
157 Self {
158 words,
159 topology_len: cpu_count,
160 allowed_count: cpu_count,
161 }
162 }
163
164 /// Creates an empty CPU set for a topology.
165 pub fn empty(cpu_count: usize) -> Self {
166 Self {
167 words: vec![0; cpu_count.div_ceil(Self::BITS_PER_WORD)],
168 topology_len: cpu_count,
169 allowed_count: 0,
170 }
171 }
172
173 /// Enables one CPU if it is represented by this set.
174 pub fn insert(&mut self, cpu: CpuId) -> bool {
175 let index = cpu.as_usize();
176 if index >= self.topology_len {
177 return false;
178 }
179 let mask = 1usize << (index % Self::BITS_PER_WORD);
180 let word = &mut self.words[index / Self::BITS_PER_WORD];
181 let changed = *word & mask == 0;
182 *word |= mask;
183 if changed {
184 self.allowed_count += 1;
185 }
186 changed
187 }
188
189 /// Disables one CPU if it is represented by this set.
190 pub fn remove(&mut self, cpu: CpuId) -> bool {
191 let index = cpu.as_usize();
192 if index >= self.topology_len {
193 return false;
194 }
195 let mask = 1usize << (index % Self::BITS_PER_WORD);
196 let word = &mut self.words[index / Self::BITS_PER_WORD];
197 let changed = *word & mask != 0;
198 *word &= !mask;
199 if changed {
200 self.allowed_count -= 1;
201 }
202 changed
203 }
204
205 pub(crate) fn clear(&mut self) {
206 self.words.fill(0);
207 self.allowed_count = 0;
208 }
209
210 /// Tests whether a CPU is allowed.
211 pub fn contains(&self, cpu: CpuId) -> bool {
212 let index = cpu.as_usize();
213 index < self.topology_len
214 && self.words[index / Self::BITS_PER_WORD] & (1usize << (index % Self::BITS_PER_WORD))
215 != 0
216 }
217
218 /// Returns the number of CPUs represented by the set.
219 pub fn topology_len(&self) -> usize {
220 self.topology_len
221 }
222
223 /// Returns the number of CPUs selected by this set.
224 pub(crate) fn count(&self) -> usize {
225 self.allowed_count
226 }
227
228 /// Iterates selected CPUs in ascending logical-ID order.
229 pub fn iter(&self) -> impl Iterator<Item = CpuId> + '_ {
230 (0..self.topology_len)
231 .map(|index| CpuId::new(index as u32))
232 .filter(|cpu| self.contains(*cpu))
233 }
234
235 /// Returns the only allowed CPU when migration is impossible.
236 pub(crate) fn sole_cpu(&self) -> Option<CpuId> {
237 if self.allowed_count != 1 {
238 return None;
239 }
240 let (word_index, word) = self
241 .words
242 .iter()
243 .copied()
244 .enumerate()
245 .find(|(_, word)| *word != 0)?;
246 let index = word_index * Self::BITS_PER_WORD + word.trailing_zeros() as usize;
247 (index < self.topology_len).then_some(CpuId::new(index as u32))
248 }
249
250 /// Returns whether a runnable thread can leave its current allowed CPU.
251 pub(crate) fn is_migration_capable(&self) -> bool {
252 self.allowed_count > 1
253 }
254
255 /// Returns whether this set permits every CPU selected by `required`.
256 pub fn covers(&self, required: &Self) -> bool {
257 self.topology_len == required.topology_len
258 && self
259 .words
260 .iter()
261 .zip(&required.words)
262 .all(|(allowed, is_required)| allowed & is_required == *is_required)
263 }
264
265 pub(crate) fn copy_from_set(&mut self, source: &Self) -> Result<(), TaskError> {
266 if self.topology_len != source.topology_len {
267 return Err(TaskError::InvalidConfiguration);
268 }
269 self.words.copy_from_slice(&source.words);
270 self.allowed_count = source.allowed_count;
271 Ok(())
272 }
273
274 /// Returns the first CPU in the intersection that satisfies `accepts`.
275 ///
276 /// This is the `cpumask_any_and()` primitive used by cpupri/cpudl: the
277 /// intersection is formed a machine word at a time rather than scanning
278 /// every logical CPU.
279 pub(crate) fn first_intersection(
280 &self,
281 other: &Self,
282 mut accepts: impl FnMut(CpuId) -> bool,
283 ) -> Option<CpuId> {
284 if self.topology_len != other.topology_len {
285 return None;
286 }
287 for (word_index, (left, right)) in self.words.iter().zip(&other.words).enumerate() {
288 let mut candidates = left & right;
289 while candidates != 0 {
290 let bit = candidates.trailing_zeros() as usize;
291 candidates &= candidates - 1;
292 let index = word_index * Self::BITS_PER_WORD + bit;
293 if index >= self.topology_len {
294 break;
295 }
296 let cpu = CpuId::new(index as u32);
297 if accepts(cpu) {
298 return Some(cpu);
299 }
300 }
301 }
302 None
303 }
304
305 pub(crate) fn word(&self, word_index: usize) -> usize {
306 self.words.get(word_index).copied().unwrap_or(0)
307 }
308}
309
310/// OS-owned callbacks attached to a thread without exposing OS types.
311#[repr(C)]
312#[derive(Debug)]
313pub struct ThreadExtensionOps {
314 /// Invoked after the incoming thread becomes current. The runtime value
315 /// is the rq-charged total before its new execution interval, allowing OS
316 /// accounting to use the switch boundary without querying the registry.
317 pub on_switch_in: unsafe extern "Rust" fn(
318 data: usize,
319 thread: ThreadId,
320 policy: SchedulePolicy,
321 charged_runtime_ns: u64,
322 ),
323 /// Invoked after the thread stops being the current execution context.
324 pub on_switch_out: unsafe extern "Rust" fn(data: usize, thread: ThreadId, reason: SwitchReason),
325 /// Invoked in task context after the thread exits.
326 pub on_exit: unsafe extern "Rust" fn(data: usize, thread: ThreadId),
327 /// Invoked in task context for requested Deadline overrun notification.
328 pub on_deadline_overrun: unsafe extern "Rust" fn(data: usize, thread: ThreadId),
329 /// Releases the OS-owned extension data in task or reaper context.
330 pub drop: unsafe extern "Rust" fn(data: usize),
331}
332
333/// Bounded OS hook invoked when the owner changes a running thread's base policy.
334pub type RunningPolicyAppliedHook = unsafe extern "Rust" fn(
335 data: usize,
336 thread: ThreadId,
337 base_policy: SchedulePolicy,
338 observed_ns: u64,
339);
340
341/// Opaque OS-specific data attached to a thread.
342#[derive(Debug)]
343pub struct ThreadExtension {
344 data: usize,
345 ops: &'static ThreadExtensionOps,
346 running_policy_applied_hook: Option<RunningPolicyAppliedHook>,
347 scheduler_tick_cpu_time: Option<Arc<SchedulerTickCpuTime>>,
348 scheduler_tick_work: Option<SchedulerTickWork>,
349}
350
351impl ThreadExtension {
352 /// Creates an extension from opaque data and a static callback table.
353 ///
354 /// # Safety
355 ///
356 /// `data` must satisfy every callback contract in `ops`, and the owning OS
357 /// must ensure callbacks do not allocate, block, or re-enter the scheduler
358 /// when invoked as switch hooks. Task-context callbacks must return to the
359 /// dedicated service thread; abandoning that stack leaves their explicit
360 /// in-flight lifetime claim closed to prevent use-after-free.
361 pub const unsafe fn new(data: usize, ops: &'static ThreadExtensionOps) -> Self {
362 Self {
363 data,
364 ops,
365 running_policy_applied_hook: None,
366 scheduler_tick_cpu_time: None,
367 scheduler_tick_work: None,
368 }
369 }
370
371 /// Attaches IRQ-safe user/system CPU-time sampling to this thread.
372 ///
373 /// The scheduler retains the capability and charges it directly from each
374 /// periodic tick. No OS callback or deferred task work runs in hard IRQ.
375 pub fn with_scheduler_tick_cpu_time(mut self, accounting: Arc<SchedulerTickCpuTime>) -> Self {
376 self.scheduler_tick_cpu_time = Some(accounting);
377 self
378 }
379
380 /// Adds a bounded callback for base-policy changes applied to a running thread.
381 ///
382 /// The callback runs after the scheduler releases the thread-state lock.
383 /// The current CPU still owns the scheduler baton, so the callback is
384 /// serialized with switch hooks for the same thread. Queued and inactive
385 /// base-policy changes are observed through the policy snapshot passed to
386 /// the next switch-in instead. PI donation does not change this value.
387 ///
388 /// # Safety
389 ///
390 /// `callback` must interpret `data` according to this extension, remain
391 /// valid for its complete lifetime, and perform only bounded operations.
392 /// It must not allocate, block, or re-enter the scheduler.
393 pub unsafe fn with_running_policy_applied_hook(
394 mut self,
395 callback: RunningPolicyAppliedHook,
396 ) -> Self {
397 self.running_policy_applied_hook = Some(callback);
398 self
399 }
400
401 /// Adds task-context work gated by scheduler tick interest.
402 ///
403 /// The scheduler hard-IRQ path only publishes a typed deferred-work record.
404 /// The callback runs later on the dedicated task-work service thread.
405 ///
406 /// # Safety
407 ///
408 /// `callback` must interpret `data` according to this extension, remain
409 /// valid for its complete lifetime, and return normally to the task-work
410 /// service. The callback may use task-context synchronization but must not
411 /// retain the borrowed extension data after it returns. It may return
412 /// [`SchedulerTickWorkDisposition::Retry`] only after a transient conflict
413 /// and before publishing any accounting, timer, or signal state.
414 pub unsafe fn with_scheduler_tick_work(
415 mut self,
416 gate: Arc<SchedulerTickGate>,
417 callback: SchedulerTickTaskWork,
418 ) -> Self {
419 self.scheduler_tick_work = Some(SchedulerTickWork::new(gate, callback));
420 self
421 }
422
423 /// Returns the opaque OS-owned value.
424 pub const fn data(&self) -> usize {
425 self.data
426 }
427
428 /// Returns the callback table used as the extension type identity.
429 pub const fn ops(&self) -> &'static ThreadExtensionOps {
430 self.ops
431 }
432
433 /// Returns the callback used to observe running-thread base-policy changes.
434 pub const fn running_policy_applied_hook(&self) -> Option<RunningPolicyAppliedHook> {
435 self.running_policy_applied_hook
436 }
437
438 /// Forwards a running-thread base-policy change to this extension.
439 ///
440 /// Returns `false` when this extension did not register such a hook.
441 ///
442 /// # Safety
443 ///
444 /// The caller must retain this extension, invoke the callback only after
445 /// scheduler metadata locks are released, and preserve the hook's bounded,
446 /// non-blocking context contract.
447 pub unsafe fn forward_running_policy_applied(
448 &self,
449 thread: ThreadId,
450 base_policy: SchedulePolicy,
451 observed_ns: u64,
452 ) -> bool {
453 let Some(callback) = self.running_policy_applied_hook else {
454 return false;
455 };
456 unsafe { callback(self.data, thread, base_policy, observed_ns) };
457 true
458 }
459
460 /// Clones the gate used to select scheduler-tick task work.
461 ///
462 /// Runtime extension composition uses this to install the same interest
463 /// generation on an outer scheduler-owned extension.
464 pub fn scheduler_tick_work_gate(&self) -> Option<Arc<SchedulerTickGate>> {
465 self.scheduler_tick_work
466 .as_ref()
467 .map(SchedulerTickWork::gate)
468 }
469
470 /// Clones the IRQ-safe CPU-time sampling capability.
471 ///
472 /// Runtime extension composition uses this to preserve the inner OS
473 /// capability on the outer scheduler-owned extension.
474 pub fn scheduler_tick_cpu_time(&self) -> Option<Arc<SchedulerTickCpuTime>> {
475 self.scheduler_tick_cpu_time.as_ref().map(Arc::clone)
476 }
477
478 /// Forwards one scheduler-tick task-work callback to this extension.
479 ///
480 /// Returns `None` when this extension did not register such work.
481 ///
482 /// # Safety
483 ///
484 /// The caller must own an ordinary task-context publication authorized by
485 /// the gate returned from [`Self::scheduler_tick_work_gate`], keep this
486 /// extension alive for the call, and invoke it at most once for that
487 /// publication. A forwarded [`SchedulerTickWorkDisposition::Retry`] keeps
488 /// the same no-partial-publication contract as the original callback.
489 pub unsafe fn forward_scheduler_tick_work(
490 &self,
491 thread: ThreadId,
492 observed_ns: u64,
493 ) -> Option<SchedulerTickWorkDisposition> {
494 let work = self.scheduler_tick_work.as_ref()?;
495 Some(unsafe { work.invoke(self.data, thread, observed_ns) })
496 }
497
498 pub(crate) const fn as_view(&self) -> ThreadExtensionView {
499 ThreadExtensionView {
500 data: self.data,
501 ops: self.ops,
502 running_policy_applied_hook: self.running_policy_applied_hook,
503 }
504 }
505
506 pub(crate) fn scheduler_tick_work(&self) -> Option<SchedulerTickWork> {
507 self.scheduler_tick_work.clone()
508 }
509}
510
511impl Drop for ThreadExtension {
512 fn drop(&mut self) {
513 // SAFETY: construction transfers the unique callback-data destruction
514 // right into this non-cloneable owner.
515 unsafe { (self.ops.drop)(self.data) };
516 }
517}
518
519/// Copy-only borrowed identity for an installed OS extension.
520#[derive(Clone, Copy, Debug)]
521pub struct ThreadExtensionView {
522 data: usize,
523 ops: &'static ThreadExtensionOps,
524 running_policy_applied_hook: Option<RunningPolicyAppliedHook>,
525}
526
527/// Extension identity borrowed for exactly as long as a strong thread handle.
528///
529/// This wrapper deliberately does not expose its copyable internal view. The
530/// strong handle borrowed by the wrapper prevents the registry reaper from
531/// destroying the extension while its opaque data is being inspected.
532#[derive(Debug)]
533pub struct ThreadExtensionBorrow<'thread> {
534 view: ThreadExtensionView,
535 _thread: &'thread ThreadHandle,
536}
537
538impl<'thread> ThreadExtensionBorrow<'thread> {
539 pub(crate) const fn new(view: ThreadExtensionView, thread: &'thread ThreadHandle) -> Self {
540 Self {
541 view,
542 _thread: thread,
543 }
544 }
545
546 /// Returns the borrowed opaque data value.
547 pub const fn data(&self) -> usize {
548 self.view.data()
549 }
550
551 /// Returns the callback table used as the extension type identity.
552 pub const fn ops(&self) -> &'static ThreadExtensionOps {
553 self.view.ops()
554 }
555}
556
557/// Owned extension lease used when the caller has no pre-existing handle.
558///
559/// Keeping this value alive pins both the thread header and the registry record,
560/// so current-thread helpers cannot return data that becomes stale immediately
561/// after their temporary lookup handle is dropped.
562#[derive(Debug)]
563pub struct ThreadExtensionLease {
564 view: ThreadExtensionView,
565 thread: ThreadHandle,
566}
567
568impl ThreadExtensionLease {
569 pub(crate) const fn new(view: ThreadExtensionView, thread: ThreadHandle) -> Self {
570 Self { view, thread }
571 }
572
573 /// Returns the generation-bearing identity pinned by this lease.
574 pub fn thread_id(&self) -> ThreadId {
575 self.thread.id()
576 }
577
578 /// Returns the leased opaque data value.
579 pub const fn data(&self) -> usize {
580 self.view.data()
581 }
582
583 /// Returns the callback table used as the extension type identity.
584 pub const fn ops(&self) -> &'static ThreadExtensionOps {
585 self.view.ops()
586 }
587
588 /// Releases the strong lookup lease while retaining the extension view.
589 ///
590 /// Fresh thread-entry trampolines need this operation before invoking an
591 /// entry point that terminates through a non-unwinding scheduler switch.
592 /// Otherwise the suspended stack permanently pins the exited thread.
593 ///
594 /// # Safety
595 ///
596 /// The caller must be the running thread identified by [`Self::thread_id`].
597 /// Its registry record must remain live until every use of the returned
598 /// view completes. The consumed lookup lease and its pinned thread header
599 /// must not be accessed again, and the returned view must not escape past
600 /// thread exit.
601 pub unsafe fn release_for_current_thread_entry(self) -> ThreadExtensionView {
602 let view = self.view;
603 drop(self);
604 view
605 }
606}
607
608impl ThreadExtensionView {
609 /// Returns the borrowed opaque data value.
610 pub const fn data(self) -> usize {
611 self.data
612 }
613
614 /// Returns the callback table used as the extension type identity.
615 pub const fn ops(self) -> &'static ThreadExtensionOps {
616 self.ops
617 }
618
619 pub(crate) unsafe fn notify_running_policy_applied(
620 self,
621 thread: ThreadId,
622 base_policy: SchedulePolicy,
623 observed_ns: u64,
624 ) {
625 if let Some(callback) = self.running_policy_applied_hook {
626 unsafe { callback(self.data, thread, base_policy, observed_ns) };
627 }
628 }
629}
630
631/// Validated inputs used to create a scheduler thread record.
632#[derive(Debug)]
633pub struct ThreadSpec {
634 policy: SchedulePolicy,
635 affinity: Option<CpuSet>,
636 // Runtime resources must be dropped before the extension that owns their
637 // address-space and entry metadata, including on fallback destruction.
638 resources: ThreadResources,
639 extension: Option<ThreadExtension>,
640}
641
642impl ThreadSpec {
643 /// Creates a thread specification with full topology affinity.
644 pub const fn new(policy: SchedulePolicy) -> Self {
645 Self {
646 policy,
647 affinity: None,
648 resources: ThreadResources::NONE,
649 extension: None,
650 }
651 }
652
653 /// Restricts the thread to an explicit CPU set.
654 pub fn with_affinity(mut self, affinity: CpuSet) -> Self {
655 self.affinity = Some(affinity);
656 self
657 }
658
659 /// Attaches OS-specific state.
660 pub fn with_extension(mut self, extension: ThreadExtension) -> Self {
661 self.extension = Some(extension);
662 self
663 }
664
665 /// Associates a complete runtime resource bundle with the thread.
666 ///
667 /// # Safety
668 ///
669 /// `resources` must satisfy [`ThreadResources::new`] and must be consumed by
670 /// exactly this specification and its eventual scheduler record.
671 pub unsafe fn with_resources(mut self, resources: ThreadResources) -> Self {
672 self.resources = resources;
673 self
674 }
675
676 /// Returns the base scheduling policy.
677 pub const fn policy(&self) -> SchedulePolicy {
678 self.policy
679 }
680
681 /// Returns explicit affinity, if one was supplied.
682 pub fn affinity(&self) -> Option<&CpuSet> {
683 self.affinity.as_ref()
684 }
685
686 pub(crate) fn into_owned_parts(mut self) -> (Option<ThreadExtension>, ThreadResources) {
687 let extension = self.extension.take();
688 let resources = core::mem::replace(&mut self.resources, ThreadResources::NONE);
689 (extension, resources)
690 }
691}