//! Independent bounded process identity/lifecycle model, not an EL0 scheduler. //! TaskId identifies execution slots; ProcessId identifies an owning process. //! The bounded acceptance images wire these transitions to TTBR retirement. //! This metadata model alone never authorizes general/SMP page reclamation. pub const PROCESS_CAPACITY: usize = 4; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ProcessId { pub slot: u8, pub generation: u64, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum State { Ready, Running, Waiting, Exited(i32), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Error { Capacity, GenerationExhausted, StaleId, Transition, AlreadyRunning, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct Entry { generation: u64, state: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ProcessTable { entries: [Entry; PROCESS_CAPACITY], next_generation: u64, } impl Default for ProcessTable { fn default() -> Self { Self::new() } } impl ProcessTable { pub const fn new() -> Self { Self { entries: [Entry { generation: 0, state: None, }; PROCESS_CAPACITY], next_generation: 1, } } pub fn create(&mut self) -> Result { let slot = self .entries .iter() .position(|e| e.state.is_none()) .ok_or(Error::Capacity)?; let generation = self.next_generation; self.next_generation = generation .checked_add(1) .ok_or(Error::GenerationExhausted)?; self.entries[slot] = Entry { generation, state: Some(State::Ready), }; Ok(ProcessId { slot: slot as u8, generation, }) } fn entry(&self, id: ProcessId) -> Result<&Entry, Error> { let entry = self.entries.get(id.slot as usize).ok_or(Error::StaleId)?; if id.generation == 0 || entry.generation != id.generation || entry.state.is_none() { return Err(Error::StaleId); } Ok(entry) } pub fn state(&self, id: ProcessId) -> Result { self.entry(id)?.state.ok_or(Error::StaleId) } pub fn run(&mut self, id: ProcessId) -> Result<(), Error> { if self.state(id)? != State::Ready { return Err(Error::Transition); } if self.entries.iter().any(|e| e.state == Some(State::Running)) { return Err(Error::AlreadyRunning); } self.entries[id.slot as usize].state = Some(State::Running); Ok(()) } pub fn wait(&mut self, id: ProcessId) -> Result<(), Error> { self.transition(id, State::Running, State::Waiting) } pub fn wake(&mut self, id: ProcessId) -> Result<(), Error> { self.transition(id, State::Waiting, State::Ready) } pub fn yield_ready(&mut self, id: ProcessId) -> Result<(), Error> { self.transition(id, State::Running, State::Ready) } /// Select ready identity metadata only. This is not permission to switch /// TTBR or reclaim pages; the retirement path must finish while masked. pub fn next_ready(&self) -> Option { self.entries.iter().enumerate().find_map(|(slot, entry)| { (entry.state == Some(State::Ready)).then_some(ProcessId { slot: slot as u8, generation: entry.generation, }) }) } fn transition(&mut self, id: ProcessId, from: State, to: State) -> Result<(), Error> { if self.state(id)? != from { return Err(Error::Transition); } self.entries[id.slot as usize].state = Some(to); Ok(()) } pub fn exit(&mut self, id: ProcessId, code: i32) -> Result<(), Error> { if matches!(self.state(id)?, State::Exited(_)) { return Err(Error::Transition); } self.entries[id.slot as usize].state = Some(State::Exited(code)); Ok(()) } /// Reap identity metadata only. Deliberately does not free physical pages, /// retire ASIDs or pretend that a running CPU/device has been quiesced. pub fn reap(&mut self, id: ProcessId) -> Result { let State::Exited(code) = self.state(id)? else { return Err(Error::Transition); }; self.entries[id.slot as usize].state = None; Ok(code) } } #[cfg(test)] mod tests { use super::*; #[test] fn p07_k536_ready_selection_never_returns_exited_reaped_or_old_generation() { let mut table = ProcessTable::new(); let dying = table.create().unwrap(); let survivor = table.create().unwrap(); table.run(dying).unwrap(); table.exit(dying, -11).unwrap(); assert_eq!(table.next_ready(), Some(survivor)); assert_eq!(table.run(dying), Err(Error::Transition)); table.reap(dying).unwrap(); assert_eq!(table.next_ready(), Some(survivor)); let replacement = table.create().unwrap(); assert_ne!(replacement, dying); assert_eq!(table.next_ready(), Some(replacement)); assert_eq!(table.run(dying), Err(Error::StaleId)); table.run(replacement).unwrap(); table.wait(replacement).unwrap(); table.exit(survivor, 0).unwrap(); assert_eq!(table.next_ready(), None); } #[test] fn p10_k521_independent_three_process_ids_reject_old_generation() { let mut table = ProcessTable::new(); let a = table.create().unwrap(); let b = table.create().unwrap(); let c = table.create().unwrap(); assert_ne!(a, b); assert_ne!(b, c); assert_ne!(a, c); table.exit(c, -11).unwrap(); table.reap(c).unwrap(); let replacement = table.create().unwrap(); assert_eq!(replacement.slot, c.slot); assert!(replacement.generation > c.generation); assert_eq!(table.run(c), Err(Error::StaleId)); assert_eq!(table.exit(c, 0), Err(Error::StaleId)); assert_eq!(table.state(a), Ok(State::Ready)); assert_eq!(table.state(b), Ok(State::Ready)); table.next_generation = u64::MAX; assert_eq!(table.create(), Err(Error::GenerationExhausted)); } #[test] fn p10_k529_wait_exit_reap_and_reuse_have_explicit_transitions() { let mut table = ProcessTable::new(); let a = table.create().unwrap(); let b = table.create().unwrap(); table.run(a).unwrap(); assert_eq!(table.run(b), Err(Error::AlreadyRunning)); table.wait(a).unwrap(); table.run(b).unwrap(); table.yield_ready(b).unwrap(); table.wake(a).unwrap(); table.run(a).unwrap(); table.exit(a, 42).unwrap(); assert_eq!(table.wake(a), Err(Error::Transition)); assert_eq!(table.run(a), Err(Error::Transition)); assert_eq!(table.reap(a), Ok(42)); assert_eq!(table.reap(a), Err(Error::StaleId)); let next = table.create().unwrap(); assert_eq!(next.slot, a.slot); assert_ne!(next.generation, a.generation); assert_eq!(table.wait(a), Err(Error::StaleId)); table.run(next).unwrap(); } }