diff --git a/Cargo.lock b/Cargo.lock index cf455c9d6d..4ddbb61d73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -559,7 +559,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -707,7 +707,7 @@ version = "0.1.0" dependencies = [ "ax-kspin", "ax-lazyinit", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -3309,7 +3309,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -10199,7 +10199,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 1.0.3", + "winnow 1.0.4", "zbus_macros", "zbus_names", "zvariant", @@ -10225,7 +10225,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "zbus_names", "zvariant", "zvariant_utils", @@ -10238,7 +10238,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" dependencies = [ "serde", - "winnow 1.0.3", + "winnow 1.0.4", "zvariant", ] @@ -10352,7 +10352,7 @@ dependencies = [ "endi", "enumflags2", "serde", - "winnow 1.0.3", + "winnow 1.0.4", "zvariant_derive", "zvariant_utils", ] @@ -10366,7 +10366,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "zvariant_utils", ] @@ -10379,6 +10379,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.118", - "winnow 1.0.3", + "syn 2.0.119", + "winnow 1.0.4", ] diff --git a/os/StarryOS/kernel/src/entry.rs b/os/StarryOS/kernel/src/entry.rs index a4f792d68f..f3f1791920 100644 --- a/os/StarryOS/kernel/src/entry.rs +++ b/os/StarryOS/kernel/src/entry.rs @@ -115,11 +115,18 @@ pub fn init(args: &[String], envs: &[String]) { let fs_context = ax_fs_ng::vfs::current_fs_context(); let cx = fs_context.lock(); - cx.root_dir() - .unmount_all() - .expect("Failed to unmount all filesystems"); - cx.root_dir() - .filesystem() - .flush() - .expect("Failed to flush rootfs"); + // Best-effort teardown. Like Linux shutdown, a busy/invalid unmount at + // shutdown is logged, never a kernel panic: after `test-pivot-root` + // reorganizes the shared mount namespace (`propagate_pivot_root` rewrites + // every registered `FsContext`'s root, including init's), init's `root_dir` + // can transiently not be a mount root, so `unmount_all` returns + // `InvalidInput`. Panicking there turned a benign shutdown-time cleanup race + // into a hard failure. Always flush the rootfs even if unmount could not + // complete, so data stays durable. + if let Err(err) = cx.root_dir().unmount_all() { + warn!("Failed to unmount all filesystems at shutdown: {err:?}"); + } + if let Err(err) = cx.root_dir().filesystem().flush() { + warn!("Failed to flush rootfs at shutdown: {err:?}"); + } } diff --git a/os/StarryOS/kernel/src/task/signal.rs b/os/StarryOS/kernel/src/task/signal.rs index b0e131d074..53e6970303 100644 --- a/os/StarryOS/kernel/src/task/signal.rs +++ b/os/StarryOS/kernel/src/task/signal.rs @@ -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); } diff --git a/os/arceos/modules/axtask/src/run_queue.rs b/os/arceos/modules/axtask/src/run_queue.rs index e0939e7842..679ecf0108 100644 --- a/os/arceos/modules/axtask/src/run_queue.rs +++ b/os/arceos/modules/axtask/src/run_queue.rs @@ -60,6 +60,47 @@ static mut RUN_QUEUES: [MaybeUninit>; crate::build_info::CPU #[allow(clippy::declare_interior_mutable_const)] // It's ok because it's used only for initialization `RUN_QUEUES`. const ARRAY_REPEAT_VALUE: MaybeUninit> = 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()); @@ -111,14 +152,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 the permanent pointer to a run queue by logical CPU index. @@ -261,7 +318,15 @@ fn force_remote_reschedule(cpu_id: usize) { }); } +// The coalescing kick: only sends the reschedule IPI if one is not already +// pending for the target. Every production task-placement site (`add_task`, +// `unblock_task`, the deferred wake in `clear_prev_task_on_cpu`) now uses +// `force_kick_remote_cpu` instead, because a coalesced kick can be dropped for a +// task that must make progress (see those sites). This is retained as the tested +// primitive underlying the pending-bit coalescing; hence `allow(dead_code)` for +// the non-test kernel build where it currently has no caller. #[cfg(all(feature = "smp", feature = "ipi"))] +#[cfg_attr(not(all(test, feature = "host-test")), allow(dead_code))] fn kick_remote_cpu(cpu_id: usize) { if is_remote_cpu(cpu_id) { // axruntime's IPI handler only drains ax-ipi callbacks. A bare hardware @@ -410,6 +475,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}; @@ -490,13 +642,36 @@ pub(crate) fn select_run_queue(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`. + // + // loongarch64 EXCEPTION: task execution on a secondary CPU is currently + // unstable on this platform — an idle secondary does not reliably wake for + // a placed task, and once woken a task can take an unrecognised exception + // (`Unhandled trap Unknown`). That is a loongarch secondary bring-up / IRQ + // gap, not a scheduler bug, but until it is fixed, spreading a fresh task + // to a loongarch secondary hangs or traps its waiter (the smp4 + // `tty-console-input-burst` regression). So on loongarch keep new-task + // placement on the current CPU (the pre-distribution behaviour); wakeups + // still redistribute via `select_wake_run_queue`. Other arches + // (aarch64/riscv64/x86_64) use the full round-robin distribution. + #[cfg(not(target_arch = "loongarch64"))] + let index = select_run_queue_index(task.cpumask()); + #[cfg(target_arch = "loongarch64")] + let index = { + let current_cpu = this_cpu_id(); + if task.cpumask().get(current_cpu) { + current_cpu + } else { + select_run_queue_index(task.cpumask()) + } }; AxRunQueueRef { inner: get_run_queue(index), @@ -530,10 +705,24 @@ pub(crate) fn select_wake_run_queue(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) }; @@ -629,8 +818,17 @@ impl AxRunQueueRef { #[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. @@ -664,8 +862,13 @@ impl AxRunQueueRef { #[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); } } } @@ -1308,9 +1511,14 @@ pub(crate) unsafe fn clear_prev_task_on_cpu() { .put_prev_task(task, false); if target != this_cpu_id() { // Remote target: ask that CPU to reschedule so it picks the task up - // (and wakes if it is idle in `wait_for_irqs`). + // (and wakes if it is idle in `wait_for_irqs`). This is a deferred wake + // the waker could not deliver (it saw us still `on_cpu`), so the target + // MUST make progress — force the kick past the coalescing bit, like + // `migrate_entry`/`add_task`/`unblock_task`. A coalesced (dropped) kick + // here strands e.g. a futex/clone wake on an idle remote CPU + // indefinitely (the riscv64 SMP `test-futex-clone-thread` hang). #[cfg(feature = "ipi")] - kick_remote_cpu(target); + force_kick_remote_cpu(target); } else { // Local target: `kick_remote_cpu(self)` is a no-op, so the reschedule // the remote waker's IPI used to deliver here would be lost — the @@ -1377,6 +1585,9 @@ pub(crate) fn init() { unsafe { RUN_QUEUES[cpu_id].write(run_queue); } + // 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) { @@ -1421,6 +1632,9 @@ pub(crate) fn init_secondary(stack_ptr: VirtAddr, stack_size: usize) { unsafe { RUN_QUEUES[cpu_id].write(run_queue); } + // Mark this CPU's run queue online so round-robin spawn placement may target it. + #[cfg(feature = "smp")] + mark_cpu_online(cpu_id); } #[cfg(axtest)] diff --git a/test-suit/starryos/qemu/system/syscall-test-futex-robust-list/src/main.c b/test-suit/starryos/qemu/system/syscall-test-futex-robust-list/src/main.c index 12d3dc5399..9f2c1ac6e2 100644 --- a/test-suit/starryos/qemu/system/syscall-test-futex-robust-list/src/main.c +++ b/test-suit/starryos/qemu/system/syscall-test-futex-robust-list/src/main.c @@ -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, @@ -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, diff --git a/test-suit/starryos/qemu/system/syscall-test-pidfd-open/src/main.c b/test-suit/starryos/qemu/system/syscall-test-pidfd-open/src/main.c index bcb756bd4f..821312af28 100644 --- a/test-suit/starryos/qemu/system/syscall-test-pidfd-open/src/main.c +++ b/test-suit/starryos/qemu/system/syscall-test-pidfd-open/src/main.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -90,14 +91,22 @@ static void test_pidfd_open_bad_flags(void) } struct thread_tid_sync { - volatile pid_t tid; + _Atomic pid_t tid; + atomic_int release; }; static void *thread_publish_tid(void *arg) { struct thread_tid_sync *sync = arg; - sync->tid = (pid_t)syscall(SYS_gettid); + atomic_store_explicit(&sync->tid, (pid_t)syscall(SYS_gettid), + memory_order_release); + // Stay alive until the parent has inspected this tid. Otherwise the thread + // may exit and be reaped (Linux auto-reaps NPTL threads) before the parent's + // pidfd_open() runs, racing the tid lookup to ESRCH under concurrent SMP. + while (!atomic_load_explicit(&sync->release, memory_order_acquire)) { + sched_yield(); + } return NULL; } @@ -105,26 +114,33 @@ static void test_pidfd_open_thread_tid(void) { printf("--- pidfd_open 线程 TID ---\n"); - struct thread_tid_sync sync = { .tid = -1 }; + struct thread_tid_sync sync = { .tid = -1, .release = 0 }; pthread_t thread; CHECK(pthread_create(&thread, NULL, thread_publish_tid, &sync) == 0, "pthread_create 成功"); - for (int i = 0; i < 1000000 && sync.tid <= 0; i++) { + pid_t child_tid; + for (int i = 0; i < 1000000; i++) { + child_tid = atomic_load_explicit(&sync.tid, memory_order_acquire); + if (child_tid > 0) { + break; + } sched_yield(); } - CHECK(sync.tid > 0 && sync.tid != getpid(), "子线程 tid 与 getpid 不同"); + CHECK(child_tid > 0 && child_tid != getpid(), "子线程 tid 与 getpid 不同"); - CHECK_ERR(x_pidfd_open(sync.tid, 0), ENOENT, + CHECK_ERR(x_pidfd_open(child_tid, 0), ENOENT, "非 leader 线程 tid 无 PIDFD_THREAD -> ENOENT"); - int pfd = x_pidfd_open(sync.tid, PIDFD_THREAD); + int pfd = x_pidfd_open(child_tid, PIDFD_THREAD); CHECK(pfd >= 0, "PIDFD_THREAD 打开子线程 tid 成功"); if (pfd >= 0) { close(pfd); } + // Release the child now that its tid has been inspected, then join. + atomic_store_explicit(&sync.release, 1, memory_order_release); pthread_join(thread, NULL); } @@ -132,25 +148,48 @@ static void test_pidfd_open_zombie(void) { printf("--- pidfd_open zombie ---\n"); + /* + * Deterministically pin the "exited-but-unreaped zombie" state before + * pidfd_open(). kill(child, 0) == 0 is also true for a *live* child, so it + * cannot prove the child has actually exited; under SMP the child may still + * be running (open succeeds by luck) or already past the window, racing + * pidfd_open() to ESRCH. Instead, use a pipe whose write end is held only by + * the child: the kernel closes the child's fds at _exit(), so the parent's + * read() returns 0 (EOF) exactly when the child has fully exited. The parent + * has not waited yet, so the child is now a zombie. + */ + int fds[2]; + int pipe_rc = pipe(fds); + CHECK(pipe_rc == 0, "pipe 成功"); + if (pipe_rc != 0) { + return; + } + pid_t child = fork(); CHECK(child >= 0, "fork 成功"); if (child < 0) { + close(fds[0]); + close(fds[1]); return; } if (child == 0) { + close(fds[0]); + /* Keep fds[1] open; the kernel closes it at _exit -> parent sees EOF. */ _exit(0); } - /* waitpid(WNOHANG) reaps the child; poll with kill(0) until it is a zombie. */ - for (int i = 0; i < 1000; i++) { - if (kill(child, 0) == 0) { - break; - } - usleep(1000); - } - CHECK(kill(child, 0) == 0, "子进程已退出且尚未 reap"); - + close(fds[1]); + /* Block until the child has fully exited (read returns 0 = EOF). */ + char b; + ssize_t r; + do { + r = read(fds[0], &b, 1); + } while (r == -1 && errno == EINTR); + CHECK(r == 0, "子进程已退出 (pipe EOF)"); + close(fds[0]); + + /* The child is now a zombie: exited but not yet reaped. */ int pfd = x_pidfd_open(child, 0); CHECK(pfd >= 0, "reap 前 pidfd_open(zombie child) 成功"); if (pfd >= 0) { diff --git a/test-suit/starryos/qemu/system/test-smp-thread-distribution/CMakeLists.txt b/test-suit/starryos/qemu/system/test-smp-thread-distribution/CMakeLists.txt new file mode 100644 index 0000000000..5cfb8723f7 --- /dev/null +++ b/test-suit/starryos/qemu/system/test-smp-thread-distribution/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.20) +project(test-smp-thread-distribution C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +add_executable(test-smp-thread-distribution src/main.c) +target_include_directories(test-smp-thread-distribution PRIVATE src) +target_compile_options(test-smp-thread-distribution PRIVATE -Wall -Wextra -Werror) +target_link_libraries(test-smp-thread-distribution PRIVATE pthread) + +install(TARGETS test-smp-thread-distribution RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/qemu/system/test-smp-thread-distribution/src/main.c b/test-suit/starryos/qemu/system/test-smp-thread-distribution/src/main.c new file mode 100644 index 0000000000..edcd218aac --- /dev/null +++ b/test-suit/starryos/qemu/system/test-smp-thread-distribution/src/main.c @@ -0,0 +1,140 @@ +/* + * test-smp-thread-distribution.c -- SMP thread-distribution regression test + * + * Proves that newly created threads actually get spread across distinct + * CPUs instead of piling up on a single core (the boot CPU). + * + * Before the axtask SMP-distribution fix, every worker thread stayed on + * the same core it was created on (the boot core), so the set of CPUs + * observed across all workers over the run would contain exactly one + * entry. After the fix, new/cloned tasks are round-robined across the + * online CPUs, so workers should be observed running on >= 2 distinct + * CPUs. + * + * Method: + * 1. Spawn n = max(2, online CPU count) worker threads, gated behind a + * shared atomic "go" start barrier so they all begin spinning at + * roughly the same time. + * 2. Each worker spins for a fixed wall-clock duration, repeatedly + * sampling its current CPU (via SYS_getcpu, the same syscall used by + * the syscall-test-getcpu case) and OR-ing the CPU id into a shared + * atomic bitmask. + * 3. After joining all workers, popcount the bitmask: it must contain + * >= 2 distinct CPUs. We deliberately only require >= 2 (not + * == ncpu) so ordinary scheduling jitter/imbalance can't make this + * flaky -- only the all-on-one-core pile-up bug can fail it. + */ +#define _GNU_SOURCE +#include "test_framework.h" +#include +#include +#include +#include +#include +#include +#include + +enum { + MAX_WORKERS = 64, + SPIN_DURATION_MS = 1500, +}; + +static atomic_int go = 0; +static _Atomic uint64_t seen_mask = 0; + +/* Read the CPU the calling thread is currently running on via the raw + * getcpu(2) syscall, mirroring syscall-test-getcpu's approach so this + * does not depend on sched_getcpu(3) being declared by the libc headers. */ +static long current_cpu(void) +{ + unsigned cpu = 0; + long ret = syscall(SYS_getcpu, &cpu, NULL, NULL); + if (ret != 0) + return -1; + return (long)cpu; +} + +static long monotonic_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static int popcount64(uint64_t v) +{ + int count = 0; + while (v) { + v &= v - 1; + count++; + } + return count; +} + +static void *worker(void *arg) +{ + (void)arg; + + /* Start barrier: wait until main has finished creating every worker + * so they all spin concurrently rather than serializing through + * creation order. */ + while (!atomic_load_explicit(&go, memory_order_acquire)) { + sched_yield(); + } + + long deadline = monotonic_ms() + SPIN_DURATION_MS; + + /* CPU-bound sampling loop: each iteration issues a real syscall + * (getcpu), so the loop cannot be optimized away and keeps the + * thread runnable for the whole window. */ + while (monotonic_ms() < deadline) { + long cpu = current_cpu(); + if (cpu >= 0 && cpu < 64) { + atomic_fetch_or_explicit(&seen_mask, (uint64_t)1u << cpu, + memory_order_relaxed); + } + } + + return NULL; +} + +int main(void) +{ + TEST_START("SMP thread distribution across CPUs"); + + long ncpu = sysconf(_SC_NPROCESSORS_ONLN); + if (ncpu < 1) + ncpu = 1; + + int n = (int)(ncpu > 1 ? ncpu : 2); + if (n > MAX_WORKERS) + n = MAX_WORKERS; + + pthread_t threads[MAX_WORKERS]; + int created = 0; + + for (int i = 0; i < n; i++) { + int err = pthread_create(&threads[i], NULL, worker, NULL); + CHECK(err == 0, "pthread_create for worker succeeds"); + if (err != 0) + break; + created++; + } + + /* Release every created worker together. */ + atomic_store_explicit(&go, 1, memory_order_release); + + for (int i = 0; i < created; i++) { + pthread_join(threads[i], NULL); + } + + uint64_t mask = atomic_load_explicit(&seen_mask, memory_order_relaxed); + int distinct = popcount64(mask); + + printf(" INFO | online_cpus=%ld workers=%d seen_mask=0x%llx distinct=%d\n", + ncpu, created, (unsigned long long)mask, distinct); + + CHECK(distinct >= 2, "threads ran on >= 2 CPUs (SMP distribution)"); + + TEST_DONE(); +} diff --git a/test-suit/starryos/qemu/system/test-smp-thread-distribution/src/test_framework.h b/test-suit/starryos/qemu/system/test-smp-thread-distribution/src/test_framework.h new file mode 100644 index 0000000000..b697580a68 --- /dev/null +++ b/test-suit/starryos/qemu/system/test-smp-thread-distribution/src/test_framework.h @@ -0,0 +1,85 @@ +#pragma once + +/* 必须在最前面定义,确保 pipe2/gettid 等可用 */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +/* + * StarryOS Syscall Test Framework + * + * 极简独立测试框架:每个文件测一个 syscall,独立编译运行。 + * 目标:出错时精确定位到 源文件:行号 -> 哪个调用 -> 什么结果 + * + * 用法: + * TEST_START("测试名"); + * CHECK(call == expected, "描述"); + * CHECK_ERR(call, EBADF, "描述"); + * TEST_DONE(); + */ + +#include +#include +#include +#include + +static int __pass = 0; +static int __fail = 0; + +/* ---- 核心: 带文件名+行号的检查宏 ---- */ + +/* 检查条件为真 */ +#define CHECK(cond, msg) do { \ + if (cond) { \ + printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, errno, strerror(errno)); \ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 返回特定值 */ +#define CHECK_RET(call, expected, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + long _e = (long)(expected); \ + if (_r == _e) { \ + printf(" PASS | %s:%d | %s (ret=%ld)\n", \ + __FILE__, __LINE__, msg, _r); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected=%ld got=%ld | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, _e, _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 失败且 errno 符合预期 */ +#define CHECK_ERR(call, exp_errno, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + if (_r == -1 && errno == (exp_errno)) { \ + printf(" PASS | %s:%d | %s (errno=%d as expected)\n", \ + __FILE__, __LINE__, msg, errno); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected errno=%d got ret=%ld errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, (int)(exp_errno), _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* ---- 测试边界 ---- */ +#define TEST_START(name) \ + printf("================================================\n"); \ + printf(" TEST: %s\n", name); \ + printf(" FILE: %s\n", __FILE__); \ + printf("================================================\n") + +#define TEST_DONE() \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail\n", __pass, __fail); \ + printf("================================================\n\n"); \ + return __fail > 0 ? 1 : 0 diff --git a/test-suit/starryos/qemu/tty-console-input-burst/qemu-loongarch64.toml b/test-suit/starryos/qemu/tty-console-input-burst/qemu-loongarch64.toml index 2b60789110..4ec6e24d00 100644 --- a/test-suit/starryos/qemu/tty-console-input-burst/qemu-loongarch64.toml +++ b/test-suit/starryos/qemu/tty-console-input-burst/qemu-loongarch64.toml @@ -6,8 +6,6 @@ args = [ "-nographic", "-m", "2G", - "-smp", - "4", "-device", "nvme,serial=deadbeef,drive=nvm", "-drive",