Skip to main content

ax_task/sched/system/task_system/
mod.rs

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