diff --git a/.gitmodules b/.gitmodules index 8cd4ab82..473ecab1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,11 @@ path = arceos url = https://github.com/Starry-OS/arceos branch = main +[submodule "local_crates/starry-signal"] + path = local_crates/starry-signal + url = https://github.com/TomGoh/starry-signal.git + branch = feat/sigstop-cont +[submodule "local_crates/starry-process"] + path = local_crates/starry-process + url = https://github.com/TomGoh/starry-process.git + branch = feat/state-machine diff --git a/Cargo.lock b/Cargo.lock index d5801805..2f3e83a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1035,6 +1035,22 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "ctor" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec09e802f5081de6157da9a75701d6c713d8dc3ba52571fd4bd25f412644e8a6" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" + [[package]] name = "darling" version = "0.13.4" @@ -1133,6 +1149,21 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "dtor" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" + [[package]] name = "either" version = "1.15.0" @@ -2248,9 +2279,9 @@ dependencies = [ [[package]] name = "starry-process" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88fa031a95c25b7bcfe8883f9f53238c9053a2a89f790bb1a7c35d080c6d3b65" dependencies = [ + "bitflags 2.10.0", + "ctor", "kspin", "lazyinit", "weak-map", @@ -2259,7 +2290,6 @@ dependencies = [ [[package]] name = "starry-signal" version = "0.2.3" -source = "git+https://github.com/Starry-OS/starry-signal.git?tag=dev-v02#0597c1695b7c724f6d40e0d5873eaa148679e8bd" dependencies = [ "axcpu", "bitflags 2.10.0", diff --git a/Cargo.toml b/Cargo.toml index 6a369489..9538c9a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,8 +72,8 @@ memory_addr = "0.4" scope-local = "0.1" slab = { version = "0.4.9", default-features = false } spin = "0.10" -starry-process = "0.2" -starry-signal = { version = "0.2", git = "https://github.com/Starry-OS/starry-signal.git", tag = "dev-v02" } +starry-process = { path = "./local_crates/starry-process" } +starry-signal = { path = "./local_crates/starry-signal" } starry-vm = "0.2" starry-core = { path = "./core" } diff --git a/Makefile b/Makefile index ba61b85f..d6062965 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Build Options -export ARCH := riscv64 +export ARCH := aarch64 export LOG := warn export BACKTRACE := y export MEMTRACK := n diff --git a/api/src/signal.rs b/api/src/signal.rs index 9aa064cb..3720c2ca 100644 --- a/api/src/signal.rs +++ b/api/src/signal.rs @@ -4,22 +4,109 @@ use axerrno::AxResult; use axhal::uspace::UserContext; use axtask::current; use starry_core::task::{AsThread, Thread}; -use starry_signal::{SignalOSAction, SignalSet}; +use starry_signal::{SignalOSAction, SignalSet, Signo}; -use crate::task::do_exit; +use crate::task::{do_continue, do_exit, do_stop}; +/// Check, and handle non-customized signals for a thread. +/// +/// This function checks the signals for a thread and handles them if any are +/// pending. The procedure of signal handling is as follows: +/// 1. Do a pre-check for `SIGCONT`, if there is a `SIGCONT` and current process +/// is stopped, continue the process no matter the signal is blocked/ignored +/// or not. +/// 2. Check the signal queue for the thread and handle the signal if any is +/// pending. If the signal is unblocked, it would be removed from the thread- +/// level signal queue and the signal would be handled following. +/// 3. An early-return for a special case of `SIGCONT` where its corresponding +/// disposition is set to be `SIG_IGN` (ignored). Since the process has +/// already been continued in step 1, we return `true` without further +/// processing. +/// 4. Handle an unexpected case where a signal has no OS action (should not +/// happen for normal signals). Log a warning and return `false`. +/// 5. Handle the signal's OS-level action: Terminate, CoreDump, Stop, Continue, +/// or Handler (which has already set up the user context for handler +/// execution). +/// +/// # Arguments +/// +/// * `thr` - The thread to check signals for. +/// * `uctx` - The user context to use for signal delivery. +/// * `restore_blocked` - The signal set to restore after signal delivery. +/// +/// # Returns +/// +/// * `true` - If the signal is handled. +/// * `false` - If the signal is not handled. pub fn check_signals( thr: &Thread, uctx: &mut UserContext, restore_blocked: Option, ) -> bool { + // Per POSIX.1-2024, when SIGCONT is sent to a stopped process: + // 1. The process MUST continue (transition from STOPPED to RUNNING), even if: + // - SIGCONT is blocked by all threads + // - SIGCONT disposition is SIG_IGN (ignored) + // - SIGCONT has a custom handler registered + // 2. The signal remains pending if blocked (delivered when unblocked later) + // 3. The handler executes after continuation (if not ignored) + // + // This pre-check ensures the side effect (continuing) happens before signal + // delivery as a must, which may be deferred if the signal is blocked. + // + // We only check with `has_signal()`, not `dequeue_signal()`, because blocked + // signals won't be dequeued but must still trigger the continue operation. + if thr.proc_data.proc.is_stopped() { + if thr.signal.has_signal(Signo::SIGCONT) || thr.proc_data.signal.has_signal(Signo::SIGCONT) + { + info!( + "Process {} continuing due to pending SIGCONT (may be blocked)", + thr.proc_data.proc.pid() + ); + do_continue(); + } + } + + // A side effect of `check_signals` here woould be: + // if the signal is unblocked, it would be removed from the thread-level signal + // queue, and the signal would be handled following. let Some((sig, os_action)) = thr.signal.check_signals(uctx, restore_blocked) else { return false; }; let signo = sig.signo(); + + // Special case: + // SIGCONT with ignored disposition. + // `os_action` is None, but we still need to continue. + // Since the `do_continue` is called initially, we may safely return. + if signo == Signo::SIGCONT && os_action.is_none() { + info!( + "Process {} continuing due to ignored SIGCONT with stopped: {}", + thr.proc_data.proc.pid(), + thr.proc_data.proc.is_stopped() + ); + + return true; + } + + // Handle normal signals with OS actions + let Some(os_action) = os_action else { + // This shouldn't happen for other signals, but handle gracefully + warn!( + "Process {} received signal {} with no OS action", + thr.proc_data.proc.pid(), + signo as u8 + ); + return false; + }; + match os_action { SignalOSAction::Terminate => { + info!( + "Process {} terminating due to signal", + thr.proc_data.proc.pid() + ); do_exit(signo as i32, true); } SignalOSAction::CoreDump => { @@ -27,14 +114,17 @@ pub fn check_signals( do_exit(128 + signo as i32, true); } SignalOSAction::Stop => { - // TODO: implement stop - do_exit(1, true); + info!("Process {} stopped due to signal", thr.proc_data.proc.pid()); + do_stop(signo); } SignalOSAction::Continue => { - // TODO: implement continue + info!( + "Process {} continuing due to signal (handled by pre-check)", + thr.proc_data.proc.pid() + ); } SignalOSAction::Handler => { - // do nothing + info!("Process {} handling signal", thr.proc_data.proc.pid()); } } true diff --git a/api/src/task.rs b/api/src/task.rs index 2408798f..c88704e8 100644 --- a/api/src/task.rs +++ b/api/src/task.rs @@ -1,8 +1,8 @@ -use core::{ffi::c_long, sync::atomic::Ordering}; +use core::{ffi::c_long, future::poll_fn, sync::atomic::Ordering, task::Poll}; use axerrno::{AxError, AxResult}; use axhal::uspace::{ExceptionKind, ReturnReason, UserContext}; -use axtask::{TaskInner, current}; +use axtask::{CurrentTask, TaskInner, current, future::block_on}; use bytemuck::AnyBitPattern; use linux_raw_sys::general::ROBUST_LIST_LIMIT; use starry_core::{ @@ -10,8 +10,8 @@ use starry_core::{ mm::access_user_memory, shm::SHM_MANAGER, task::{ - AsThread, get_process_data, get_task, send_signal_to_process, send_signal_to_thread, - set_timer_state, + AsThread, Thread, get_process_data, get_task, send_signal_to_process, + send_signal_to_thread, set_timer_state, }, time::TimerState, }; @@ -85,6 +85,20 @@ pub fn new_user_task( } } + // Check for a potential stop signal + if !unblock_next_signal() { + while check_signals(thr, &mut uctx, None) {} + } + + // Handle the stop signal if any + handle_stopped_state(&curr, thr); + + // Check for potential continue or kill signal + // after a `Ready` result is returned from the helper + // function handling the potential stopped state. + // This time, the state of the process may be + // set to be `Running` if it was previously stopped + // in the `do_continue` function within `check_signals`. if !unblock_next_signal() { while check_signals(thr, &mut uctx, None) {} } @@ -184,6 +198,14 @@ pub fn do_exit(exit_code: i32, group_exit: bool) { let process = &thr.proc_data.proc; if process.exit_thread(curr.id().as_u64() as Pid, exit_code) { + // Transition the process to ZOMBIE state before reparenting children. + // + // Rationale for this ordering: + // 1. Ensures the parent's wait() sees the `ZOMBIE` state immediately upon + // wakeup (via Release-Acquire memory ordering with `is_zombie()`). + // 2. Maintains single responsibility: `Process::exit()` only handles child + // reparenting, while state transitions remain explicit at the API layer. + process.transition_to_zombie(); process.exit(); if let Some(parent) = process.parent() { if let Some(signo) = thr.proc_data.exit_signal { @@ -225,3 +247,212 @@ pub fn raise_signal_fatal(sig: SignalInfo) -> AxResult<()> { Ok(()) } + +/// Handle the potential stopped state of the current process, +/// actually performing the stoppage. +/// +/// The procedure is as follows: +/// 1. Check if the process is stopped. If not, return immediately. +/// 2. If the process is stopped, block the current task on the `stop_event`. +/// 3. The task will be woken when a `SIGCONT` or `SIGKILL` signal arrives. +/// +/// # Implementation Details +/// +/// How this function implements the process stop: +/// - Using a `block_on` function to block the current task on a future that +/// will not be ready unless the process is continued. +/// - The future is created by a `poll_fn` function, which will poll the stopped +/// state of the process. +/// - The stopped state is checked in the `poll_fn` function, and the future +/// will be ready when the stopped state is changed to other states. +/// +/// This blocking future is registered in the `stop_event` of the process +/// declared in the `ProcessData` struct. It will be woken +/// if and only if a `SIGKILL` or a `SIGCONT` signal is sent to the process. +/// When a `SIGKILL` or a `SIGCONT` signal is sent to the process (see +/// `send_signal_to_process` in `core/src/task.rs`), the `stop_event` will wake +/// up this blocked future. The future then checks: +/// 1. If the process state has changed to non-stopped, return `Ready` +/// immediately. +/// 2. Otherwise, check if `SIGCONT` or `SIGKILL` is pending via `has_signal()`. +/// If found, return `Ready` to allow signal processing to continue/kill the +/// process. +/// +/// # Arguments +/// +/// * `curr` - The current task. +/// * `thr` - The current thread. +fn handle_stopped_state(curr: &CurrentTask, thr: &Thread) { + // Check if process is stopped and block until continued. + // If the process is not in the stopped state, return. + // The stopped state should have already been set + // if there is a signal that requests to stop the process + // in the previous `check_signals` function call with + // `do_stop` function in the stopped branch. + if !thr.proc_data.proc.is_stopped() { + return; + } + + info!( + "Task {} blocked (process {} stopped)", + curr.id().as_u64(), + thr.proc_data.proc.pid() + ); + + // Deploy an async event for actually stopping the process + block_on(poll_fn(|cx| { + // Fast route: only directly return `Ready` when the process's stopped state has + // been updated to other states. + // This check is essential for multi-threading setting, can prevent the task + // from being blocked when the process is already continued by some other + // threads within the same process. + if !thr.proc_data.proc.is_stopped() { + Poll::Ready(()) + } else { + // This won't got executed when process is stopped until a + // SIGCONT or SIGKILL arrives, which would change the process state + info!( + "Task {} blocked (process {} stopped), checking signals", + curr.id().as_u64(), + thr.proc_data.proc.pid() + ); + if thr.signal.has_signal(Signo::SIGCONT) + || thr.signal.has_signal(Signo::SIGKILL) + || thr.proc_data.signal.has_signal(Signo::SIGCONT) + || thr.proc_data.signal.has_signal(Signo::SIGKILL) + { + info!( + "Task {} blocked (process {} stopped), signal received", + curr.id().as_u64(), + thr.proc_data.proc.pid() + ); + return Poll::Ready(()); + } + + info!( + "Task {} blocked (process {} stopped), waiting for signal", + curr.id().as_u64(), + thr.proc_data.proc.pid() + ); + + // Register the waker for a `stop_event` + thr.proc_data.stop_event.register(cx.waker()); + Poll::Pending + } + })); + + info!( + "Task {} resumed (process {} continued)", + curr.id().as_u64(), + thr.proc_data.proc.pid() + ); +} +/// Stop the current process per a stopping signal. +/// +/// Several procedures are involved in this function: +/// 1. Remove all SIGCONT signals pending in the process's queue. +/// 2. Remove all SIGCONT signals pending in each thread's queue. +/// 3. Record the stop signal in `ProcessData`. +/// 4. Change the state of current process to `STOPPED`. +/// 5. Notify parent process for this stoppage state change. +/// +/// # Arguments +/// +/// * `stop_signal` - The signal that causes the process to stop. +pub(crate) fn do_stop(stop_signal: Signo) { + let curr = current(); + let curr_thread = curr.as_thread(); + let curr_process = &curr_thread.proc_data.proc; + + // If current process is not running, do nothing. + if !curr_process.is_running() { + warn!("Process {} is not running", curr_process.pid()); + return; + } + + info!( + "Process {} stopping due to signal {}", + curr_process.pid(), + stop_signal as u8 + ); + + // remove all SIGCONT signals pending in the process's queue + curr_thread.proc_data.signal.remove_signal(Signo::SIGCONT); + + // remove all SIGCONT signals pending in each thread's queue + curr_process.threads().iter().for_each(|tid| { + if let Ok(thread) = get_task(*tid) { + thread.as_thread().signal.remove_signal(Signo::SIGCONT); + } + }); + + // record the stop signal in the `ProcessSignalManager` + curr_thread.proc_data.signal.set_stop_signal(stop_signal); + + // change the state of current process to `STOPPED` + curr_process.transition_to_stopped(); + + // notify parent process for this stoppage state change + if let Some(parent) = curr_process.parent() + && let Ok(parent_data) = get_process_data(parent.pid()) + { + parent_data.child_exit_event.wake(); + + // POSIX: Send SIGCHLD to parent when child stops (unless SA_NOCLDSTOP set) + // TODO: Check SA_NOCLDSTOP flag when implemented + let siginfo = SignalInfo::new_kernel(Signo::SIGCHLD); + let _ = send_signal_to_process(parent.pid(), Some(siginfo)); + } +} + +/// Continue the current process per a `SIGCONT` signal. +/// +/// The procedure is as follows: +/// 1. Remove all stopping signals pending in the process's queue. +/// 2. Remove all stopping signals pending in each thread's queue. +/// 3. Change the state of current process to `RUNNING`. +/// 4. Resume all threads in the process. +/// 5. Notify parent process for this stoppage state change. +pub(crate) fn do_continue() { + let curr = current(); + let curr_thread = curr.as_thread(); + let curr_proc = &curr_thread.proc_data.proc; + + // If current process is not stopped, do nothing. + if !curr_proc.is_stopped() { + warn!("Process {} is not stopped", curr_proc.pid()); + return; + } + + info!("Process {} continuing due to signal", curr_proc.pid()); + + // remove all stopping signals pending in the process's queue + curr_thread.proc_data.signal.flush_stop_signals(); + + // remove all stopping signals pending in each thread's queue + for thread_pid in curr_proc.threads().iter() { + if let Ok(thread) = get_task(*thread_pid) { + thread.as_thread().signal.flush_stop_signals(); + } + } + + // record the continue event in the `ProcessSignalManager` + curr_thread.proc_data.signal.set_cont_signal(); + + // change the state of current process to `RUNNING` + curr_proc.transition_to_running(); + + // wake up all threads + for thread_pid in curr_proc.threads().iter() { + if let Ok(thread) = get_task(*thread_pid) { + thread.interrupt(); + } + } + + // Notify parent process for this continuation state change + if let Some(parent) = curr_proc.parent() + && let Ok(parent_data) = get_process_data(parent.pid()) + { + parent_data.child_exit_event.wake(); + } +} diff --git a/core/src/task.rs b/core/src/task.rs index 48034b21..c118c5a6 100644 --- a/core/src/task.rs +++ b/core/src/task.rs @@ -215,7 +215,14 @@ pub struct ProcessData { pub child_exit_event: Arc, /// Self exit event pub exit_event: Arc, - /// The exit signal of the thread + /// Self stop event + pub stop_event: Arc, + /// The signal to be sent to the parent process when this task terminates. + /// + /// - For normal processes (fork), this is usually `SIGCHLD`. + /// - For threads (clone with CLONE_THREAD), this is usually `None` (0). + /// + /// NOTE: This is NOT the signal that caused the task to exit. pub exit_signal: Option, /// The process signal manager @@ -251,6 +258,7 @@ impl ProcessData { child_exit_event: Arc::default(), exit_event: Arc::default(), + stop_event: Arc::default(), exit_signal, signal: Arc::new(ProcessSignalManager::new( @@ -470,6 +478,14 @@ pub fn set_timer_state(task: &TaskInner, state: TimerState) { } fn send_signal_thread_inner(task: &TaskInner, thr: &Thread, sig: SignalInfo) { + let signo = sig.signo(); + + // Wake the process up, if it is stopped, i.e. blocked on the `stop_event`, + // when a SIGCONT or a SIGKILL arrives + if signo == Signo::SIGCONT || signo == Signo::SIGKILL { + thr.proc_data.stop_event.wake(); + } + if thr.signal.send_signal(sig) { task.interrupt(); } @@ -498,6 +514,13 @@ pub fn send_signal_to_process(pid: Pid, sig: Option) -> AxResult<()> if let Some(sig) = sig { let signo = sig.signo(); info!("Send signal {signo:?} to process {pid}"); + + // Wake the process up, if it is stopped, i.e. blocked on the `stop_event`, + // when a SIGCONT or a SIGKILL arrives + if signo == Signo::SIGCONT || signo == Signo::SIGKILL { + proc_data.stop_event.wake(); + } + if let Some(tid) = proc_data.signal.send_signal(sig) && let Ok(task) = get_task(tid) { diff --git a/local_crates/starry-process b/local_crates/starry-process new file mode 160000 index 00000000..ed921228 --- /dev/null +++ b/local_crates/starry-process @@ -0,0 +1 @@ +Subproject commit ed9212288c12704185cd2a2aa545bcc1e88d1399 diff --git a/local_crates/starry-signal b/local_crates/starry-signal new file mode 160000 index 00000000..eb33931f --- /dev/null +++ b/local_crates/starry-signal @@ -0,0 +1 @@ +Subproject commit eb33931fc4d36446842eb9ab8a9386541e34d428