Skip to main content

ax_task/sched/system/task_system/
mod.rs

1//! Generation-checked registry and scheduling orchestration.
2
3mod balance;
4mod cancellation;
5mod cpu_lifecycle;
6mod deadline;
7mod deferred_work;
8mod delivery;
9mod dispatch;
10mod exited_work;
11mod lifecycle;
12mod membarrier;
13mod migration;
14mod model;
15mod outcome;
16mod park_exit;
17mod pi;
18#[cfg(axtest)]
19pub use {
20    pi::PiScheduleTestProbeSnapshot, pi::begin_pi_schedule_test_probe,
21    pi::end_pi_schedule_test_probe, pi::pi_schedule_test_probe_snapshot,
22};
23mod placement;
24mod priority_index;
25mod registry;
26mod root_domain;
27mod scheduling;
28mod switch;
29mod thread_api;
30mod thread_callbacks;
31mod thread_creation;
32
33use alloc::{sync::Arc, vec::Vec};
34use core::{pin::Pin, ptr};
35
36use exited_work::ExitedThreadWork;
37pub(crate) use membarrier::{MembarrierCpuTargets, MembarrierTarget};
38use model::{
39    BalanceReason, BalanceTransferOutcome, DeferredTaskWorkClass, DetachedOwnerMessageBatch,
40    FAIR_BALANCE_BALANCED_BACKOFF_FACTOR, FAIR_BALANCE_CONSTRAINED_BACKOFF_FACTOR,
41    FairBalanceResult, FairPolicyPlacement,
42};
43pub use model::{DeferredTaskWorkBatch, OwnedThreadReapError, TaskSystem};
44pub(crate) use outcome::SwitchEndpoint;
45pub use outcome::{
46    ChargeOutcome, DeadlineActivitySnapshot, DeadlineRuntimeSnapshot, OwnerControlDrain,
47    ScheduleDecision, SchedulerOutcome, SwitchInCompletion, YieldOutcome,
48};
49pub(crate) use park_exit::CurrentExitPermit;
50use priority_index::RootDomainPriorityIndex;
51use registry::{
52    CpuRegistration, DeadlineCallbackClaim, DetachedThreadRecord, TaskSystemState, ThreadRecord,
53    ThreadSlot,
54};
55use root_domain::{DeadlineBandwidthRebuild, RootDomain, RootDomainPushClass, RootDomainState};
56use thread_callbacks::ThreadCallbackState;
57
58use super::thread_sched::{
59    PiScheduleUpdate, ThreadDeadlineInit, ThreadPlacementInit, ThreadPolicyInit, ThreadRuntimeInit,
60    ThreadSchedCell, ThreadSchedInit, ThreadSchedState,
61};
62use crate::{
63    executor::CoroutineHeader,
64    runtime::{
65        RuntimeStatus,
66        config::TaskSystemConfig,
67        cpu::{CpuLocal, CpuRemote, CpuRemoteHandle, CpuSnapshot, RuntimeCpuId},
68        delivery::{
69            inbox::{InboxKind, InboxMessage, InboxOperation, PublishResult, SchedulerInbox},
70            work::{TaskWorkConsumerGuard, TaskWorkDoorbell},
71        },
72        lock::{IrqScope, IrqTicketLock, PreemptTicketLock},
73        resource::{
74            AddressSpaceDestroyOutcome, AddressSpaceMembarrierId, AddressSpaceMembarrierState,
75            AddressSpaceReclaimArmOutcome, MembarrierRegistration, MembarrierRegistrationPhase,
76            ThreadResources,
77        },
78        switch::{ContextThreadBinding, CurrentThreadPublication, CurrentThreadRef},
79        sync::{PiMutexRaw, PiWaitToken},
80        task_runtime,
81    },
82    sched::{
83        CpuId, CpuSet, DeadlineBandwidthSnapshot, FairMode, SchedulePolicy, SchedulerTimestamp,
84        SchedulingClass, ThreadAffinityChange,
85        algorithm::{
86            ActiveSchedulingState, DeadlineAdmission, EnqueueReason, PickedThread, QueuedThread,
87            QueuedThreadSnapshot, RqTaskMetadata, SchedulingEntity,
88        },
89        system::{
90            CpuRemotePublication, SchedulerDeadlineDerivationSource, SchedulerPolicyRef,
91            cpu::{
92                CpuRunQueueState, CurrentDispatch, DeadlineBaseGuardSource, EqualRtWakeAction,
93                HardTimerServiceClaim, HardTimerServiceStep, IdlePullReservation,
94                KtimerServiceClaim, OwnerRqEntry, OwnerRqTxn, PreparedMigrationDelivery,
95                RescheduleKind, RunQueueClockSnapshot, RunQueueDomainPublication,
96                RunQueueGuardSource, SchedulerDeadlineRqObservation, SchedulerRequestScope,
97                SchedulerThreadRef, SoftTimerExpireBatch, WakePreemptionDecision,
98            },
99        },
100    },
101    thread::{
102        DEADLINE_CLASS_RANK, DeadlineEntity, DeadlineServer, OwnedThreadSchedulerExit, ParkCommit,
103        ParkPrepare, ParkTicket, PiDonation, PiWaitKey, PiWaitRegistration, REALTIME_CLASS_RANK,
104        SchedulingUrgency, SwitchReason, TaskError, ThreadCore, ThreadCoreInit, ThreadExtension,
105        ThreadExtensionBorrow, ThreadExtensionLease, ThreadExtensionView, ThreadHandle, ThreadId,
106        ThreadRuntimeSnapshot, ThreadSpec, ThreadState, ThreadWakeHandle, WaitWakeClaim,
107        WaitWakeDelivery, WakeIntent, WakeResult,
108    },
109    time::{
110        MonotonicDeadline, MonotonicInstant,
111        queue::{
112            ExpiredTaskDeadline, KernelTimerExecution, TaskDeadlineArmPlan, TaskDeadlineError,
113            TaskDeadlineKind, TaskDeadlineNode, TaskDeadlineQueue, TaskDeadlineRegistration,
114        },
115    },
116};
117
118struct UnpublishedThreadGuard<'system> {
119    system: &'system TaskSystem,
120    spec: Option<ThreadSpec>,
121}
122
123fn apply_pi_schedule_update(
124    sched: &mut ThreadSchedState,
125    mut active: ActiveSchedulingState,
126    update: PiScheduleUpdate,
127    owner_now_ns: u64,
128    fair_placement: Option<FairPolicyPlacement>,
129) -> Result<ActiveSchedulingState, TaskError> {
130    if update.generation != sched.policy.dispatch_generation {
131        return Err(TaskError::InvalidPiState);
132    }
133
134    let PiScheduleUpdate {
135        policy,
136        donor,
137        deadline_donor,
138        deadline_donor_core,
139        deadline_donor_server,
140        generation: _,
141    } = update;
142    let old_donor = sched.pi.donor;
143    let old_deadline_donor = sched.pi.deadline_donor;
144    let base = sched.policy.base;
145    let old_uses_inherited = active.uses_inherited_entity();
146    let next_uses_inherited = donor.is_some() && !pi_reuses_base_entity(base, policy);
147    if old_uses_inherited && !next_uses_inherited {
148        active.use_base_entity(base);
149    }
150
151    let source_changed = old_donor != donor || old_deadline_donor != deadline_donor;
152    let donor_server = deadline_donor_server;
153    match (donor, policy) {
154        (None, base_policy) if base_policy == base => {
155            active.use_base_entity(base_policy);
156        }
157        (Some(_), SchedulePolicy::Deadline(_)) => {
158            if !next_uses_inherited {
159                return Err(TaskError::InvalidPiState);
160            }
161            if !old_uses_inherited || source_changed {
162                active.use_inherited_entity(
163                    policy,
164                    SchedulingEntity::Deadline(crate::thread::DeadlineEntity::from_donor_server(
165                        sched.deadline.server.clone(),
166                        donor_server.ok_or(TaskError::InvalidPiState)?,
167                    )),
168                );
169            } else {
170                active.update_inherited_effective_policy(policy);
171            }
172            let SchedulingEntity::Deadline(deadline) = active.entity() else {
173                return Err(TaskError::InvalidPiState);
174            };
175            deadline.replenish_for_pi(owner_now_ns);
176        }
177        (Some(_), SchedulePolicy::Fifo { .. }) => {
178            if next_uses_inherited {
179                active.use_inherited_entity(policy, SchedulingEntity::Fifo);
180            } else if !matches!(active.base_entity(), SchedulingEntity::Fifo) {
181                return Err(TaskError::InvalidPiState);
182            } else {
183                active.use_base_entity_with_effective_policy(policy);
184            }
185        }
186        (Some(_), SchedulePolicy::RoundRobin { quantum_ns, .. }) => {
187            if next_uses_inherited {
188                return Err(TaskError::InvalidPiState);
189            }
190            if !matches!(active.base_entity(), SchedulingEntity::RoundRobin { .. }) {
191                return Err(TaskError::InvalidPiState);
192            }
193            if old_donor.is_none()
194                && let SchedulingEntity::RoundRobin {
195                    remaining_quantum_ns,
196                } = active.base_entity_mut()
197                && *remaining_quantum_ns > quantum_ns
198            {
199                *remaining_quantum_ns = quantum_ns;
200            }
201            active.use_base_entity_with_effective_policy(policy);
202        }
203        (Some(_), SchedulePolicy::Fair { nice, mode }) => {
204            if next_uses_inherited {
205                return Err(TaskError::InvalidPiState);
206            }
207            let SchedulingEntity::Fair(fair) = *active.base_entity() else {
208                return Err(TaskError::InvalidPiState);
209            };
210            let placement = fair_placement.ok_or(TaskError::InvalidPiState)?;
211            active.replace_base_entity(SchedulingEntity::Fair(fair.reconfigure(
212                nice,
213                mode,
214                placement.source_virtual_time,
215                placement.destination_virtual_time,
216            )));
217            active.use_base_entity_with_effective_policy(policy);
218        }
219        _ => return Err(TaskError::InvalidPiState),
220    }
221
222    sched.pi.donor = donor;
223    sched.pi.deadline_donor = deadline_donor;
224    sched.pi.deadline_donor_core = deadline_donor_core;
225    Ok(active)
226}
227
228fn pi_reuses_base_entity(base: SchedulePolicy, effective: SchedulePolicy) -> bool {
229    matches!(
230        (base, effective),
231        (
232            SchedulePolicy::Fifo { .. } | SchedulePolicy::RoundRobin { .. },
233            SchedulePolicy::Fifo { .. } | SchedulePolicy::RoundRobin { .. }
234        ) | (SchedulePolicy::Fair { .. }, SchedulePolicy::Fair { .. })
235    )
236}
237
238struct OwnerNext {
239    core: SchedulerThreadRef,
240    policy: SchedulerPolicyRef,
241    urgency: SchedulingUrgency,
242}
243
244/// Scheduler-clock consequence of one committed owner selection.
245///
246/// Linux updates hrtick and class timers only when the selected classes or
247/// their deadline-bearing state changed. A plain FIFO-to-FIFO rotation has no
248/// per-task clockevent, so it carries an explicit unchanged result instead of
249/// re-deriving every shared deadline after each context switch.
250enum OwnerSchedulerDeadline {
251    Unchanged,
252    Reevaluate(SchedulerDeadlineRqObservation),
253}
254
255impl<'system> UnpublishedThreadGuard<'system> {
256    fn new(system: &'system TaskSystem, spec: ThreadSpec) -> Self {
257        Self {
258            system,
259            spec: Some(spec),
260        }
261    }
262
263    fn into_owned_parts(mut self) -> (Option<ThreadExtension>, ThreadResources) {
264        self.spec
265            .take()
266            .expect("unpublished thread transaction must still own its specification")
267            .into_owned_parts()
268    }
269
270    fn into_spec(mut self) -> ThreadSpec {
271        self.spec
272            .take()
273            .expect("unpublished thread transaction must still own its specification")
274    }
275
276    fn spec(&self) -> &ThreadSpec {
277        self.spec
278            .as_ref()
279            .expect("unpublished thread transaction must still own its specification")
280    }
281}
282
283impl Drop for UnpublishedThreadGuard<'_> {
284    fn drop(&mut self) {
285        if let Some(spec) = self.spec.take() {
286            let (extension, resources) = spec.into_owned_parts();
287            self.system
288                .release_unpublished_thread(DetachedThreadRecord::new(resources, extension));
289        }
290    }
291}
292
293impl TaskSystem {
294    /// Creates an empty scheduler instance for a fixed topology.
295    ///
296    /// # Errors
297    ///
298    /// Returns [`TaskError::InvalidCpuCount`] for an empty or unrepresentable
299    /// topology and [`TaskError::InvalidConfiguration`] for inconsistent fixed
300    /// capacities or bandwidth values.
301    pub fn new(config: TaskSystemConfig) -> Result<Self, TaskError> {
302        Self::create(config, |_| 1024)
303    }
304
305    /// Creates a scheduler with immutable firmware capacities in logical CPU order.
306    ///
307    /// Capacities use Linux's 1024 scale and may be zero after normalization.
308    /// They affect initial and explicit-affinity Fair placement, not frequency scaling,
309    /// wake affinity, RT/DL admission, or periodic balancing. Missing firmware
310    /// data must be resolved by the platform before calling this constructor.
311    ///
312    /// # Errors
313    ///
314    /// In addition to [`Self::new`] errors, rejects a topology length mismatch
315    /// or capacities above 1024 before publishing any scheduler state.
316    pub fn new_with_cpu_capacities(
317        config: TaskSystemConfig,
318        capacities: &[u16],
319    ) -> Result<Self, TaskError> {
320        if capacities.len() != config.cpu_count() || capacities.iter().any(|&c| c > 1024) {
321            return Err(TaskError::InvalidConfiguration);
322        }
323        Self::create(config, |index| capacities[index])
324    }
325
326    fn create(
327        config: TaskSystemConfig,
328        capacity: impl Fn(usize) -> u16,
329    ) -> Result<Self, TaskError> {
330        validate_config(config)?;
331        let task_work = Arc::new(TaskWorkDoorbell::new());
332        let cpu_remotes = (0..config.cpu_count())
333            .map(|index| CpuRemote::create(CpuId::new(index as u32), config, capacity(index)))
334            .collect::<Result<Vec<_>, _>>()?;
335        let cpu_registrations = cpu_remotes
336            .iter()
337            .cloned()
338            .map(|remote| CpuRegistration { remote })
339            .collect();
340        let root_domain = RootDomain::new(config, cpu_remotes.clone());
341        Ok(Self {
342            config,
343            cpu_remotes,
344            state: PreemptTicketLock::new(TaskSystemState {
345                cpus: cpu_registrations,
346                slots: crate::thread::allocation::try_vec(config.thread_capacity())?,
347                free_slots: crate::thread::allocation::try_vec(config.thread_capacity())?,
348                pending_address_space_reclaims: crate::thread::allocation::try_vec(
349                    config.thread_capacity().max(config.cpu_count()),
350                )?,
351                task_work_class_cursor: DeferredTaskWorkClass::Deadline,
352                address_space_reclaim_first: true,
353                exited_work: ExitedThreadWork::new(config.thread_capacity())?,
354            }),
355            root_domain,
356            deferred_coroutine_reclaims: SchedulerInbox::new(InboxKind::Reclaim),
357            deferred_thread_cancellations: SchedulerInbox::new(InboxKind::Reclaim),
358            deferred_deadline_callbacks: SchedulerInbox::new(InboxKind::TaskWork),
359            deferred_scheduler_ticks: SchedulerInbox::new(InboxKind::TaskWork),
360            task_work,
361        })
362    }
363}
364
365fn validate_config(config: TaskSystemConfig) -> Result<(), TaskError> {
366    if config.cpu_count() == 0 || config.cpu_count() > u32::MAX as usize {
367        return Err(TaskError::InvalidCpuCount(config.cpu_count()));
368    }
369    if config.deadline_cap_percent() == 0
370        || config.deadline_cap_percent() > 100
371        || config.rt_period_ns() == 0
372        || config.rt_runtime_ns() > config.rt_period_ns()
373        || config.balance_interval_ns() == 0
374        || config.thread_capacity() == 0
375        || config.thread_capacity() > u32::MAX as usize
376        || config.batch_limit() == 0
377        || config.batch_limit() > crate::runtime::config::DEFAULT_BATCH_LIMIT
378        || config.pi_chain_limit() == 0
379    {
380        return Err(TaskError::InvalidConfiguration);
381    }
382    Ok(())
383}
384
385fn deadline_zero_lag(deadline: &DeadlineEntity) -> SchedulerTimestamp {
386    let policy = deadline.policy();
387    let lag_ns = deadline.remaining_runtime_ns() as u128 * policy.period_ns() as u128
388        / policy.runtime_ns() as u128;
389    let lag_ns = u64::try_from(lag_ns)
390        .expect("Deadline zero-lag interval cannot exceed one scheduler period");
391    SchedulerTimestamp::from_nanos(
392        deadline
393            .absolute_deadline_ns()
394            .expect("an active Deadline entity must own a zero-lag anchor"),
395    )
396    .retreat(lag_ns)
397}
398
399fn ensure_runtime_success(status: RuntimeStatus) -> Result<(), TaskError> {
400    if status == RuntimeStatus::Success {
401        Ok(())
402    } else {
403        Err(TaskError::RuntimeFailure(status as u32))
404    }
405}
406
407fn validate_affinity(affinity: &CpuSet, cpu_count: usize) -> Result<(), TaskError> {
408    if affinity.topology_len() == cpu_count {
409        Ok(())
410    } else {
411        Err(TaskError::InvalidConfiguration)
412    }
413}
414
415// The top bit of the generation-bearing identity is reserved for compact
416// scheduler-adjacent owner words such as the Linux-style PI mutex waiters bit.
417// Exhausting a slot retires it instead of wrapping and reintroducing ABA.
418const MAX_THREAD_GENERATION: u32 = i32::MAX as u32;
419
420const fn next_generation(generation: u32) -> u32 {
421    if generation < MAX_THREAD_GENERATION {
422        generation + 1
423    } else {
424        generation
425    }
426}
427
428fn advance_thread_slot_generation(slot: &mut ThreadSlot) -> bool {
429    assert_eq!(
430        slot.pending_deadline_reservation, 0,
431        "a reusable thread slot must not retain Deadline admission"
432    );
433    let next = next_generation(slot.generation);
434    if next == slot.generation {
435        // The empty slot remains in the registry so every stale identity still
436        // resolves to `record == None`, but it is never returned to free_slots:
437        // wrapping would make an older generation-bearing ThreadId valid again.
438        false
439    } else {
440        slot.generation = next;
441        true
442    }
443}