-
Notifications
You must be signed in to change notification settings - Fork 126
fix(axtask): distribute new and woken threads across CPUs for SMP scaling #1656
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 13 commits
ff6c3e2
6735186
7ae1ac9
403b922
0e417db
7e239d6
b50b8d9
3246f3f
65762fc
3d5e803
724b8e3
d9d9912
91eb07c
5bd2dd3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,6 +58,47 @@ static mut RUN_QUEUES: [MaybeUninit<&'static mut AxRunQueue>; crate::build_info: | |
| #[allow(clippy::declare_interior_mutable_const)] // It's ok because it's used only for initialization `RUN_QUEUES`. | ||
| const ARRAY_REPEAT_VALUE: MaybeUninit<&'static mut AxRunQueue> = MaybeUninit::uninit(); | ||
|
|
||
| /// Number of `usize` words needed to hold one online-bit per CPU, up to | ||
| /// `CPU_CAPACITY`. `CPU_CAPACITY` (from `build.rs`, the `SMP` env var) can | ||
| /// exceed `usize::BITS` — `AxCpuMask` supports up to 1024 CPUs — so the | ||
| /// online bitmap is sharded across multiple words instead of a single | ||
| /// `AtomicUsize`, which would overflow/alias `1 << cpu_id` for `cpu_id >= | ||
| /// usize::BITS` (e.g. CPU 64 would alias CPU 0 on a 64-bit target). | ||
| #[cfg(feature = "smp")] | ||
| const RUN_QUEUE_ONLINE_WORDS: usize = | ||
| crate::build_info::CPU_CAPACITY.div_ceil(usize::BITS as usize); | ||
|
|
||
| /// Bitmask (bit `cpu_id % usize::BITS`, word `cpu_id / usize::BITS`) of CPUs | ||
| /// whose per-CPU run queue in [`RUN_QUEUES`] has been initialized. | ||
| /// Round-robin spawn placement ([`select_run_queue_index`]) must only target | ||
| /// online queues: during early boot the secondaries have not yet written | ||
| /// their `RUN_QUEUES` slot, and `get_run_queue` on an uninitialized slot is a | ||
| /// use of uninitialized memory (a near-null data abort). Set by each CPU as | ||
| /// it initializes (see `init` / `init_secondary`), through [`mark_cpu_online`]. | ||
| #[cfg(feature = "smp")] | ||
| static RUN_QUEUE_ONLINE: [core::sync::atomic::AtomicUsize; RUN_QUEUE_ONLINE_WORDS] = | ||
| [const { core::sync::atomic::AtomicUsize::new(0) }; RUN_QUEUE_ONLINE_WORDS]; | ||
|
|
||
| /// Marks `cpu`'s run queue as online in the sharded [`RUN_QUEUE_ONLINE`] bitmap. | ||
| #[cfg(feature = "smp")] | ||
| #[inline] | ||
| fn mark_cpu_online(cpu: usize) { | ||
| RUN_QUEUE_ONLINE[cpu / usize::BITS as usize].fetch_or( | ||
| 1 << (cpu % usize::BITS as usize), | ||
| core::sync::atomic::Ordering::Release, | ||
| ); | ||
| } | ||
|
|
||
| /// Returns whether `cpu`'s run queue in [`RUN_QUEUES`] has been initialized. | ||
| #[cfg(feature = "smp")] | ||
| #[inline] | ||
| fn is_cpu_online(cpu: usize) -> bool { | ||
| cpu < crate::build_info::CPU_CAPACITY | ||
| && RUN_QUEUE_ONLINE[cpu / usize::BITS as usize].load(core::sync::atomic::Ordering::Acquire) | ||
| & (1 << (cpu % usize::BITS as usize)) | ||
| != 0 | ||
| } | ||
|
|
||
| #[cfg(not(feature = "host-test"))] | ||
| fn main_task_stack() -> TaskStack { | ||
| let (stack_ptr, stack_size) = ax_hal::mem::boot_stack_bounds(this_cpu_id()); | ||
|
|
@@ -118,14 +159,30 @@ fn select_run_queue_index(cpumask: AxCpuMask) -> usize { | |
|
|
||
| assert!(!cpumask.is_empty(), "No available CPU for task execution"); | ||
|
|
||
| // Round-robin selection of the run queue index. | ||
| loop { | ||
| // Round-robin over CPUs that are both allowed by the affinity mask AND online | ||
| // (their run queue is initialized). The online gate matters during early boot, | ||
| // when secondaries have not yet registered their run queue — targeting one | ||
| // would dereference uninitialized memory in `get_run_queue`. | ||
| for _ in 0..crate::build_info::CPU_CAPACITY { | ||
| let index = | ||
| RUN_QUEUE_INDEX.fetch_add(1, Ordering::SeqCst) % crate::build_info::CPU_CAPACITY; | ||
| if cpumask.get(index) { | ||
| if cpumask.get(index) && is_cpu_online(index) { | ||
| return index; | ||
| } | ||
| } | ||
| // No allowed-and-online CPU in a full sweep: either very early boot, or a task | ||
| // pinned to a not-yet-online CPU. Fall back to the current CPU, whose run queue | ||
| // is necessarily initialized. This is availability-over-affinity, matching | ||
| // Linux's `select_fallback_rq`: the scheduler runs the task on an available CPU | ||
| // rather than block when its affinity target is offline, which avoids a | ||
| // boot-time deadlock (a permanently-blocked task on the only CPU that could | ||
| // later bring its affinity target online). Affinity is then restored by | ||
| // `migrate_current_to_affinity` — the pending-migration equivalent — which | ||
| // re-homes the task to an allowed CPU on the next reschedule once one is | ||
| // online. `AxCpuMask` is bounded to `CPU_CAPACITY`, so a non-empty mask always | ||
| // names a configured CPU that comes online during boot; the fallback never | ||
| // permanently strands a task off its affinity. | ||
| this_cpu_id() | ||
| } | ||
|
|
||
| /// Retrieves a `'static` reference to the run queue corresponding to the given index. | ||
|
|
@@ -378,6 +435,93 @@ mod tests { | |
| } | ||
| } | ||
|
|
||
| #[cfg(all(test, feature = "smp", feature = "host-test"))] | ||
| mod online_bitmap_tests { | ||
| use core::sync::atomic::Ordering; | ||
|
|
||
| // Reset every word of the shared bitmap so tests do not observe state left | ||
| // behind by `init`/`init_secondary` or by other tests in this process. | ||
| fn reset_online_bitmap() { | ||
| for word in super::RUN_QUEUE_ONLINE.iter() { | ||
| word.store(0, Ordering::Release); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn mark_cpu_online_is_visible_only_for_marked_cpu() { | ||
| reset_online_bitmap(); | ||
|
|
||
| super::mark_cpu_online(0); | ||
|
|
||
| assert!( | ||
| super::is_cpu_online(0), | ||
| "a CPU marked online must report itself as online", | ||
| ); | ||
| assert!( | ||
| !super::is_cpu_online(1), | ||
| "marking one CPU online must not mark an unrelated CPU online", | ||
| ); | ||
|
|
||
| reset_online_bitmap(); | ||
| } | ||
|
|
||
| // The reviewer explicitly asked for coverage across the word boundary a | ||
| // single-word `AtomicUsize` bitmap could not represent (CPU_CAPACITY >= | ||
| // usize::BITS, e.g. SMP=65): CPU 64 must not alias CPU 0. This only | ||
| // exercises a second word when the test build's `CPU_CAPACITY` exceeds | ||
| // `usize::BITS`; it is a no-op assertion of intent otherwise, and the | ||
| // real cross-word coverage is the `SMP=65` build check (see PR | ||
| // discussion / commit message). | ||
| #[test] | ||
| fn cross_word_cpu_does_not_alias_low_word() { | ||
| reset_online_bitmap(); | ||
|
|
||
| super::mark_cpu_online(0); | ||
|
|
||
| if crate::build_info::CPU_CAPACITY > usize::BITS as usize { | ||
| assert!( | ||
| super::is_cpu_online(0), | ||
| "CPU 0 must remain online after marking it", | ||
| ); | ||
| assert!( | ||
| !super::is_cpu_online(64), | ||
| "CPU 64 must not alias CPU 0's bit in a sharded bitmap", | ||
| ); | ||
| } | ||
|
|
||
| reset_online_bitmap(); | ||
| } | ||
|
|
||
| // Boot-time affinity/migration interleave (the reviewer's requested case): a | ||
| // task whose only allowed CPU is still offline must fall back to an online CPU | ||
| // (availability over affinity, like Linux `select_fallback_rq`) — never a hang | ||
| // and never an offline/uninitialized queue — and must honor the affinity once | ||
| // that CPU comes online. | ||
| #[test] | ||
| fn offline_affinity_target_falls_back_then_honors_when_online() { | ||
| reset_online_bitmap(); | ||
| let this = super::this_cpu_id(); | ||
| super::mark_cpu_online(this); | ||
|
|
||
| // An allowed CPU distinct from `this`, kept offline for now. | ||
| let other = if this == 1 { 2 } else { 1 }; | ||
| if other >= crate::build_info::CPU_CAPACITY { | ||
| reset_online_bitmap(); | ||
| return; // too few configured CPUs in this test build to exercise it | ||
| } | ||
| let only_other = super::AxCpuMask::one_shot(other); | ||
|
|
||
| // Allowed CPU offline -> availability fallback to the current (online) CPU. | ||
| assert_eq!(super::select_run_queue_index(only_other), this); | ||
|
|
||
| // Allowed CPU online -> affinity is honored. | ||
| super::mark_cpu_online(other); | ||
| assert_eq!(super::select_run_queue_index(only_other), other); | ||
|
|
||
| reset_online_bitmap(); | ||
| } | ||
| } | ||
|
|
||
| #[cfg(all(test, feature = "sched-rr", feature = "host-test"))] | ||
| mod rr_tests { | ||
| use alloc::{string::String, sync::Arc}; | ||
|
|
@@ -455,14 +599,16 @@ pub(crate) fn select_run_queue<G: BaseGuard>(task: &AxTaskRef) -> AxRunQueueRef< | |
| } | ||
| #[cfg(feature = "smp")] | ||
| { | ||
| // When SMP is enabled, prefer the current CPU to keep the task's | ||
| // cache warm. Fall back to round-robin only when affinity forbids it. | ||
| let current_cpu = this_cpu_id(); | ||
| let index = if task.cpumask().get(current_cpu) { | ||
| current_cpu | ||
| } else { | ||
| select_run_queue_index(task.cpumask()) | ||
| }; | ||
| // New tasks (spawn / clone) get round-robin placement across the CPUs | ||
| // their affinity allows, so a burst of threads spreads over the machine | ||
| // instead of piling onto the core that ran the spawning syscall. A fresh | ||
| // task has no warm cache to preserve, and with no load balancer to | ||
| // redistribute later (see the TODO above), initial placement is the only | ||
| // spreading we get — pinning every clone to the current CPU left all of a | ||
| // process's threads on the boot core (flat multi-core scaling). Wakeups | ||
| // still prefer the waking/last CPU for cache warmth; see | ||
| // `select_wake_run_queue`. | ||
| let index = select_run_queue_index(task.cpumask()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 阻塞(测试覆盖):这里改变了所有未显式绑定任务的 spawn/clone 投放契约,但新增的 |
||
| AxRunQueueRef { | ||
| inner: get_run_queue(index), | ||
| state: irq_state, | ||
|
|
@@ -494,10 +640,24 @@ pub(crate) fn select_wake_run_queue<G: BaseGuard>(task: &AxTaskRef) -> AxRunQueu | |
| let current_cpu = this_cpu_id(); | ||
| let last_cpu = task.cpu_id() as usize; | ||
| let cpumask = task.cpumask(); | ||
| let index = if cpumask.get(current_cpu) { | ||
| current_cpu | ||
| } else if last_cpu < crate::build_info::CPU_CAPACITY && cpumask.get(last_cpu) { | ||
| // Prefer the CPU the woken task last ran on. This is cache-warm for the | ||
| // *woken* task and, crucially, keeps threads spread out: preferring the | ||
| // *waker's* CPU (as before) piled every worker onto one core whenever a | ||
| // barrier/broadcast wake fanned out from a single thread — that | ||
| // re-collapsed the round-robin spawn placement and was why multi-threaded | ||
| // workloads stayed on the boot core. Fall back to the waker's CPU, then | ||
| // round-robin; only ever select an online CPU. | ||
| // | ||
| // `last_cpu` is checked against `CPU_CAPACITY` before indexing into | ||
| // `cpumask`: `AxCpuMask::get` only `debug_assert`s in-range, so an | ||
| // out-of-range `last_cpu` must be filtered here first. | ||
| let index = if last_cpu < crate::build_info::CPU_CAPACITY | ||
| && cpumask.get(last_cpu) | ||
| && is_cpu_online(last_cpu) | ||
| { | ||
| last_cpu | ||
| } else if cpumask.get(current_cpu) { | ||
| current_cpu | ||
| } else { | ||
| select_run_queue_index(cpumask) | ||
| }; | ||
|
|
@@ -568,8 +728,17 @@ impl<G: BaseGuard> AxRunQueueRef<'_, G> { | |
| #[cfg(feature = "smp")] | ||
| task.set_cpu_id(cpu_id as _); | ||
| self.inner.scheduler.lock().add_task(task); | ||
| // A newly runnable task placed on a remote CPU may be one the spawner | ||
| // must see make progress (e.g. `fork` then `waitpid`, or a broadcast | ||
| // wake fanning a barrier out to idle CPUs). If that CPU is idle in | ||
| // `wait_for_irqs`, only the IPI wakes it — so do NOT let a stale | ||
| // coalescing bit suppress the kick, exactly as `migrate_entry` does. | ||
| // Coalescing here can otherwise drop the wake for a task that lands in | ||
| // the window after the target's in-flight reschedule already read its | ||
| // run queue, stranding it until the next tick or (tickless idle) | ||
| // indefinitely — the riscv64 SMP `test-fcntl-lock-lifecycle` hang. | ||
| #[cfg(all(feature = "smp", feature = "ipi"))] | ||
| kick_remote_cpu(cpu_id); | ||
| force_kick_remote_cpu(cpu_id); | ||
| } | ||
|
|
||
| /// Unblock one task by inserting it into the run queue. | ||
|
|
@@ -603,8 +772,13 @@ impl<G: BaseGuard> AxRunQueueRef<'_, G> { | |
| #[cfg(feature = "preempt")] | ||
| crate::current().set_preempt_pending(true); | ||
| } | ||
| // Cross-core wake: the woken task must actually run (a waiter someone | ||
| // is blocked on — e.g. an `F_SETLKW` parked task woken when the lock | ||
| // is released). If its target CPU is idle in `wait_for_irqs`, only the | ||
| // IPI wakes it, so force the kick rather than let a stale coalescing | ||
| // bit drop it (same rationale as `migrate_entry` / `add_task`). | ||
| #[cfg(all(feature = "smp", feature = "ipi"))] | ||
| kick_remote_cpu(cpu_id); | ||
| force_kick_remote_cpu(cpu_id); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -1217,6 +1391,9 @@ pub(crate) fn init() { | |
| unsafe { | ||
| RUN_QUEUES[cpu_id].write(RUN_QUEUE.current_ref_mut_raw()); | ||
| } | ||
| // Mark this CPU's run queue online so round-robin spawn placement may target it. | ||
| #[cfg(feature = "smp")] | ||
| mark_cpu_online(cpu_id); | ||
| } | ||
|
|
||
| pub(crate) fn init_secondary(stack_ptr: VirtAddr, stack_size: usize) { | ||
|
|
@@ -1240,4 +1417,7 @@ pub(crate) fn init_secondary(stack_ptr: VirtAddr, stack_size: usize) { | |
| unsafe { | ||
| RUN_QUEUES[cpu_id].write(RUN_QUEUE.current_ref_mut_raw()); | ||
| } | ||
| // Mark this CPU's run queue online so round-robin spawn placement may target it. | ||
| #[cfg(feature = "smp")] | ||
| mark_cpu_online(cpu_id); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
阻塞(正确性 / affinity):这里在完整扫描后返回
this_cpu_id()会突破cpumask。例如 CPU0 上的线程经sched_setaffinity()被限制为仅 CPU1 后立刻迁移,而 CPU1 尚未执行init_secondary():migrate_entry()调用本选择器后会把该线程重新入队 CPU0;任务首次被调度时task_entry()会直接执行用户 closure,只有之后主动 yield 或发生 preempt 才会检查并迁移,因而用户态已在不允许的 CPU 上运行。RUN_QUEUE_ONLINE的目的正是避免未初始化队列,不能以静默违反 affinity 作为替代。请让“没有 allowed-and-online CPU”成为显式结果,并延后投递/迁移到允许队列上线后;同时增加覆盖该启动期sched_setaffinity/迁移交错的回归测试。