Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0"
repository = "https://github.com/Starry-OS/starry-process"

[dependencies]
bitflags = "2.6"
kspin = "0.1"
lazyinit = "0.2.1"
weak-map = "0.1"
Expand Down
121 changes: 115 additions & 6 deletions src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ use alloc::{
};
use core::{
fmt,
sync::atomic::{AtomicBool, Ordering},
sync::atomic::{AtomicU8, Ordering},
};

use bitflags::bitflags;
use kspin::SpinNoIrq;
use lazyinit::LazyInit;
use weak_map::StrongMap;
Expand All @@ -21,10 +22,19 @@ pub(crate) struct ThreadGroup {
pub(crate) group_exited: bool,
}

bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ProcessState: u8 {
const RUNNING = 1 << 0;
const STOPPED = 1 << 1;
const ZOMBIE = 1 << 2;
}
}
Comment thread
TomGoh marked this conversation as resolved.

/// A process.
pub struct Process {
pid: Pid,
is_zombie: AtomicBool,
state: AtomicU8,
pub(crate) tg: SpinNoIrq<ThreadGroup>,

// TODO: child subreaper9
Expand Down Expand Up @@ -191,10 +201,110 @@ impl Process {
impl Process {
/// Returns `true` if the [`Process`] is a zombie process.
pub fn is_zombie(&self) -> bool {
self.is_zombie.load(Ordering::Acquire)
let bits = self.state.load(Ordering::Acquire);
ProcessState::from_bits_truncate(bits).contains(ProcessState::ZOMBIE)
}

/// Returns `true` if the [`Process`] is running.
pub fn is_running(&self) -> bool {
let bits = self.state.load(Ordering::Acquire);
ProcessState::from_bits_truncate(bits).contains(ProcessState::RUNNING)
}

/// Returns `true` if the [`Process`] is stopped.
pub fn is_stopped(&self) -> bool {
Comment thread
TomGoh marked this conversation as resolved.
let bits = self.state.load(Ordering::Acquire);
ProcessState::from_bits_truncate(bits).contains(ProcessState::STOPPED)
}

/// Change the [`Process`] from Running to `Stopped`.
Comment thread
TomGoh marked this conversation as resolved.
Outdated
///
/// This method atomically transitions the process state to STOPPED using
/// CAS, ensuring the state is either successfully changed or already in
/// ZOMBIE state (in which case no change occurs).
///
/// # Memory Ordering
///
/// Uses `Release` ordering on success to synchronize with `Acquire` loads
/// in `is_stopped()`. This ensures that any writes before this
/// transition, such as setting `stop_signal` in the
/// `ProcessSignalManager`, are visible to threads that observe the
/// `STOPPED` state transition.
pub fn transition_to_stopped(&self) {
let _ = self.state.fetch_update(
Ordering::Release, // Success: synchronize with is_stopped()
Ordering::Relaxed, // Failure: no synchronization needed
|curr| {
let mut flags = ProcessState::from_bits_truncate(curr);
if flags.contains(ProcessState::ZOMBIE) || !flags.contains(ProcessState::RUNNING) {
None // Already zombie or not running, don't transition
} else {
flags.remove(ProcessState::RUNNING);
flags.insert(ProcessState::STOPPED);
Some(flags.bits())
}
},
);
}

/// Change the [`Process`] from `Stopped` to `Running`.
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

[nitpick] Similar to transition_to_stopped, the documentation says "Change the [Process] from Stopped to Running" but should more accurately reflect what the method does. Consider: "Transitions the [Process] to Running state."

Suggested change
/// Change the [`Process`] from `Stopped` to `Running`.
/// Transitions the [`Process`] to `Running` state.

Copilot uses AI. Check for mistakes.
///
/// This method atomically transitions the process state to RUNNING using
/// CAS. The transition succeeds if and only if the current state is
/// `STOPPED` and not `ZOMBIE`.
///
/// # Memory Ordering
///
/// Uses `Release` ordering on success to synchronize with `Acquire` loads
/// in `is_running()`. This ensures that any writes before this
/// transition, for example, setting `cont_signal` in the
/// `ProcessSignalManager`, are visible to threads that observe the
/// `RUNNING` state transition.
pub fn transition_to_running(&self) {
let _ = self.state.fetch_update(
Ordering::Release, // Success: synchronize with is_running()
Ordering::Relaxed, // Failure: no synchronization needed
|curr| {
let mut flags = ProcessState::from_bits_truncate(curr);
if !flags.contains(ProcessState::STOPPED) || flags.contains(ProcessState::ZOMBIE) {
None // Not stopped or already zombie, don't transition
} else {
flags.remove(ProcessState::STOPPED);
flags.insert(ProcessState::RUNNING);
Some(flags.bits())
}
},
);
}
Comment thread
TomGoh marked this conversation as resolved.

/// Change the [`Process`] from `Stopped` or `Running` to `Zombie`.
Comment thread
TomGoh marked this conversation as resolved.
Outdated
///
/// This is a terminal state transition - once a process becomes a zombie,
/// it cannot transition to any other state (it can only be freed via
/// `free()`).
///
/// # Memory Ordering
///
/// Uses `Release` ordering to synchronize with `Acquire` loads in
/// `is_zombie()`, ensuring that when a parent process's `wait()` observes
/// the ZOMBIE state, it also observes all writes that happened before
/// the transition, particularly the exit_code set by `exit_thread()`.
pub fn transition_to_zombie(&self) {
let _ = self
.state
.fetch_update(Ordering::Release, Ordering::Relaxed, |curr| {
let mut flags = ProcessState::from_bits_truncate(curr);
if flags.contains(ProcessState::ZOMBIE) {
None // Already zombie
} else {
flags.remove(ProcessState::RUNNING | ProcessState::STOPPED);
flags.insert(ProcessState::ZOMBIE);
Some(flags.bits())
}
});
}

/// Terminates the [`Process`], marking it as a zombie process.
/// Terminates the [`Process`].
///
/// Child processes are inherited by the init process or by the nearest
/// subreaper process.
Expand All @@ -209,7 +319,6 @@ impl Process {
}

let mut children = self.children.lock(); // Acquire the lock first
self.is_zombie.store(true, Ordering::Release);

let mut reaper_children = reaper.children.lock();
let reaper = Arc::downgrade(reaper);
Expand Down Expand Up @@ -266,7 +375,7 @@ impl Process {

let process = Arc::new(Process {
pid,
is_zombie: AtomicBool::new(false),
state: AtomicU8::new(ProcessState::RUNNING.bits()),
tg: SpinNoIrq::new(ThreadGroup::default()),
children: SpinNoIrq::new(StrongMap::new()),
parent: SpinNoIrq::new(parent.as_ref().map(Arc::downgrade).unwrap_or_default()),
Expand Down
42 changes: 42 additions & 0 deletions test_debug.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use std::sync::atomic::{AtomicU8, Ordering};
use bitflags::bitflags;

bitflags\! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ProcessState: u8 {
const RUNNING = 1 << 0;
const STOPPED = 1 << 1;
const ZOMBIE = 1 << 2;
}
}

fn main() {
let state = AtomicU8::new(ProcessState::RUNNING.bits());

// Simulate what happens during concurrent transitions
println\!("Initial: {:08b}", state.load(Ordering::Acquire));

// Thread 1: transition_to_stopped
let result = state.fetch_update(Ordering::Release, Ordering::Relaxed, |curr| {
let mut flags = ProcessState::from_bits_truncate(curr);
println\!("Thread 1 sees: {:?} ({:08b})", flags, curr);
if flags.contains(ProcessState::ZOMBIE) || \!flags.contains(ProcessState::RUNNING) {
None
} else {
flags.remove(ProcessState::RUNNING);
flags.insert(ProcessState::STOPPED);
println\!("Thread 1 setting to: {:?} ({:08b})", flags, flags.bits());
Some(flags.bits())
}
});

println\!("After T1: {:08b}, result: {:?}", state.load(Ordering::Acquire), result);

// Check the state
let bits = state.load(Ordering::Acquire);
let flags = ProcessState::from_bits_truncate(bits);
println\!("Checking: running={}, stopped={}, zombie={}",
flags.contains(ProcessState::RUNNING),
flags.contains(ProcessState::STOPPED),
flags.contains(ProcessState::ZOMBIE));
}
Comment thread
TomGoh marked this conversation as resolved.
Outdated
2 changes: 2 additions & 0 deletions tests/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ fn cleanup() {
assert!(group.upgrade().is_some());

child.exit();
child.transition_to_zombie();
child.free();
drop(child);
assert!(group.upgrade().is_none());
Expand Down Expand Up @@ -134,6 +135,7 @@ fn cleanup_processes() {
let group = parent.create_group().unwrap();

parent.exit();
parent.transition_to_zombie();
parent.free();
drop(parent);

Expand Down
135 changes: 133 additions & 2 deletions tests/process.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use std::sync::Arc;
use std::{
sync::{Arc, Barrier},
thread,
time::Duration,
};

use starry_process::init_proc;

Expand All @@ -18,6 +22,7 @@ fn exit() {
let parent = init_proc();
let child = parent.new_child();
child.exit();
child.transition_to_zombie();
assert!(child.is_zombie());
assert!(parent.children().iter().any(|c| Arc::ptr_eq(c, &child)));
}
Expand All @@ -33,6 +38,7 @@ fn free() {
let parent = init_proc().new_child();
let child = parent.new_child();
child.exit();
child.transition_to_zombie();
child.free();
assert!(parent.children().is_empty());
}
Expand Down Expand Up @@ -70,4 +76,129 @@ fn thread_exit() {
let last2 = child.exit_thread(102, 3);
assert!(last2);
assert_eq!(child.exit_code(), 7);
}
}

#[test]
fn state_lifecycle() {
let process = init_proc().new_child();

// Initial state should be RUNNING
assert!(process.is_running());
assert!(!process.is_stopped());
assert!(!process.is_zombie());

// RUNNING -> STOPPED
process.transition_to_stopped();
assert!(!process.is_running());
assert!(process.is_stopped());
assert!(!process.is_zombie());

// STOPPED -> RUNNING
process.transition_to_running();
assert!(process.is_running());
assert!(!process.is_stopped());
assert!(!process.is_zombie());

// RUNNING -> ZOMBIE
process.transition_to_zombie();
assert!(!process.is_running());
assert!(!process.is_stopped());
assert!(process.is_zombie());
}

#[test]
fn invalid_transitions() {
let process = init_proc().new_child();

// STOPPED -> STOPPED (idempotent)
process.transition_to_stopped();
assert!(process.is_stopped());
process.transition_to_stopped();
assert!(process.is_stopped());

// STOPPED -> ZOMBIE
process.transition_to_zombie();
assert!(process.is_zombie());

// ZOMBIE -> RUNNING (Invalid)
process.transition_to_running();
assert!(process.is_zombie());
assert!(!process.is_running());

// ZOMBIE -> STOPPED (Invalid)
process.transition_to_stopped();
assert!(process.is_zombie());
assert!(!process.is_stopped());
}

#[test]
fn concurrent_transitions() {
let process = init_proc().new_child();
let barrier = Arc::new(Barrier::new(4));

// Strategy:
// Spawn 4 threads to change the state concurrently.
// All of them start at the same time after all 4 barriers are reached,
// simulating multiple kernel threads accessing and changing the process state
// simultaneously.
let mut handles = vec![];

// Thread 1: Tries to stop
let p1 = process.clone();
let b1 = barrier.clone();
handles.push(thread::spawn(move || {
b1.wait();
for _ in 0..1000 {
p1.transition_to_stopped();
thread::yield_now();
}
}));

// Thread 2: Tries to continue
let p2 = process.clone();
let b2 = barrier.clone();
handles.push(thread::spawn(move || {
b2.wait();
for _ in 0..1000 {
p2.transition_to_running();
thread::yield_now();
}
}));

// Thread 3: Validates terminal state consistency
// validate the terminal state property:
// once a process is a zombie, it should always be a zombie.
let p3 = process.clone();
let b3 = barrier.clone();
handles.push(thread::spawn(move || {
b3.wait();
let mut observed_zombie = false;
for _ in 0..1000 {
if p3.is_zombie() {
observed_zombie = true;
} else if observed_zombie {
// If we previously saw zombie, we should never see non-zombie
panic!("Process transitioned from zombie to non-zombie state!");
}
thread::yield_now();
}
}));

// Thread 4: The killer
let p4 = process.clone();
let b4 = barrier.clone();
handles.push(thread::spawn(move || {
b4.wait();
thread::sleep(Duration::from_millis(50));
p4.transition_to_zombie();
}));

for h in handles {
h.join().unwrap();
}

// Must be zombie at the end
assert!(process.is_zombie());
assert!(!process.is_running());
assert!(!process.is_stopped());
}
Loading