Skip to main content

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}