Skip to main content

ax_task/sched/system/task_system/
cpu_lifecycle.rs

1//! CPU-local scheduler allocation and online publication.
2
3use super::*;
4
5impl TaskSystem {
6    /// Allocates one pinned CPU-local scheduler object without publishing it.
7    pub fn create_cpu_local(
8        &self,
9        cpu: CpuId,
10    ) -> Result<Pin<alloc::boxed::Box<CpuLocal>>, TaskError> {
11        let remote = Arc::clone(&self.state.lock().cpu_registration(cpu)?.remote);
12        Ok(CpuLocal::create(
13            cpu,
14            self.config,
15            remote,
16            Arc::clone(self.root_domain.rt_bandwidth()),
17        ))
18    }
19
20    /// Returns the stable remote-publication endpoint of a placement-active CPU.
21    pub fn cpu_remote(&self, cpu: CpuId) -> Option<&CpuRemote> {
22        self.cpu_remotes
23            .get(cpu.as_usize())
24            .map(Arc::as_ref)
25            .filter(|remote| remote.accepts_placement())
26    }
27
28    /// Returns the opaque runtime endpoint for a configured CPU.
29    ///
30    /// This bootstrap capability is available before online publication so a
31    /// runtime can cache its current-CPU endpoint in architecture-owned
32    /// storage. Ordinary scheduler producers must use [`Self::cpu_remote`],
33    /// which rejects an offline CPU.
34    #[doc(hidden)]
35    pub fn runtime_cpu_remote_handle(&self, cpu: CpuId) -> CpuRemoteHandle {
36        self.cpu_remotes
37            .get(cpu.as_usize())
38            .map_or(CpuRemoteHandle::NONE, |remote| {
39                // SAFETY: TaskSystem retains this Arc allocation until the
40                // system is destroyed. Runtime providers may publish the raw
41                // handle only while they retain that TaskSystem lifetime.
42                unsafe { CpuRemoteHandle::from_raw(Arc::as_ptr(remote).expose_provenance()) }
43            })
44    }
45
46    /// Returns cumulative non-idle runtime charged by one online CPU.
47    pub fn cpu_busy_runtime_ns(&self, cpu: CpuId) -> Result<u64, TaskError> {
48        let remote = self
49            .cpu_remotes
50            .get(cpu.as_usize())
51            .ok_or(TaskError::InvalidCpu(cpu.as_u32()))?;
52        if !remote.is_online() {
53            return Err(TaskError::CpuOffline(cpu.as_u32()));
54        }
55        Ok(remote.busy_runtime_ns())
56    }
57
58    pub(super) fn ensure_owner_cpu_online(&self, cpu: &CpuLocal) -> Result<(), TaskError> {
59        self.ensure_owner_cpu_context(cpu)?;
60        self.ensure_owner_cpu_registration_online(cpu)
61    }
62
63    /// Verifies the published owner/remote identity after the caller has
64    /// established its CPU ownership context.
65    pub(super) fn ensure_owner_cpu_registration_online(
66        &self,
67        cpu: &CpuLocal,
68    ) -> Result<(), TaskError> {
69        let remote = self
70            .cpu_remotes
71            .get(cpu.owner().as_usize())
72            .ok_or(TaskError::InvalidCpu(cpu.owner().as_u32()))?;
73        if Arc::ptr_eq(remote, cpu.remote()) && remote.is_online() {
74            Ok(())
75        } else {
76            Err(TaskError::CpuOffline(cpu.owner().as_u32()))
77        }
78    }
79
80    /// Enforces the post-publication owner-CPU access contract.
81    ///
82    /// Standalone scheduler models deliberately operate on an unpublished
83    /// `TaskSystem` and retain their direct pinned CpuLocal allocation. Once a
84    /// runtime publishes this exact system handle, every online owner access
85    /// must instead retain either its IRQ pin or scheduler baton. This mirrors
86    /// Linux's rq-lock assertion and closes interrupt-return re-entry over a
87    /// live mutable runqueue borrow.
88    pub(super) fn ensure_owner_cpu_context(&self, cpu: &CpuLocal) -> Result<(), TaskError> {
89        if !cpu.is_online() {
90            return Ok(());
91        }
92        // SAFETY: reading the opaque handle neither dereferences it nor extends
93        // its lifetime. Equality only determines whether this model instance
94        // has crossed the runtime publication boundary.
95        let published = unsafe { task_runtime::task_system_handle() }.into_raw();
96        let this = (self as *const Self).expose_provenance();
97        if published == 0 || published != this {
98            return Ok(());
99        }
100        match task_runtime::validate_owner_cpu_context() {
101            RuntimeStatus::Success => Ok(()),
102            RuntimeStatus::UnsafeContext => Err(TaskError::UnsafeContext),
103            status => Err(TaskError::RuntimeFailure(status as u32)),
104        }
105    }
106
107    /// Completes CPU registration and publishes it in the online root domain.
108    pub fn bring_cpu_online(&self, mut cpu: Pin<&mut CpuLocal>) -> Result<(), TaskError> {
109        let _irq = IrqScope::enter();
110        self.ensure_owner_cpu_context(&cpu)?;
111        let id = cpu.owner();
112        let state = self.state.lock();
113        let mut root_domain = self.root_domain.lock();
114        let registration = state.cpu_registration(id)?;
115        if registration.remote.lifecycle_state() != crate::runtime::cpu::CpuLifecycleState::Offline
116        {
117            return Err(TaskError::CpuAlreadyOnline(id.as_u32()));
118        }
119        if !Arc::ptr_eq(&registration.remote, cpu.remote()) {
120            return Err(TaskError::InvalidRuntimeHandle);
121        }
122        if root_domain.online.contains(id) {
123            return Err(TaskError::InvalidConfiguration);
124        }
125        if state
126            .slots
127            .iter()
128            .filter_map(|slot| slot.record.as_ref())
129            .any(|record| {
130                let sched = record.sched.lock();
131                (matches!(sched.policy.base, SchedulePolicy::Deadline(_))
132                    || matches!(sched.policy.requested_policy(), SchedulePolicy::Deadline(_)))
133                    && !sched.affinity.affinity.contains(id)
134            })
135        {
136            return Err(TaskError::DeadlineAffinity);
137        }
138        ensure_runtime_success(task_runtime::prepare_cpu_online(RuntimeCpuId::new(
139            id.as_u32(),
140        )))?;
141        let monotonic_now = task_runtime::monotonic_now();
142        cpu.as_mut()
143            .reset_fair_balance(monotonic_now, self.config.balance_interval_ns());
144        let online_count = root_domain
145            .online
146            .count()
147            .checked_add(1)
148            .ok_or(TaskError::InvalidConfiguration)?;
149        let deadline_rebuild = state.deadline_bandwidth_rebuild(online_count)?;
150        self.root_domain.enable_rt_runtime(id);
151        assert!(
152            root_domain.insert_online(id, deadline_rebuild),
153            "validated offline CPU must be absent from the root domain"
154        );
155        assert!(
156            cpu.as_ref().get_ref().remote().mark_online(),
157            "validated offline CPU must accept final publication"
158        );
159        OwnerRqTxn::begin(self, cpu.remote()).commit();
160        if cpu
161            .lock_run_queue(RunQueueGuardSource::Lifecycle)
162            .has_runnable_rt()
163        {
164            self.root_domain.activate_rt_period(id, || monotonic_now);
165        }
166        Ok(())
167    }
168
169    /// Removes a quiescent owner CPU from placement and remote publication.
170    ///
171    /// The caller must first migrate or retire every non-idle thread, cancel
172    /// local task deadlines, and consume the CPU's scheduler IPI. The packed
173    /// remote lifecycle first closes placement, then waits for prior publishers
174    /// before closing owner delivery. `NotReady` retains `Inactive`: the owner
175    /// must service normal scheduler work and continue this operation. Other
176    /// errors roll back admission. Success cannot strand an inbox node between
177    /// queue insertion and its doorbell.
178    pub fn take_cpu_offline(&self, mut cpu: Pin<&mut CpuLocal>) -> Result<(), TaskError> {
179        self.ensure_owner_cpu_context(&cpu)?;
180        let _irq = IrqScope::enter();
181        let id = cpu.owner();
182        let state = self.state.lock();
183        let mut root_domain = self.root_domain.lock();
184        let remote = Arc::clone(&state.cpu_registration(id)?.remote);
185        if !Arc::ptr_eq(&remote, cpu.remote()) {
186            return Err(TaskError::InvalidRuntimeHandle);
187        }
188        let result = (|| {
189            match remote.lifecycle_state() {
190                crate::runtime::cpu::CpuLifecycleState::Offline => {
191                    return Err(TaskError::CpuOffline(id.as_u32()));
192                }
193                crate::runtime::cpu::CpuLifecycleState::Draining => {
194                    return Err(TaskError::CpuNotQuiescent(id.as_u32()));
195                }
196                crate::runtime::cpu::CpuLifecycleState::Online
197                | crate::runtime::cpu::CpuLifecycleState::Inactive => {}
198            }
199            // A staged first activation is a persistent admission reservation, not
200            // an in-flight publication reader. Registry ownership serializes this
201            // check with stage/activate/cancel, before placement is closed.
202            if state
203                .slots
204                .iter()
205                .filter_map(|slot| slot.record.as_ref())
206                .any(|record| {
207                    record
208                        .activation
209                        .as_ref()
210                        .is_some_and(|activation| activation.target() == id)
211                })
212            {
213                #[cfg(feature = "fault-injection")]
214                crate::runtime::cpu::record_idle_offline_rejection(
215                    crate::runtime::cpu::IdleOfflineRejection::PlacementPublication,
216                );
217                return Err(TaskError::CpuNotQuiescent(id.as_u32()));
218            }
219            if root_domain.online.count() <= 1 {
220                return Err(TaskError::LastOnlineCpu(id.as_u32()));
221            }
222            if !root_domain.can_deactivate_cpu(id) {
223                return Err(TaskError::DeadlineAdmission);
224            }
225            let remaining_online = root_domain
226                .online
227                .count()
228                .checked_sub(1)
229                .ok_or(TaskError::InvalidConfiguration)?;
230            let deadline_rebuild = state.deadline_bandwidth_rebuild(remaining_online)?;
231            let rt_period_replacement = (0..root_domain.online.topology_len())
232                .map(|index| CpuId::new(index as u32))
233                .find(|candidate| *candidate != id && root_domain.online.contains(*candidate))
234                .ok_or(TaskError::LastOnlineCpu(id.as_u32()))?;
235
236            self.migrate_dormant_deadline_bandwidth_for_cpu_offline(&state, &root_domain, id)?;
237
238            if remote.lifecycle_state() == crate::runtime::cpu::CpuLifecycleState::Online
239                && !remote.try_deactivate()
240            {
241                #[cfg(feature = "fault-injection")]
242                crate::runtime::cpu::record_idle_offline_rejection(
243                    crate::runtime::cpu::IdleOfflineRejection::PlacementPublication,
244                );
245                Err(TaskError::CpuNotQuiescent(id.as_u32()))
246            } else if !remote.try_begin_draining() {
247                #[cfg(feature = "fault-injection")]
248                crate::runtime::cpu::record_idle_offline_rejection(
249                    crate::runtime::cpu::IdleOfflineRejection::OwnerPublication,
250                );
251                Err(TaskError::NotReady)
252            } else if !cpu.is_quiescent_for_offline() {
253                #[cfg(feature = "fault-injection")]
254                crate::runtime::cpu::record_idle_offline_rejection(
255                    if cpu.needs_reschedule() || cpu.has_remote_work() {
256                        crate::runtime::cpu::IdleOfflineRejection::SchedulerWork
257                    } else {
258                        crate::runtime::cpu::IdleOfflineRejection::CpuState
259                    },
260                );
261                if cpu.needs_reschedule() || cpu.has_remote_work() {
262                    Err(TaskError::NotReady)
263                } else {
264                    Err(TaskError::CpuNotQuiescent(id.as_u32()))
265                }
266            } else if !Self::prepare_thread_targets_for_cpu_offline(&state, &root_domain, id) {
267                #[cfg(feature = "fault-injection")]
268                crate::runtime::cpu::record_idle_offline_rejection(
269                    crate::runtime::cpu::IdleOfflineRejection::ThreadTarget,
270                );
271                Err(TaskError::CpuNotQuiescent(id.as_u32()))
272            } else if !Self::threads_allow_cpu_offline(&state, &root_domain, id) {
273                #[cfg(feature = "fault-injection")]
274                crate::runtime::cpu::record_idle_offline_rejection(
275                    crate::runtime::cpu::IdleOfflineRejection::ThreadOwnership,
276                );
277                Err(TaskError::CpuNotQuiescent(id.as_u32()))
278            } else if let Err(error) = ensure_runtime_success(task_runtime::prepare_cpu_offline(
279                RuntimeCpuId::new(id.as_u32()),
280            )) {
281                Err(error)
282            } else if !root_domain.remove_online(id, deadline_rebuild) {
283                Err(TaskError::InvalidConfiguration)
284            } else {
285                cpu.as_mut().clear_fair_balance();
286                self.root_domain.disable_rt_runtime(id);
287                remote.finish_offline();
288                remote
289                    .lock_run_queue(RunQueueGuardSource::Lifecycle)
290                    .invalidate_domain_publication();
291                self.root_domain.publish_offline(id);
292                if self
293                    .root_domain
294                    .rt_bandwidth()
295                    .migrate_owner(id, rt_period_replacement)
296                {
297                    self.cpu_remotes[rt_period_replacement.as_usize()].kick_scheduler_work();
298                }
299                Ok(())
300            }
301        })();
302        // Pending work resumes on an inactive CPU: placement stays closed
303        // across normal scheduling. Every terminal error reopens admission.
304        match (&result, remote.lifecycle_state()) {
305            (Err(TaskError::NotReady), crate::runtime::cpu::CpuLifecycleState::Draining) => {
306                remote.resume_owner_drain()
307            }
308            (Err(TaskError::NotReady), _) => {}
309            (Err(_), crate::runtime::cpu::CpuLifecycleState::Draining) => remote.cancel_draining(),
310            (Err(_), crate::runtime::cpu::CpuLifecycleState::Inactive) => {
311                remote.cancel_deactivation()
312            }
313            _ => {}
314        }
315        result
316    }
317
318    /// Mirrors Linux `dl_task_offline_migration()` for blocked DL tasks.
319    ///
320    /// Runnable/on-CPU tasks must already leave through the normal placement
321    /// carrier. A dormant reservation, however, still owns `this_bw`, optional
322    /// `running_bw`, and inactive/CBS timer entries. Those facts move together
323    /// before the source CPU closes placement publication.
324    fn migrate_dormant_deadline_bandwidth_for_cpu_offline(
325        &self,
326        state: &TaskSystemState,
327        root_domain: &RootDomainState,
328        source: CpuId,
329    ) -> Result<(), TaskError> {
330        let source_remote = &self.cpu_remotes[source.as_usize()];
331        for record in state.slots.iter().filter_map(|slot| slot.record.as_ref()) {
332            let core = &record.core;
333            let mut sched = record.sched.lock();
334            if sched.deadline.bandwidth.reservation_owner() != Some(source) {
335                continue;
336            }
337            if sched.placement.queued_cpu().is_some()
338                || sched.placement.on_cpu().is_some()
339                || sched.placement.has_pending_migration()
340            {
341                return Err(TaskError::CpuNotQuiescent(source.as_u32()));
342            }
343            let target = (0..root_domain.online.topology_len())
344                .map(|index| CpuId::new(index as u32))
345                .find(|candidate| {
346                    *candidate != source
347                        && root_domain.online.contains(*candidate)
348                        && sched.affinity.affinity.contains(*candidate)
349                        && self.cpu_remotes[candidate.as_usize()].accepts_placement()
350                })
351                .ok_or(TaskError::DeadlineAffinity)?;
352            let target_remote = &self.cpu_remotes[target.as_usize()];
353            let publication = target_remote
354                .begin_publication()
355                .ok_or(TaskError::CpuNotQuiescent(target.as_u32()))?;
356
357            let mut source_rq = OwnerRqTxn::begin(self, source_remote);
358            Self::detach_owner_deadline_bandwidth_in_rq(
359                core,
360                &mut sched,
361                source_remote,
362                &mut source_rq,
363            );
364            source_rq.commit();
365
366            let active = sched.deadline.bandwidth.is_active();
367            let mut target_rq = OwnerRqTxn::begin(self, target_remote);
368            Self::attach_deadline_bandwidth_locked(
369                core,
370                &mut sched,
371                &mut target_rq,
372                target,
373                active,
374            );
375            target_rq.commit();
376            core.set_wake_cpu_hint(target);
377            drop(sched);
378            drop(publication);
379            self.publish_owner_deadline_refresh(core, target);
380        }
381        Ok(())
382    }
383
384    /// Stops dormant threads from retaining an inactive preferred target.
385    ///
386    /// Placement is closed before this pass. A producer that sampled the old
387    /// target either finishes its runqueue publication before final draining,
388    /// making the CPU non-quiescent, or revalidates and selects an online CPU.
389    /// This is the active-before-online split used by Linux CPU hotplug around
390    /// `task_cpu()` placement.
391    fn prepare_thread_targets_for_cpu_offline(
392        state: &TaskSystemState,
393        root_domain: &RootDomainState,
394        cpu: CpuId,
395    ) -> bool {
396        let is_idle = |id| {
397            state
398                .cpus
399                .iter()
400                .any(|registration| registration.remote.idle_thread() == Some(id))
401        };
402        let fallback_for = |affinity: &CpuSet| {
403            state
404                .cpus
405                .iter()
406                .enumerate()
407                .map(|(index, registration)| (CpuId::new(index as u32), registration))
408                .find(|(candidate, registration)| {
409                    *candidate != cpu
410                        && root_domain.online.contains(*candidate)
411                        && registration.remote.accepts_placement()
412                        && affinity.contains(*candidate)
413                })
414                .map(|(candidate, _)| candidate)
415        };
416
417        for record in state.slots.iter().filter_map(|slot| slot.record.as_ref()) {
418            if is_idle(record.core.id()) {
419                continue;
420            }
421
422            let sched = record.sched.lock();
423            if sched.lifecycle.state() == ThreadState::Exited {
424                continue;
425            }
426            if Self::is_parked_ktimer_worker(state, cpu, &record.core, &sched) {
427                continue;
428            }
429            if fallback_for(&sched.affinity.affinity).is_none() {
430                return false;
431            }
432            let physically_owned = sched.placement.queued_cpu() == Some(cpu)
433                || sched.placement.on_cpu() == Some(cpu)
434                || sched.placement.committed_migration_target() == Some(cpu)
435                || sched.deadline.bandwidth.reservation_owner() == Some(cpu)
436                || record.core.sleep_timer_cpu() == Some(cpu);
437            if physically_owned {
438                return false;
439            }
440            let has_other_placement = sched.placement.queued_cpu().is_some()
441                || sched.placement.on_cpu().is_some()
442                || sched.placement.has_pending_migration()
443                || sched.deadline.bandwidth.reservation_owner().is_some()
444                || record.core.sleep_timer_cpu().is_some();
445            if record.core.wake_cpu_hint() == Some(cpu) && has_other_placement {
446                return false;
447            }
448        }
449
450        for record in state.slots.iter().filter_map(|slot| slot.record.as_ref()) {
451            if is_idle(record.core.id()) {
452                continue;
453            }
454            let sched = record.sched.lock();
455            if Self::is_parked_ktimer_worker(state, cpu, &record.core, &sched) {
456                continue;
457            }
458            drop(sched);
459            if record.core.wake_cpu_hint() != Some(cpu) {
460                continue;
461            }
462            let sched = record.sched.lock();
463            if sched.lifecycle.state() == ThreadState::Exited {
464                continue;
465            }
466            let Some(fallback) = fallback_for(&sched.affinity.affinity) else {
467                return false;
468            };
469            // Linux deliberately leaves task_cpu() unchanged for blocked
470            // tasks; the next wakeup selects from the then-current active mask.
471            record.core.set_wake_cpu_hint(fallback);
472        }
473        true
474    }
475
476    fn threads_allow_cpu_offline(
477        state: &TaskSystemState,
478        root_domain: &RootDomainState,
479        cpu: CpuId,
480    ) -> bool {
481        state
482            .slots
483            .iter()
484            .filter_map(|slot| slot.record.as_ref())
485            .all(|record| {
486                let id = record.core.id();
487                let is_idle = state
488                    .cpus
489                    .iter()
490                    .any(|registration| registration.remote.idle_thread() == Some(id));
491                let sched = record.sched.lock();
492                // Migration exclusion applies even to idle. Do not bypass a
493                // live pin merely because this task has no class-queue node.
494                if sched.affinity.migration_cpu == Some(cpu) {
495                    return false;
496                }
497                if is_idle || sched.lifecycle.state() == ThreadState::Exited {
498                    return true;
499                }
500                if Self::is_parked_ktimer_worker(state, cpu, &record.core, &sched) {
501                    return true;
502                }
503                let has_remaining_destination = (0..state.cpus.len()).any(|index| {
504                    let candidate = CpuId::new(index as u32);
505                    candidate != cpu
506                        && root_domain.online.contains(candidate)
507                        && sched.affinity.affinity.contains(candidate)
508                });
509                let owned_by_cpu = sched.placement.queued_cpu() == Some(cpu)
510                    || sched.placement.on_cpu() == Some(cpu)
511                    || sched.placement.committed_migration_target() == Some(cpu)
512                    || sched.deadline.bandwidth.reservation_owner() == Some(cpu)
513                    || record.core.sleep_timer_cpu() == Some(cpu)
514                    || record.core.wake_cpu_hint() == Some(cpu);
515                has_remaining_destination && !owned_by_cpu
516            })
517    }
518
519    /// Linux keeps each `ktimers/%u` task allocated while its CPU is offline.
520    /// The fixed task is hotplug-quiescent only after it has parked on its IRQ
521    /// event and relinquished every rq, timer, and Deadline ownership record.
522    fn is_parked_ktimer_worker(
523        state: &TaskSystemState,
524        cpu: CpuId,
525        core: &ThreadCore,
526        sched: &ThreadSchedState,
527    ) -> bool {
528        state.cpus.get(cpu.as_usize()).is_some_and(|registration| {
529            registration.remote.ktimer_worker() == Some(core.id())
530                && sched.lifecycle.state() == ThreadState::Blocked
531                && sched.placement.queued_cpu().is_none()
532                && sched.placement.on_cpu().is_none()
533                && !sched.placement.has_pending_migration()
534                && sched.deadline.bandwidth.reservation_owner().is_none()
535                && core.sleep_timer_cpu().is_none()
536        })
537    }
538
539    /// Installs an idle thread for a CPU; idle is selected only when queues empty.
540    pub fn install_idle_thread(
541        &self,
542        mut cpu: Pin<&mut CpuLocal>,
543        thread: ThreadId,
544    ) -> Result<(), TaskError> {
545        self.ensure_owner_cpu_context(&cpu)?;
546        let core = {
547            let state = self.state.lock();
548            state.cpu_registration(cpu.owner())?;
549            Arc::clone(&state.thread_record(thread)?.core)
550        };
551        self.install_idle_core(cpu.as_mut(), core)
552    }
553}