Skip to main content

ax_task/sched/system/task_system/
balance.rs

1//! Owner-runqueue load publication and SMP balancing.
2
3use super::*;
4/// One owner-selected migration candidate and destination.
5///
6/// Selection is intentionally move-only. The owner may revalidate and commit
7/// this exact candidate once, but a caller cannot accidentally scan the source
8/// runqueue again after choosing a destination.
9pub(super) struct OwnerBalanceSelection {
10    candidate: QueuedThreadSnapshot,
11    target: CpuId,
12    reason: BalanceReason,
13}
14
15/// Whether one optional balance pass changed the owner runqueue after the
16/// preceding selection transaction captured its deadline inputs.
17#[derive(Clone, Copy)]
18pub(super) struct OwnerBalanceOutcome {
19    run_queue_changed: bool,
20}
21
22impl OwnerBalanceOutcome {
23    pub(super) const fn run_queue_changed(self) -> bool {
24        self.run_queue_changed
25    }
26}
27
28impl OwnerBalanceSelection {
29    pub(super) const fn target(&self) -> CpuId {
30        self.target
31    }
32}
33
34fn fair_migration_imbalance(
35    source_demand: u64,
36    target_demand: u64,
37    candidate_demand: u64,
38) -> Option<u64> {
39    if candidate_demand == 0 || source_demand <= target_demand {
40        return None;
41    }
42    let imbalance_before = source_demand - target_demand;
43    let source_after = source_demand.saturating_sub(candidate_demand);
44    let target_after = target_demand.saturating_add(candidate_demand);
45    let imbalance_after = source_after.abs_diff(target_after);
46    (imbalance_after < imbalance_before).then_some(imbalance_after)
47}
48
49impl TaskSystem {
50    /// Returns the fixed CPU topology width accepted by affinity masks.
51    pub const fn cpu_topology_len(&self) -> usize {
52        self.config.cpu_count()
53    }
54
55    /// Captures stable state for deterministic scheduler comparisons.
56    pub fn snapshot(&self, cpu: Pin<&CpuLocal>) -> Result<CpuSnapshot, TaskError> {
57        self.ensure_owner_cpu_context(&cpu)?;
58        Ok(CpuSnapshot::capture(&cpu))
59    }
60
61    /// Returns the number of CPUs currently available for placement.
62    pub fn online_cpu_count(&self) -> usize {
63        self.root_domain.lock().online.count()
64    }
65
66    /// Returns the CPUs that currently accept runnable placement.
67    ///
68    /// This is the scheduler's Linux-style active mask, not the fixed possible
69    /// CPU topology. Callers that must start a runnable worker immediately must
70    /// choose its affinity from this snapshot.
71    pub fn active_cpu_set(&self) -> CpuSet {
72        self.root_domain.lock().online.clone()
73    }
74
75    pub(crate) fn publish_run_queue_summary(
76        &self,
77        remote: &CpuRemote,
78        run_queue: &mut CpuRunQueueState,
79    ) {
80        let online = remote.accepts_placement();
81        if !run_queue.take_summary_dirty(online) {
82            return;
83        }
84        let _ = remote.publish_run_queue_load_summary(run_queue);
85        if let Some((previous, publication)) = run_queue.take_domain_publication(online) {
86            self.root_domain
87                .publish_run_queue(remote.owner(), previous, publication);
88        }
89    }
90
91    /// Mirrors Linux `need_pull_rt_task()`/`need_pull_dl_task()` followed by
92    /// `tell_cpu_to_push()`: when this rq installs a less urgent task, start
93    /// the root-domain push iterator. The iterator serializes delivery across
94    /// overloaded rq owners instead of broadcasting one IPI per source.
95    pub(super) fn notify_overloaded_owners_after_priority_drop(
96        &self,
97        owner: CpuId,
98        previous: Option<SchedulingUrgency>,
99        next: SchedulingUrgency,
100    ) {
101        let Some(previous) = previous else {
102            return;
103        };
104        if !matches!(
105            previous.class_rank(),
106            DEADLINE_CLASS_RANK | REALTIME_CLASS_RANK
107        ) || next <= previous
108        {
109            return;
110        }
111        let class = match previous.class_rank() {
112            DEADLINE_CLASS_RANK => RootDomainPushClass::Deadline,
113            REALTIME_CLASS_RANK => RootDomainPushClass::Realtime,
114            _ => return,
115        };
116        self.root_domain.request_rt_deadline_push(class, owner);
117    }
118
119    fn select_owner_balance_transfer_by(
120        &self,
121        cpu: &CpuLocal,
122        reason: BalanceReason,
123        class_filter: Option<SchedulingClass>,
124        mut select_target: impl FnMut(&QueuedThreadSnapshot, &ThreadSchedState) -> Option<CpuId>,
125    ) -> Option<OwnerBalanceSelection> {
126        let source = cpu.owner();
127        let (current_policy, mut scan) = {
128            let mut transaction = OwnerRqTxn::begin(self, cpu.remote());
129            let current_policy = transaction.current().map(CurrentDispatch::schedule_policy);
130            let scan = transaction.begin_balance_scan(class_filter);
131            transaction.commit();
132            (current_policy, scan)
133        };
134        loop {
135            let candidate = {
136                let mut transaction = OwnerRqTxn::begin(self, cpu.remote());
137                let queued_top_rt = transaction.highest_rt_priority();
138                let top_rt_count =
139                    queued_top_rt.map_or(0, |priority| transaction.rt_count_at_priority(priority));
140                let candidate = transaction.next_balance_candidate(&mut scan, |candidate| {
141                    let class_allowed = match reason {
142                        // Linux treats SCHED_IDLE as ordinary fair-class work
143                        // for both idle pull and periodic Fair balancing.
144                        BalanceReason::IdlePull | BalanceReason::FairPeriodic => {
145                            matches!(candidate.policy(), SchedulePolicy::Fair { .. })
146                        }
147                        BalanceReason::RtDeadlinePush => matches!(
148                            candidate.policy(),
149                            SchedulePolicy::Deadline(_)
150                                | SchedulePolicy::Fifo { .. }
151                                | SchedulePolicy::RoundRobin { .. }
152                        ),
153                    };
154                    let matches_filter = class_filter.is_none_or(|class| match class {
155                        SchedulingClass::Deadline => {
156                            matches!(candidate.policy(), SchedulePolicy::Deadline(_))
157                        }
158                        SchedulingClass::Realtime => matches!(
159                            candidate.policy(),
160                            SchedulePolicy::Fifo { .. } | SchedulePolicy::RoundRobin { .. }
161                        ),
162                        SchedulingClass::Fair => {
163                            matches!(candidate.policy(), SchedulePolicy::Fair { .. })
164                        }
165                        SchedulingClass::Stop => {
166                            matches!(candidate.policy(), SchedulePolicy::KernelStop)
167                        }
168                    });
169                    if !class_allowed || !matches_filter {
170                        return false;
171                    }
172                    let candidate_priority = match candidate.policy() {
173                        SchedulePolicy::Fifo { priority }
174                        | SchedulePolicy::RoundRobin { priority, .. } => priority.get(),
175                        _ => return true,
176                    };
177                    match current_policy {
178                        Some(SchedulePolicy::Deadline(_)) => true,
179                        Some(SchedulePolicy::Fifo { priority })
180                        | Some(SchedulePolicy::RoundRobin { priority, .. }) => {
181                            candidate_priority <= priority.get()
182                        }
183                        _ => queued_top_rt.is_some_and(|top| {
184                            candidate_priority < top
185                                || (candidate_priority == top && top_rt_count > 1)
186                        }),
187                    }
188                });
189                transaction.commit();
190                candidate
191            }?;
192            let sched = candidate.core.sched().lock();
193            let Some(target) = select_target(&candidate, &sched) else {
194                continue;
195            };
196            let target_is_allowed = |target: CpuId| {
197                self.cpu_remotes
198                    .get(target.as_usize())
199                    .is_some_and(|remote| {
200                        remote.accepts_placement()
201                            && remote.is_scheduler_ready()
202                            && sched.affinity.affinity.contains(target)
203                    })
204            };
205            let deadline_covers_online = !matches!(sched.policy.base, SchedulePolicy::Deadline(_))
206                || self.cpu_remotes.iter().enumerate().all(|(index, remote)| {
207                    !remote.accepts_placement()
208                        || sched.affinity.affinity.contains(CpuId::new(index as u32))
209                });
210            if target == source
211                || !target_is_allowed(target)
212                || sched.placement.queued_cpu() != Some(source)
213                || sched.placement.has_pending_migration()
214                || sched.placement.on_cpu().is_some()
215                || candidate.core.sleep_timer_cpu().is_some()
216                || !deadline_covers_online
217            {
218                continue;
219            }
220            let queued = {
221                let transaction = OwnerRqTxn::begin(self, cpu.remote());
222                let queued = transaction.queued_thread(candidate.id);
223                transaction.commit();
224                queued
225            };
226            if let Some(queued) = queued {
227                return Some(OwnerBalanceSelection {
228                    candidate: queued,
229                    target,
230                    reason,
231                });
232            }
233        }
234    }
235
236    pub(super) fn select_owner_balance_transfer(
237        &self,
238        cpu: &CpuLocal,
239        target: CpuId,
240        reason: BalanceReason,
241        class_filter: Option<SchedulingClass>,
242    ) -> Option<OwnerBalanceSelection> {
243        self.select_owner_balance_transfer_by(cpu, reason, class_filter, |_, _| Some(target))
244    }
245
246    pub(super) fn select_rt_deadline_balance_transfer(
247        &self,
248        cpu: &CpuLocal,
249        class: Option<SchedulingClass>,
250    ) -> Option<OwnerBalanceSelection> {
251        let source = cpu.owner();
252        self.select_owner_balance_transfer_by(
253            cpu,
254            BalanceReason::RtDeadlinePush,
255            class,
256            |candidate, sched| {
257                self.select_rt_deadline_push_cpu(
258                    candidate.policy,
259                    candidate.entity.clone(),
260                    &sched.affinity.affinity,
261                    source,
262                )
263            },
264        )
265    }
266
267    pub(super) fn commit_owner_balance_transfer(
268        &self,
269        mut cpu: Pin<&mut CpuLocal>,
270        selection: OwnerBalanceSelection,
271    ) -> Result<BalanceTransferOutcome, TaskError> {
272        self.ensure_owner_cpu_online(&cpu)?;
273        let _irq = IrqScope::enter();
274        let OwnerBalanceSelection {
275            candidate,
276            target,
277            reason,
278        } = selection;
279        if self
280            .cpu_remote(target)
281            .is_none_or(|remote| !remote.is_scheduler_ready())
282        {
283            return Ok(BalanceTransferOutcome::Retry);
284        }
285        let source = cpu.owner();
286        if source == target {
287            return Ok(BalanceTransferOutcome::NoCandidate);
288        }
289        let migrated_fair = matches!(candidate.policy(), SchedulePolicy::Fair { .. });
290        let core = candidate.core;
291        let mut sched = core.sched().lock();
292        let deadline_covers_online = !matches!(sched.policy.base, SchedulePolicy::Deadline(_))
293            || self.cpu_remotes.iter().enumerate().all(|(index, remote)| {
294                !remote.accepts_placement()
295                    || sched.affinity.affinity.contains(CpuId::new(index as u32))
296            });
297        if sched.lifecycle.state() != ThreadState::Running
298            || sched.placement.queued_cpu() != Some(source)
299            || sched.placement.has_pending_migration()
300            || sched.placement.on_cpu().is_some()
301            || !sched.affinity.affinity.contains(target)
302            || core.sleep_timer_cpu().is_some()
303            || !deadline_covers_online
304        {
305            return Ok(BalanceTransferOutcome::Retry);
306        }
307
308        let carrier = match self.prepare_owner_migration(&core, source, target) {
309            Ok(carrier) => carrier,
310            Err(_) => {
311                return Ok(BalanceTransferOutcome::Retry);
312            }
313        };
314        let remote = Arc::clone(cpu.remote());
315        let mut transaction = OwnerRqTxn::begin(self, &remote);
316        let detached = {
317            let current_fair = transaction.current_fair_contender();
318            let Some(detached) = transaction.detach_for_transfer(
319                core.id(),
320                current_fair,
321                self.config.timing_granularity_ns(),
322            ) else {
323                transaction.commit();
324                return Ok(BalanceTransferOutcome::Retry);
325            };
326            detached
327        };
328        Self::detach_owner_deadline_bandwidth_in_rq(
329            &core,
330            &mut sched,
331            cpu.remote(),
332            &mut transaction,
333        );
334        core.sched()
335            .install_active(&mut sched, detached.into_active());
336        sched.placement.begin_migration(source, target);
337        core.set_wake_cpu_hint(target);
338        transaction.commit();
339        drop(sched);
340        carrier.commit();
341        if migrated_fair && reason != BalanceReason::FairPeriodic {
342            cpu.as_mut().reset_fair_balance(
343                task_runtime::monotonic_now(),
344                self.config.balance_interval_ns(),
345            );
346        }
347
348        Ok(BalanceTransferOutcome::Migrated(core.id()))
349    }
350
351    pub(super) fn transfer_owner_balance_candidate(
352        &self,
353        cpu: Pin<&mut CpuLocal>,
354        target: CpuId,
355        reason: BalanceReason,
356        class_filter: Option<SchedulingClass>,
357    ) -> Result<BalanceTransferOutcome, TaskError> {
358        self.ensure_owner_cpu_online(&cpu)?;
359        let Some(selection) = self.select_owner_balance_transfer(
360            cpu.as_ref().get_ref(),
361            target,
362            reason,
363            class_filter,
364        ) else {
365            return Ok(BalanceTransferOutcome::NoCandidate);
366        };
367        self.commit_owner_balance_transfer(cpu, selection)
368    }
369
370    /// Returns whether this owner has scheduler-class balance work to service.
371    ///
372    /// The owner has just published a coherent runqueue snapshot. Like Linux's
373    /// rq balance callbacks, an ordinary context switch is not itself a reason
374    /// to enter SMP balancing: idle entry, an overloaded RT/Deadline queue, or
375    /// the periodic Fair deadline must request the work explicitly.
376    pub(super) fn owner_balance_work_pending(&self, cpu: &CpuLocal, next: ThreadId) -> bool {
377        // Every caller owns the scheduler transaction or its post-selection
378        // tail, which already excludes hard IRQ entry. Linux tests the rq
379        // callback pointer directly here instead of revalidating interrupt
380        // context through an architecture/runtime boundary.
381        let idle = cpu.remote().idle_thread() == Some(next);
382        let idle_pull_pending = idle
383            && cpu.idle_pull_pending()
384            // Linux `sched_balance_newidle()` skips the pass when the root
385            // domain has no overloaded source. Keep the one-shot armed so a
386            // later source publication can drive the real pull.
387            && self.root_domain.has_idle_pull_source();
388        if idle_pull_pending
389            || cpu.fair_balance_pending()
390            || self.root_domain.fair_nohz_balancer_pending(cpu.owner())
391        {
392            return true;
393        }
394        rt_deadline_balance_work_pending(self.root_domain.push_target_pending(cpu.owner()))
395    }
396
397    pub(super) fn service_owner_balance(
398        &self,
399        mut cpu: Pin<&mut CpuLocal>,
400        next: ThreadId,
401    ) -> Result<OwnerBalanceOutcome, TaskError> {
402        let idle = cpu.remote().idle_thread() == Some(next);
403        let class_pull_required = idle
404            && (self.root_domain.cpu_has_rt_deadline_overload(cpu.owner())
405                || self.root_domain.push_target_pending(cpu.owner()));
406        let idle_pull_required =
407            idle && (cpu.as_mut().take_idle_pull_pending() || class_pull_required);
408        let fair_nohz_claim = self.root_domain.claim_fair_nohz_balancer(cpu.owner());
409        let push_claim = self.root_domain.claim_rt_deadline_push(cpu.owner());
410        let mut fair_nohz_serviced = false;
411        let balance = (|| -> Result<(Option<ThreadId>, Option<ThreadId>), TaskError> {
412            if idle {
413                if idle_pull_required {
414                    let _requested = self.request_idle_pull(cpu.as_mut())?;
415                }
416                if fair_nohz_claim.is_some() {
417                    fair_nohz_serviced = self.request_fair_nohz_idle_pulls();
418                }
419                let fair = self.balance_fair(cpu.as_mut())?;
420                Ok((None, fair))
421            } else {
422                let class = push_claim
423                    .as_ref()
424                    .map(|claim| claim.class().scheduling_class());
425                let pushed = self.push_rt_deadline_from_root_domain(cpu.as_mut(), class)?;
426                let fair = self.balance_fair(cpu.as_mut())?;
427                Ok((pushed, fair))
428            }
429        })();
430        let (pushed, fair) = match balance {
431            Ok(outcome) => outcome,
432            Err(error) => {
433                if let Some(claim) = fair_nohz_claim {
434                    self.root_domain.finish_fair_nohz_balancer(claim, false);
435                }
436                if let Some(claim) = push_claim {
437                    self.root_domain.finish_rt_deadline_push(claim, false);
438                }
439                return Err(error);
440            }
441        };
442        if let Some(claim) = fair_nohz_claim {
443            self.root_domain
444                .finish_fair_nohz_balancer(claim, fair_nohz_serviced);
445        }
446        if let Some(claim) = push_claim {
447            self.root_domain
448                .finish_rt_deadline_push(claim, pushed.is_some());
449        } else if pushed.is_some() && self.root_domain.cpu_has_rt_deadline_overload(cpu.owner()) {
450            // Linux `push_rt_tasks()`/`push_dl_tasks()` keep running the
451            // callback while a migration makes progress. Preserve that loop
452            // as another bounded owner safe point instead of monopolizing one
453            // scheduler entry.
454            cpu.request_scheduler_work();
455        }
456        Ok(OwnerBalanceOutcome {
457            run_queue_changed: pushed.is_some() || fair.is_some(),
458        })
459    }
460
461    pub(super) fn balance_fair(
462        &self,
463        mut cpu: Pin<&mut CpuLocal>,
464    ) -> Result<Option<ThreadId>, TaskError> {
465        if task_runtime::in_hard_irq() || !cpu.fair_balance_pending() {
466            return Ok(None);
467        }
468        self.ensure_owner_cpu_online(&cpu)?;
469        let source = cpu.owner();
470        self.root_domain.kick_fair_nohz_balance_if_source(source);
471        let source_demand = cpu.remote().placement_demand();
472        let result = {
473            let lower_load_target_seen =
474                self.cpu_remotes.iter().enumerate().any(|(index, remote)| {
475                    let target = CpuId::new(index as u32);
476                    remote.accepts_placement()
477                        && target != source
478                        && remote.placement_demand() < source_demand
479                });
480            let selection = self.select_owner_balance_transfer_by(
481                cpu.as_ref().get_ref(),
482                BalanceReason::FairPeriodic,
483                Some(SchedulingClass::Fair),
484                |candidate, sched| {
485                    let candidate_demand = candidate.placement_demand();
486                    self.cpu_remotes
487                        .iter()
488                        .enumerate()
489                        .filter_map(|(index, remote)| {
490                            let target = CpuId::new(index as u32);
491                            if target == source
492                                || !remote.accepts_placement()
493                                || !remote.is_scheduler_ready()
494                                || !sched.affinity.affinity.contains(target)
495                            {
496                                return None;
497                            }
498                            let target_demand = remote.placement_demand();
499                            fair_migration_imbalance(source_demand, target_demand, candidate_demand)
500                                .map(|imbalance| (imbalance, target_demand, target))
501                        })
502                        .min_by_key(|(imbalance, demand, target)| {
503                            (*imbalance, *demand, target.as_u32())
504                        })
505                        .map(|(_, _, target)| target)
506                },
507            );
508            if let Some(selection) = selection {
509                match self.commit_owner_balance_transfer(cpu.as_mut(), selection)? {
510                    BalanceTransferOutcome::Migrated(thread) => FairBalanceResult::Migrated(thread),
511                    BalanceTransferOutcome::NoCandidate | BalanceTransferOutcome::Retry => {
512                        FairBalanceResult::Constrained
513                    }
514                }
515            } else if lower_load_target_seen {
516                FairBalanceResult::Constrained
517            } else {
518                FairBalanceResult::Balanced
519            }
520        };
521        // Linux records a completed balance pass from the clock observed at
522        // the end of the pass (`sd->last_balance = jiffies`). Do not reuse the
523        // entry sample: a long owner-side scan would otherwise publish an
524        // already-expired retry deadline.
525        let completion_now = task_runtime::monotonic_now();
526        let minimum_interval_ns = self.config.balance_interval_ns();
527        match result {
528            FairBalanceResult::Migrated(_) => {
529                cpu.as_mut()
530                    .reset_fair_balance(completion_now, minimum_interval_ns);
531            }
532            FairBalanceResult::Balanced => {
533                cpu.as_mut().backoff_fair_balance(
534                    completion_now,
535                    minimum_interval_ns,
536                    minimum_interval_ns.saturating_mul(FAIR_BALANCE_BALANCED_BACKOFF_FACTOR),
537                );
538            }
539            FairBalanceResult::Constrained => {
540                cpu.as_mut().backoff_fair_balance(
541                    completion_now,
542                    minimum_interval_ns,
543                    minimum_interval_ns.saturating_mul(FAIR_BALANCE_CONSTRAINED_BACKOFF_FACTOR),
544                );
545            }
546        }
547        Ok(result.migrated())
548    }
549}
550
551const fn rt_deadline_balance_work_pending(push_target_pending: bool) -> bool {
552    push_target_pending
553}
554
555/// Returns the root-domain push iterator class owning one policy's pushes.
556pub(in crate::sched::system::task_system) const fn push_class_for_policy(
557    policy: SchedulePolicy,
558) -> Option<RootDomainPushClass> {
559    match policy {
560        SchedulePolicy::Fifo { .. } | SchedulePolicy::RoundRobin { .. } => {
561            Some(RootDomainPushClass::Realtime)
562        }
563        SchedulePolicy::Deadline(_) => Some(RootDomainPushClass::Deadline),
564        SchedulePolicy::Fair { .. } | SchedulePolicy::KernelStop => None,
565    }
566}