⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/process/
wait.rs

1//! Process Wait/Exit Infrastructure
2//!
3//! Provides `waitpid`-style semantics for parent processes to wait on children.
4//! Implements a global wait queue with notification and zombie collection.
5
6// waitpid infrastructure
7
8#[cfg(feature = "alloc")]
9extern crate alloc;
10
11#[cfg(feature = "alloc")]
12use alloc::collections::BTreeMap;
13#[cfg(feature = "alloc")]
14use alloc::vec::Vec;
15
16use spin::Mutex;
17
18use super::{pcb::ProcessState, ProcessId};
19use crate::error::KernelError;
20
21// ---------------------------------------------------------------------------
22// Wait Options
23// ---------------------------------------------------------------------------
24
25/// Options controlling `waitpid` behavior, modeled after POSIX flags.
26#[derive(Debug, Clone, Copy, Default)]
27pub struct WaitOptions {
28    flags: u32,
29}
30
31impl WaitOptions {
32    /// Do not block if no child has changed state.
33    pub const WNOHANG: u32 = 1;
34    /// Also return if a child has stopped (traced or SIGSTOP).
35    pub const WUNTRACED: u32 = 2;
36    /// Also return if a stopped child has been resumed by SIGCONT.
37    pub const WCONTINUED: u32 = 8;
38
39    /// Create options from raw flags.
40    pub fn from_flags(flags: u32) -> Self {
41        Self { flags }
42    }
43
44    /// Check whether WNOHANG is set.
45    pub fn is_nohang(&self) -> bool {
46        self.flags & Self::WNOHANG != 0
47    }
48
49    /// Check whether WUNTRACED is set.
50    pub fn is_untraced(&self) -> bool {
51        self.flags & Self::WUNTRACED != 0
52    }
53
54    /// Check whether WCONTINUED is set.
55    pub fn is_continued(&self) -> bool {
56        self.flags & Self::WCONTINUED != 0
57    }
58}
59
60// ---------------------------------------------------------------------------
61// Wait Status
62// ---------------------------------------------------------------------------
63
64/// Status returned by `waitpid` describing how a child changed state.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum WaitStatus {
67    /// Child exited normally with the given status code.
68    Exited(i32),
69    /// Child was terminated by a signal.
70    Signaled(i32),
71    /// Child was stopped by a signal (only with WUNTRACED).
72    Stopped(i32),
73    /// Child was resumed by SIGCONT (only with WCONTINUED).
74    Continued,
75}
76
77impl WaitStatus {
78    /// Encode the status as a raw `i32` matching POSIX `wstatus` layout.
79    ///
80    /// - Exited: `(code & 0xFF) << 8`
81    /// - Signaled: `signum & 0x7F`
82    /// - Stopped: `0x7F | (signum << 8)`
83    /// - Continued: `0xFFFF`
84    pub fn to_raw(self) -> i32 {
85        match self {
86            Self::Exited(code) => (code & 0xFF) << 8,
87            Self::Signaled(sig) => sig & 0x7F,
88            Self::Stopped(sig) => 0x7F | (sig << 8),
89            Self::Continued => 0xFFFF_u16 as i32,
90        }
91    }
92}
93
94// ---------------------------------------------------------------------------
95// Wait Entry
96// ---------------------------------------------------------------------------
97
98/// A single entry in a wait queue tracking a parent waiting for children.
99#[derive(Debug, Clone, Copy)]
100struct WaitEntry {
101    /// The PID of the waiting parent.
102    waiter_pid: ProcessId,
103    /// The specific child PID being waited for, or `None` for any child.
104    #[allow(dead_code)] // Used when filtered waitpid is wired
105    target_pid: Option<ProcessId>,
106    /// Options controlling wait behavior.
107    #[allow(dead_code)] // Used when WNOHANG/WUNTRACED are supported
108    options: WaitOptions,
109}
110
111// ---------------------------------------------------------------------------
112// Wait Queue
113// ---------------------------------------------------------------------------
114
115/// Global wait queue tracking which parents are waiting for children.
116///
117/// Keyed by the parent PID, each entry contains a list of outstanding waits.
118#[cfg(feature = "alloc")]
119struct WaitQueue {
120    /// Map from parent PID to list of wait entries.
121    entries: BTreeMap<ProcessId, Vec<WaitEntry>>,
122}
123
124#[cfg(feature = "alloc")]
125impl WaitQueue {
126    const fn new() -> Self {
127        Self {
128            entries: BTreeMap::new(),
129        }
130    }
131
132    /// Register a wait entry for a parent process.
133    fn register(&mut self, entry: WaitEntry) {
134        self.entries
135            .entry(entry.waiter_pid)
136            .or_default()
137            .push(entry);
138    }
139
140    /// Remove all wait entries for a parent process.
141    fn remove_waiter(&mut self, parent_pid: ProcessId) {
142        self.entries.remove(&parent_pid);
143    }
144
145    /// Check whether a parent has any outstanding waits.
146    #[allow(dead_code)] // Wait queue introspection API
147    fn has_waiter(&self, parent_pid: ProcessId) -> bool {
148        self.entries.get(&parent_pid).is_some_and(|v| !v.is_empty())
149    }
150}
151
152/// Global wait queue instance.
153#[cfg(feature = "alloc")]
154static WAIT_QUEUE: Mutex<WaitQueue> = Mutex::new(WaitQueue::new());
155
156// ---------------------------------------------------------------------------
157// System Call: waitpid
158// ---------------------------------------------------------------------------
159
160/// Wait for a child process to change state.
161///
162/// # Arguments
163/// * `pid` - Process to wait for:
164///   - `pid > 0`: Wait for the specific child with that PID.
165///   - `pid == -1`: Wait for any child process.
166///   - Other negative values are reserved (currently treated as any child).
167/// * `options` - [`WaitOptions`] controlling blocking and status filters.
168///
169/// # Returns
170/// A tuple of `(child_pid, status)` on success.
171#[cfg(feature = "alloc")]
172pub fn sys_waitpid(pid: i64, options: WaitOptions) -> Result<(ProcessId, WaitStatus), KernelError> {
173    let current = super::current_process().ok_or(KernelError::NotInitialized {
174        subsystem: "current process",
175    })?;
176    let parent_pid = current.pid;
177
178    // Determine which child we are waiting for.
179    let target: Option<ProcessId> = if pid > 0 {
180        Some(ProcessId(pid as u64))
181    } else {
182        None // -1 or other negative => any child.
183    };
184
185    loop {
186        // Scan children for one matching the target that has changed state.
187        let children = super::table::PROCESS_TABLE.find_children(parent_pid);
188
189        if children.is_empty() {
190            return Err(KernelError::NotFound {
191                resource: "child process",
192                id: 0,
193            });
194        }
195
196        let mut target_exists = false;
197
198        for child_pid in &children {
199            if let Some(target_pid) = target {
200                if *child_pid != target_pid {
201                    continue;
202                }
203            }
204            target_exists = true;
205
206            if let Some(child) = super::table::get_process(*child_pid) {
207                let state = child.get_state();
208
209                // Zombie => child has exited.
210                if state == ProcessState::Zombie {
211                    let exit_code = child.get_exit_code();
212                    let status = WaitStatus::Exited(exit_code);
213
214                    // Collect the zombie.
215                    collect_zombie(*child_pid, parent_pid)?;
216
217                    return Ok((*child_pid, status));
218                }
219
220                // Stopped child (WUNTRACED).
221                if options.is_untraced() && state == ProcessState::Blocked {
222                    // SIGSTOP = 19
223                    return Ok((*child_pid, WaitStatus::Stopped(19)));
224                }
225
226                // Continued child (WCONTINUED).
227                if options.is_continued()
228                    && (state == ProcessState::Running || state == ProcessState::Ready)
229                {
230                    return Ok((*child_pid, WaitStatus::Continued));
231                }
232            }
233        }
234
235        // If we asked for a specific child that does not exist, error out.
236        if let Some(target_pid) = target {
237            if !target_exists {
238                return Err(KernelError::ProcessNotFound { pid: target_pid.0 });
239            }
240        }
241
242        // WNOHANG: return immediately.
243        if options.is_nohang() {
244            return Err(KernelError::WouldBlock);
245        }
246
247        // Register in the wait queue and block.
248        {
249            let mut wq = WAIT_QUEUE.lock();
250            wq.register(WaitEntry {
251                waiter_pid: parent_pid,
252                target_pid: target,
253                options,
254            });
255        }
256
257        // Block the current process. When a child exits, `notify_parent` will
258        // wake us up and we will loop back to check again.
259        current.set_state(ProcessState::Blocked);
260        crate::sched::yield_cpu();
261        current.set_state(ProcessState::Running);
262
263        // If a signal interrupted us, return early.
264        if let Some(signum) = current.get_next_pending_signal() {
265            current.clear_pending_signal(signum);
266            // Clean up wait queue entry.
267            WAIT_QUEUE.lock().remove_waiter(parent_pid);
268            return Err(KernelError::WouldBlock);
269        }
270    }
271}
272
273// ---------------------------------------------------------------------------
274// Notifications
275// ---------------------------------------------------------------------------
276
277/// Notify a parent process that a child has changed state.
278///
279/// Called from the child exit path to wake the parent if it is blocked in
280/// `waitpid`. Also sends SIGCHLD to the parent.
281#[cfg(feature = "alloc")]
282pub fn notify_parent(child_pid: ProcessId, status: WaitStatus) {
283    // Find the child's parent.
284    let parent_pid = if let Some(child) = super::table::get_process(child_pid) {
285        child.parent
286    } else {
287        None
288    };
289
290    let parent_pid = match parent_pid {
291        Some(pid) => pid,
292        None => return, // No parent (e.g., init process).
293    };
294
295    let _ = status; // Status is implicit in the child's state.
296
297    // Send SIGCHLD to the parent.
298    if let Some(parent) = super::table::get_process(parent_pid) {
299        use super::exit::signals::SIGCHLD;
300        if let Err(_e) = parent.send_signal(SIGCHLD as usize) {
301            crate::kprintln!("[PROCESS] Warning: Failed to send SIGCHLD to parent");
302        }
303
304        // Wake parent if blocked.
305        if parent.get_state() == ProcessState::Blocked {
306            parent.set_state(ProcessState::Ready);
307            crate::sched::wake_up_process(parent_pid);
308        }
309    }
310
311    // Remove wait queue entries for this parent (they will re-register if
312    // they loop).
313    WAIT_QUEUE.lock().remove_waiter(parent_pid);
314}
315
316// ---------------------------------------------------------------------------
317// Zombie Collection
318// ---------------------------------------------------------------------------
319
320/// Clean up a zombie process after a successful wait.
321///
322/// Removes the child from the parent's children list and from the global
323/// process table.
324#[cfg(feature = "alloc")]
325pub fn collect_zombie(child_pid: ProcessId, parent_pid: ProcessId) -> Result<(), KernelError> {
326    // Remove from parent's children list.
327    if let Some(parent) = super::table::get_process(parent_pid) {
328        parent.children.lock().retain(|&p| p != child_pid);
329    }
330
331    // Remove from the process table.
332    super::table::remove_process(child_pid);
333
334    Ok(())
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    // --- WaitOptions tests ---
342
343    #[test]
344    fn test_wait_options_default() {
345        let opts = WaitOptions::default();
346        assert!(!opts.is_nohang());
347        assert!(!opts.is_untraced());
348        assert!(!opts.is_continued());
349    }
350
351    #[test]
352    fn test_wait_options_nohang() {
353        let opts = WaitOptions::from_flags(WaitOptions::WNOHANG);
354        assert!(opts.is_nohang());
355        assert!(!opts.is_untraced());
356        assert!(!opts.is_continued());
357    }
358
359    #[test]
360    fn test_wait_options_combined() {
361        let opts = WaitOptions::from_flags(WaitOptions::WNOHANG | WaitOptions::WUNTRACED);
362        assert!(opts.is_nohang());
363        assert!(opts.is_untraced());
364        assert!(!opts.is_continued());
365    }
366
367    #[test]
368    fn test_wait_options_all_flags() {
369        let opts = WaitOptions::from_flags(
370            WaitOptions::WNOHANG | WaitOptions::WUNTRACED | WaitOptions::WCONTINUED,
371        );
372        assert!(opts.is_nohang());
373        assert!(opts.is_untraced());
374        assert!(opts.is_continued());
375    }
376
377    // --- WaitStatus tests ---
378
379    #[test]
380    fn test_wait_status_exited() {
381        let status = WaitStatus::Exited(42);
382        assert_eq!(status, WaitStatus::Exited(42));
383        // Raw encoding: (42 & 0xFF) << 8 = 42 << 8 = 10752
384        assert_eq!(status.to_raw(), 42 << 8);
385    }
386
387    #[test]
388    fn test_wait_status_signaled() {
389        let status = WaitStatus::Signaled(11); // SIGSEGV
390        assert_eq!(status.to_raw(), 11);
391    }
392
393    #[test]
394    fn test_wait_status_stopped() {
395        // SIGSTOP, Raw: 0x7F | (19 << 8) = 127 | 4864 = 4991
396        let status = WaitStatus::Stopped(19);
397        assert_eq!(status.to_raw(), 0x7F | (19 << 8));
398    }
399
400    #[test]
401    fn test_wait_status_continued() {
402        let status = WaitStatus::Continued;
403        assert_eq!(status.to_raw(), 0xFFFF_u16 as i32);
404    }
405
406    #[test]
407    fn test_wait_status_equality() {
408        assert_eq!(WaitStatus::Exited(0), WaitStatus::Exited(0));
409        assert_ne!(WaitStatus::Exited(0), WaitStatus::Exited(1));
410        assert_ne!(WaitStatus::Exited(0), WaitStatus::Continued);
411    }
412}