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        validate_config(config)?;
303        let task_work = Arc::new(TaskWorkDoorbell::new());
304        let cpu_remotes = (0..config.cpu_count())
305            .map(|index| CpuRemote::create(CpuId::new(index as u32), config))
306            .collect::<Result<Vec<_>, _>>()?;
307        let cpu_registrations = cpu_remotes
308            .iter()
309            .cloned()
310            .map(|remote| CpuRegistration { remote })
311            .collect();
312        let root_domain = RootDomain::new(config, cpu_remotes.clone());
313        Ok(Self {
314            config,
315            cpu_remotes,
316            state: PreemptTicketLock::new(TaskSystemState {
317                cpus: cpu_registrations,
318                slots: crate::thread::allocation::try_vec(config.thread_capacity())?,
319                free_slots: crate::thread::allocation::try_vec(config.thread_capacity())?,
320                pending_address_space_reclaims: crate::thread::allocation::try_vec(
321                    config.thread_capacity().max(config.cpu_count()),
322                )?,
323                task_work_class_cursor: DeferredTaskWorkClass::Deadline,
324                address_space_reclaim_first: true,
325                exited_work: ExitedThreadWork::new(config.thread_capacity())?,
326            }),
327            root_domain,
328            deferred_coroutine_reclaims: SchedulerInbox::new(InboxKind::Reclaim),
329            deferred_thread_cancellations: SchedulerInbox::new(InboxKind::Reclaim),
330            deferred_deadline_callbacks: SchedulerInbox::new(InboxKind::TaskWork),
331            deferred_scheduler_ticks: SchedulerInbox::new(InboxKind::TaskWork),
332            task_work,
333        })
334    }
335}
336
337fn validate_config(config: TaskSystemConfig) -> Result<(), TaskError> {
338    if config.cpu_count() == 0 || config.cpu_count() > u32::MAX as usize {
339        return Err(TaskError::InvalidCpuCount(config.cpu_count()));
340    }
341    if config.deadline_cap_percent() == 0
342        || config.deadline_cap_percent() > 100
343        || config.rt_period_ns() == 0
344        || config.rt_runtime_ns() > config.rt_period_ns()
345        || config.balance_interval_ns() == 0
346        || config.thread_capacity() == 0
347        || config.thread_capacity() > u32::MAX as usize
348        || config.batch_limit() == 0
349        || config.batch_limit() > crate::runtime::config::DEFAULT_BATCH_LIMIT
350        || config.pi_chain_limit() == 0
351    {
352        return Err(TaskError::InvalidConfiguration);
353    }
354    Ok(())
355}
356
357fn deadline_zero_lag(deadline: &DeadlineEntity) -> SchedulerTimestamp {
358    let policy = deadline.policy();
359    let lag_ns = deadline.remaining_runtime_ns() as u128 * policy.period_ns() as u128
360        / policy.runtime_ns() as u128;
361    let lag_ns = u64::try_from(lag_ns)
362        .expect("Deadline zero-lag interval cannot exceed one scheduler period");
363    SchedulerTimestamp::from_nanos(
364        deadline
365            .absolute_deadline_ns()
366            .expect("an active Deadline entity must own a zero-lag anchor"),
367    )
368    .retreat(lag_ns)
369}
370
371fn ensure_runtime_success(status: RuntimeStatus) -> Result<(), TaskError> {
372    if status == RuntimeStatus::Success {
373        Ok(())
374    } else {
375        Err(TaskError::RuntimeFailure(status as u32))
376    }
377}
378
379fn validate_affinity(affinity: &CpuSet, cpu_count: usize) -> Result<(), TaskError> {
380    if affinity.topology_len() == cpu_count {
381        Ok(())
382    } else {
383        Err(TaskError::InvalidConfiguration)
384    }
385}
386
387// The top bit of the generation-bearing identity is reserved for compact
388// scheduler-adjacent owner words such as the Linux-style PI mutex waiters bit.
389// Exhausting a slot retires it instead of wrapping and reintroducing ABA.
390const MAX_THREAD_GENERATION: u32 = i32::MAX as u32;
391
392const fn next_generation(generation: u32) -> u32 {
393    if generation < MAX_THREAD_GENERATION {
394        generation + 1
395    } else {
396        generation
397    }
398}
399
400fn advance_thread_slot_generation(slot: &mut ThreadSlot) -> bool {
401    assert_eq!(
402        slot.pending_deadline_reservation, 0,
403        "a reusable thread slot must not retain Deadline admission"
404    );
405    let next = next_generation(slot.generation);
406    if next == slot.generation {
407        // The empty slot remains in the registry so every stale identity still
408        // resolves to `record == None`, but it is never returned to free_slots:
409        // wrapping would make an older generation-bearing ThreadId valid again.
410        false
411    } else {
412        slot.generation = next;
413        true
414    }
415}