Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 42 additions & 11 deletions os/StarryOS/kernel/src/task/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,23 +206,54 @@ pub fn wait_existing_ptrace_stop_current(thr: &Thread, uctx: &mut UserContext) {
}

fn wait_ptrace_resume(thr: &Thread, tid: u32, uctx: &mut UserContext) {
current().clear_interrupt();
let wait_result = block_on(interruptible(poll_fn(|cx| {
if thr.proc_data.ptrace_stop_signo_for(tid).is_none() {
Poll::Ready(())
} else {
thr.proc_data.register_ptrace_stop_waker(cx.waker());
loop {
current().clear_interrupt();
let wait_result = block_on(interruptible(poll_fn(|cx| {
if thr.proc_data.ptrace_stop_signo_for(tid).is_none() {
Poll::Ready(())
} else {
Poll::Pending
thr.proc_data.register_ptrace_stop_waker(cx.waker());
if thr.proc_data.ptrace_stop_signo_for(tid).is_none() {
Poll::Ready(())
} else {
Poll::Pending
}
}
})));

if wait_result.is_ok() {
// The tracer resumed us: the stop record's signal was cleared
// (`resume_ptrace_stop_with_signal_for`) or the whole record was
// removed (`clear_ptrace_stop`, e.g. on SIGKILL/detach).
break;
}
})));

if wait_result.is_err() {
thr.proc_data.clear_ptrace_stop();
} else if let Some(resume_uctx) = thr.proc_data.take_ptrace_stop_user_context_for(tid) {
// Interrupted while the trace-stop is still active. A TASK_TRACED
// tracee only leaves the stop for a tracer resume (the Ok path above)
// or to die. Under boot-core serialization the tracee always parked
// before the tracer ran, so no interrupt raced its stop entry; once
// tasks are distributed across CPUs the tracer (and its SIGCHLD /
// ptrace activity) runs concurrently and a benign `interrupt()` can
// land on the tracee as it enters the stop. Aborting the stop on such
// a benign interrupt silently swallowed the stop signal and let the
// tracee run on — that is the race that made a freshly-TRACEME'd child
// skip its `raise(SIGSTOP)` trace-stop and run to `_exit`. Only abandon
// the stop when the tracee genuinely must leave it: a pending
// group-exit, an execve zap, a pending SIGKILL, or a stop record a
// concurrent tracer/kill already cleared. Otherwise stay stopped and
// re-park (Linux keeps a ptrace-stopped task stopped for non-fatal
// signals).
if thr.pending_exit()
|| thr.has_exit_request()
|| thr.signal.pending().has(Signo::SIGKILL)
|| thr.proc_data.ptrace_stop_signo_for(tid).is_none()
{
thr.proc_data.clear_ptrace_stop();
return;
}
}

if let Some(resume_uctx) = thr.proc_data.take_ptrace_stop_user_context_for(tid) {
*uctx = resume_uctx;
thr.proc_data.restore_current_fp_for_ptrace(tid, uctx);
}
Expand Down
212 changes: 196 additions & 16 deletions os/arceos/modules/axtask/src/run_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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()

Copy link
Copy Markdown
Contributor

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/迁移交错的回归测试。

}

/// Retrieves a `'static` reference to the run queue corresponding to the given index.
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

阻塞(测试覆盖):这里改变了所有未显式绑定任务的 spawn/clone 投放契约,但新增的 online_bitmap_tests 只检查位图与选择器返回值,不能执行真实的 TaskInner 入队、跨 CPU 首次运行或唤醒回投路径。现有 arceos-affinity 只在任务启动后显式设置 affinity,即使保留 base 的“总是投当前 CPU”实现也能通过,因而没有覆盖本 PR 的性能/正确性主张。请在 test-suit/arceos/rust 的 SMP4 QEMU 回归中新增确定性场景:创建多个默认完整 affinity 的任务,记录首次 this_cpu_id(),再经 barrier/唤醒记录运行 CPU,并断言任务实际分布且不会在唤醒后全部收敛;该测试必须在 base 的当前 CPU 投放策略下失败、在本实现下通过。

AxRunQueueRef {
inner: get_run_queue(index),
state: irq_state,
Expand Down Expand Up @@ -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)
};
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,16 @@ static void test_robust_list_bad_chain_does_not_hang(void)
}

int status = 0;
for (int i = 0; i < 2000; i++) {
/*
* This guard only proves the kernel does NOT infinite-loop on a bad/cyclic
* robust list; the exact reap latency is irrelevant. A fixed iteration cap
* (~2s) is fragile under slow aarch64-TCG + real SMP concurrency, so bound
* the wait by a generous wall-clock deadline instead.
*/
struct timespec start;
CHECK(clock_gettime(CLOCK_MONOTONIC, &start) == 0, "clock_gettime start succeeds");
const long deadline_ms = 60000;
for (;;) {
pid_t waited = waitpid(pid, &status, WNOHANG);
if (waited == pid) {
CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0,
Expand All @@ -1069,6 +1078,11 @@ static void test_robust_list_bad_chain_does_not_hang(void)
}
CHECK(waited == 0 || (waited == -1 && errno == EINTR),
"waitpid bad robust-list child is pending or interrupted");
struct timespec now;
if (clock_gettime(CLOCK_MONOTONIC, &now) == 0 &&
elapsed_ms(&start, &now) > deadline_ms) {
break;
}
const struct timespec pause = {
.tv_sec = 0,
.tv_nsec = 1000 * 1000,
Expand Down
Loading
Loading