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 closes publication only when its active publisher count
174    /// is zero, so a successful transition cannot strand an inbox node between
175    /// queue insertion and its doorbell.
176    pub fn take_cpu_offline(&self, mut cpu: Pin<&mut CpuLocal>) -> Result<(), TaskError> {
177        self.ensure_owner_cpu_context(&cpu)?;
178        let _irq = IrqScope::enter();
179        let id = cpu.owner();
180        let state = self.state.lock();
181        let mut root_domain = self.root_domain.lock();
182        let remote = Arc::clone(&state.cpu_registration(id)?.remote);
183        if !Arc::ptr_eq(&remote, cpu.remote()) {
184            return Err(TaskError::InvalidRuntimeHandle);
185        }
186        match remote.lifecycle_state() {
187            crate::runtime::cpu::CpuLifecycleState::Offline => {
188                return Err(TaskError::CpuOffline(id.as_u32()));
189            }
190            crate::runtime::cpu::CpuLifecycleState::Inactive
191            | crate::runtime::cpu::CpuLifecycleState::Draining => {
192                return Err(TaskError::CpuNotQuiescent(id.as_u32()));
193            }
194            crate::runtime::cpu::CpuLifecycleState::Online => {}
195        }
196        if root_domain.online.count() <= 1 {
197            return Err(TaskError::LastOnlineCpu(id.as_u32()));
198        }
199        if !root_domain.can_deactivate_cpu(id) {
200            return Err(TaskError::DeadlineAdmission);
201        }
202        let remaining_online = root_domain
203            .online
204            .count()
205            .checked_sub(1)
206            .ok_or(TaskError::InvalidConfiguration)?;
207        let deadline_rebuild = state.deadline_bandwidth_rebuild(remaining_online)?;
208        let rt_period_replacement = (0..root_domain.online.topology_len())
209            .map(|index| CpuId::new(index as u32))
210            .find(|candidate| *candidate != id && root_domain.online.contains(*candidate))
211            .ok_or(TaskError::LastOnlineCpu(id.as_u32()))?;
212
213        self.migrate_dormant_deadline_bandwidth_for_cpu_offline(&state, &root_domain, id)?;
214
215        if !remote.try_deactivate() {
216            Err(TaskError::CpuNotQuiescent(id.as_u32()))
217        } else if !Self::prepare_thread_targets_for_cpu_offline(&state, &root_domain, id)
218            || !remote.try_begin_draining()
219        {
220            remote.cancel_deactivation();
221            Err(TaskError::CpuNotQuiescent(id.as_u32()))
222        } else if !cpu.is_quiescent_for_offline()
223            || !Self::threads_allow_cpu_offline(&state, &root_domain, id)
224        {
225            remote.cancel_draining();
226            Err(TaskError::CpuNotQuiescent(id.as_u32()))
227        } else if let Err(error) = ensure_runtime_success(task_runtime::prepare_cpu_offline(
228            RuntimeCpuId::new(id.as_u32()),
229        )) {
230            remote.cancel_draining();
231            Err(error)
232        } else if !root_domain.remove_online(id, deadline_rebuild) {
233            remote.cancel_draining();
234            Err(TaskError::InvalidConfiguration)
235        } else {
236            cpu.as_mut().clear_fair_balance();
237            self.root_domain.disable_rt_runtime(id);
238            remote.finish_offline();
239            remote
240                .lock_run_queue(RunQueueGuardSource::Lifecycle)
241                .invalidate_domain_publication();
242            self.root_domain.publish_offline(id);
243            if self
244                .root_domain
245                .rt_bandwidth()
246                .migrate_owner(id, rt_period_replacement)
247            {
248                self.cpu_remotes[rt_period_replacement.as_usize()].kick_scheduler_work();
249            }
250            Ok(())
251        }
252    }
253
254    /// Mirrors Linux `dl_task_offline_migration()` for blocked DL tasks.
255    ///
256    /// Runnable/on-CPU tasks must already leave through the normal placement
257    /// carrier. A dormant reservation, however, still owns `this_bw`, optional
258    /// `running_bw`, and inactive/CBS timer entries. Those facts move together
259    /// before the source CPU closes placement publication.
260    fn migrate_dormant_deadline_bandwidth_for_cpu_offline(
261        &self,
262        state: &TaskSystemState,
263        root_domain: &RootDomainState,
264        source: CpuId,
265    ) -> Result<(), TaskError> {
266        let source_remote = &self.cpu_remotes[source.as_usize()];
267        for record in state.slots.iter().filter_map(|slot| slot.record.as_ref()) {
268            let core = &record.core;
269            let mut sched = record.sched.lock();
270            if sched.deadline.bandwidth.reservation_owner() != Some(source) {
271                continue;
272            }
273            if sched.placement.queued_cpu().is_some()
274                || sched.placement.on_cpu().is_some()
275                || sched.placement.has_pending_migration()
276            {
277                return Err(TaskError::CpuNotQuiescent(source.as_u32()));
278            }
279            let target = (0..root_domain.online.topology_len())
280                .map(|index| CpuId::new(index as u32))
281                .find(|candidate| {
282                    *candidate != source
283                        && root_domain.online.contains(*candidate)
284                        && sched.affinity.affinity.contains(*candidate)
285                        && self.cpu_remotes[candidate.as_usize()].accepts_placement()
286                })
287                .ok_or(TaskError::DeadlineAffinity)?;
288            let target_remote = &self.cpu_remotes[target.as_usize()];
289            let publication = target_remote
290                .begin_publication()
291                .ok_or(TaskError::CpuNotQuiescent(target.as_u32()))?;
292
293            let mut source_rq = OwnerRqTxn::begin(self, source_remote);
294            Self::detach_owner_deadline_bandwidth_in_rq(
295                core,
296                &mut sched,
297                source_remote,
298                &mut source_rq,
299            );
300            source_rq.commit();
301
302            let active = sched.deadline.bandwidth.is_active();
303            let mut target_rq = OwnerRqTxn::begin(self, target_remote);
304            Self::attach_deadline_bandwidth_locked(
305                core,
306                &mut sched,
307                &mut target_rq,
308                target,
309                active,
310            );
311            target_rq.commit();
312            core.set_wake_cpu_hint(target);
313            drop(sched);
314            drop(publication);
315            self.publish_owner_deadline_refresh(core, target);
316        }
317        Ok(())
318    }
319
320    /// Stops dormant threads from retaining an inactive preferred target.
321    ///
322    /// Placement is closed before this pass. A producer that sampled the old
323    /// target either finishes its runqueue publication before final draining,
324    /// making the CPU non-quiescent, or revalidates and selects an online CPU.
325    /// This is the active-before-online split used by Linux CPU hotplug around
326    /// `task_cpu()` placement.
327    fn prepare_thread_targets_for_cpu_offline(
328        state: &TaskSystemState,
329        root_domain: &RootDomainState,
330        cpu: CpuId,
331    ) -> bool {
332        let is_idle = |id| {
333            state
334                .cpus
335                .iter()
336                .any(|registration| registration.remote.idle_thread() == Some(id))
337        };
338        let fallback_for = |affinity: &CpuSet| {
339            state
340                .cpus
341                .iter()
342                .enumerate()
343                .map(|(index, registration)| (CpuId::new(index as u32), registration))
344                .find(|(candidate, registration)| {
345                    *candidate != cpu
346                        && root_domain.online.contains(*candidate)
347                        && registration.remote.accepts_placement()
348                        && affinity.contains(*candidate)
349                })
350                .map(|(candidate, _)| candidate)
351        };
352
353        for record in state.slots.iter().filter_map(|slot| slot.record.as_ref()) {
354            if is_idle(record.core.id()) {
355                continue;
356            }
357
358            let sched = record.sched.lock();
359            if sched.lifecycle.state() == ThreadState::Exited {
360                continue;
361            }
362            if Self::is_parked_ktimer_worker(state, cpu, &record.core, &sched) {
363                continue;
364            }
365            if fallback_for(&sched.affinity.affinity).is_none() {
366                return false;
367            }
368            let physically_owned = sched.placement.queued_cpu() == Some(cpu)
369                || sched.placement.on_cpu() == Some(cpu)
370                || sched.placement.committed_migration_target() == Some(cpu)
371                || sched.deadline.bandwidth.reservation_owner() == Some(cpu)
372                || record.core.sleep_timer_cpu() == Some(cpu);
373            if physically_owned {
374                return false;
375            }
376            let has_other_placement = sched.placement.queued_cpu().is_some()
377                || sched.placement.on_cpu().is_some()
378                || sched.placement.has_pending_migration()
379                || sched.deadline.bandwidth.reservation_owner().is_some()
380                || record.core.sleep_timer_cpu().is_some();
381            if record.core.wake_cpu_hint() == Some(cpu) && has_other_placement {
382                return false;
383            }
384        }
385
386        for record in state.slots.iter().filter_map(|slot| slot.record.as_ref()) {
387            if is_idle(record.core.id()) {
388                continue;
389            }
390            let sched = record.sched.lock();
391            if Self::is_parked_ktimer_worker(state, cpu, &record.core, &sched) {
392                continue;
393            }
394            drop(sched);
395            if record.core.wake_cpu_hint() != Some(cpu) {
396                continue;
397            }
398            let sched = record.sched.lock();
399            if sched.lifecycle.state() == ThreadState::Exited {
400                continue;
401            }
402            let Some(fallback) = fallback_for(&sched.affinity.affinity) else {
403                return false;
404            };
405            // Linux deliberately leaves task_cpu() unchanged for blocked
406            // tasks; the next wakeup selects from the then-current active mask.
407            record.core.set_wake_cpu_hint(fallback);
408        }
409        true
410    }
411
412    fn threads_allow_cpu_offline(
413        state: &TaskSystemState,
414        root_domain: &RootDomainState,
415        cpu: CpuId,
416    ) -> bool {
417        state
418            .slots
419            .iter()
420            .filter_map(|slot| slot.record.as_ref())
421            .all(|record| {
422                let id = record.core.id();
423                let is_idle = state
424                    .cpus
425                    .iter()
426                    .any(|registration| registration.remote.idle_thread() == Some(id));
427                if is_idle {
428                    return true;
429                }
430
431                let sched = record.sched.lock();
432                if sched.lifecycle.state() == ThreadState::Exited {
433                    return true;
434                }
435                if Self::is_parked_ktimer_worker(state, cpu, &record.core, &sched) {
436                    return true;
437                }
438                let has_remaining_destination = (0..state.cpus.len()).any(|index| {
439                    let candidate = CpuId::new(index as u32);
440                    candidate != cpu
441                        && root_domain.online.contains(candidate)
442                        && sched.affinity.affinity.contains(candidate)
443                });
444                let owned_by_cpu = sched.placement.queued_cpu() == Some(cpu)
445                    || sched.placement.on_cpu() == Some(cpu)
446                    || sched.placement.committed_migration_target() == Some(cpu)
447                    || sched.deadline.bandwidth.reservation_owner() == Some(cpu)
448                    || record.core.sleep_timer_cpu() == Some(cpu)
449                    || record.core.wake_cpu_hint() == Some(cpu);
450                has_remaining_destination && !owned_by_cpu
451            })
452    }
453
454    /// Linux keeps each `ktimers/%u` task allocated while its CPU is offline.
455    /// The fixed task is hotplug-quiescent only after it has parked on its IRQ
456    /// event and relinquished every rq, timer, and Deadline ownership record.
457    fn is_parked_ktimer_worker(
458        state: &TaskSystemState,
459        cpu: CpuId,
460        core: &ThreadCore,
461        sched: &ThreadSchedState,
462    ) -> bool {
463        state.cpus.get(cpu.as_usize()).is_some_and(|registration| {
464            registration.remote.ktimer_worker() == Some(core.id())
465                && sched.lifecycle.state() == ThreadState::Blocked
466                && sched.placement.queued_cpu().is_none()
467                && sched.placement.on_cpu().is_none()
468                && !sched.placement.has_pending_migration()
469                && sched.deadline.bandwidth.reservation_owner().is_none()
470                && core.sleep_timer_cpu().is_none()
471        })
472    }
473
474    /// Installs an idle thread for a CPU; idle is selected only when queues empty.
475    pub fn install_idle_thread(
476        &self,
477        mut cpu: Pin<&mut CpuLocal>,
478        thread: ThreadId,
479    ) -> Result<(), TaskError> {
480        self.ensure_owner_cpu_context(&cpu)?;
481        let core = {
482            let state = self.state.lock();
483            state.cpu_registration(cpu.owner())?;
484            Arc::clone(&state.thread_record(thread)?.core)
485        };
486        self.install_idle_core(cpu.as_mut(), core)
487    }
488}