From 817a38993b4c13c31ed7704e5559e1e92e58ae53 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 27 Oct 2026 16:06:04 +0100 Subject: [PATCH] feat(kernel): WatchDeath + SetTimer events as notification signals; adapter wakes at earliest deadline --- crates/adapter-riscv/src/hw/timer.rs | 28 +++- crates/adapter-riscv/src/platform.rs | 8 +- crates/adapter-riscv/src/trap/mod.rs | 1 + crates/adapter-riscv/src/trap/sched.rs | 1 + crates/domain/src/lib.rs | 1 + crates/domain/src/syscall.rs | 13 ++ crates/domain/src/timer.rs | 67 ++++++++++ crates/domain/src/timer_tests.rs | 37 +++++ crates/init/src/selftest.rs | 29 ++++ crates/kernel/src/abi.rs | 8 ++ crates/kernel/src/lib.rs | 7 + crates/kernel/src/proofs/platform.rs | 4 +- crates/kernel/src/syscall/ipc/events.rs | 90 +++++++++++++ crates/kernel/src/syscall/ipc/mod.rs | 1 + crates/kernel/src/syscall/mod.rs | 14 ++ crates/kernel/src/teardown.rs | 1 + crates/kernel/src/tests/abi.rs | 4 +- crates/kernel/src/tests/events.rs | 163 +++++++++++++++++++++++ crates/kernel/src/tests/mock_platform.rs | 9 +- crates/kernel/src/tests/mod.rs | 1 + crates/libkos/src/syscall/notify.rs | 19 +++ crates/platform/src/lib.rs | 6 +- crates/types/src/syscall_abi.rs | 2 + crates/types/src/time.rs | 3 - docs/ipc.md | 21 ++- 25 files changed, 517 insertions(+), 21 deletions(-) create mode 100644 crates/domain/src/timer.rs create mode 100644 crates/domain/src/timer_tests.rs create mode 100644 crates/kernel/src/syscall/ipc/events.rs create mode 100644 crates/kernel/src/tests/events.rs diff --git a/crates/adapter-riscv/src/hw/timer.rs b/crates/adapter-riscv/src/hw/timer.rs index df4d908..ddd76c4 100644 --- a/crates/adapter-riscv/src/hw/timer.rs +++ b/crates/adapter-riscv/src/hw/timer.rs @@ -1,4 +1,10 @@ +use core::sync::atomic::{AtomicU64, Ordering}; + use crate::board; +use crate::smp::{MAX_HARTS, hart_id}; + +/// When each hart's timer is set to go off next. +static NEXT: [AtomicU64; MAX_HARTS] = [const { AtomicU64::new(u64::MAX) }; MAX_HARTS]; pub fn read_time() -> u64 { let time: u64; @@ -21,15 +27,27 @@ fn sbi_set_timer(stime: u64) { } pub fn init() { - let now = read_time(); - sbi_set_timer(now + board::TICK_INTERVAL); + set_next(read_time() + board::TICK_INTERVAL); } pub fn set_next(deadline: u64) { + if let Some(next) = NEXT.get(hart_id()) { + next.store(deadline, Ordering::Relaxed); + } sbi_set_timer(deadline); } -pub fn handle_interrupt() { - let now = read_time(); - sbi_set_timer(now + board::TICK_INTERVAL); +/// Goes off at `deadline` too, if that is before the next tick. +pub fn wake_at(deadline: u64) { + if NEXT + .get(hart_id()) + .is_some_and(|n| deadline < n.load(Ordering::Relaxed)) + { + set_next(deadline); + } +} + +/// Re-arms the tick; the kernel then re-arms for its earliest timer. +pub fn handle_interrupt() { + set_next(read_time() + board::TICK_INTERVAL); } diff --git a/crates/adapter-riscv/src/platform.rs b/crates/adapter-riscv/src/platform.rs index c2a8bfb..f24bf53 100644 --- a/crates/adapter-riscv/src/platform.rs +++ b/crates/adapter-riscv/src/platform.rs @@ -4,10 +4,9 @@ use types::{ address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress}, id::{CoreId, IrqId, ProcessId}, mapping::PagePerms, - time::{Ticks, TimerDuration}, + time::Ticks, }; -use crate::board; use crate::hw::{plic::Plic, timer}; use crate::mmu; use crate::smp; @@ -136,9 +135,8 @@ impl Platform for RiscVPlatform { } } - fn set_timer(&mut self, _duration: TimerDuration) { - let now = timer::read_time(); - timer::set_next(now + board::TICK_INTERVAL); + fn wake_at(&mut self, deadline: Ticks) { + timer::wake_at(deadline.raw()); } fn current_ticks(&self) -> Ticks { diff --git a/crates/adapter-riscv/src/trap/mod.rs b/crates/adapter-riscv/src/trap/mod.rs index 57b3e6c..a7b62a4 100644 --- a/crates/adapter-riscv/src/trap/mod.rs +++ b/crates/adapter-riscv/src/trap/mod.rs @@ -104,6 +104,7 @@ fn handle_interrupt(frame: &mut TrapFrame, cause: usize) { CAUSE_S_TIMER => { crate::hw::timer::handle_interrupt(); let kern = kern(); + kern.fire_timers(); if let Some(current_tid) = kern.current_thread() { let decision = kern.handle_tick(this_core()); if let domain::scheduler::SchedulerDecision::Preempt(next) = decision { diff --git a/crates/adapter-riscv/src/trap/sched.rs b/crates/adapter-riscv/src/trap/sched.rs index 586a99a..f53b4ee 100644 --- a/crates/adapter-riscv/src/trap/sched.rs +++ b/crates/adapter-riscv/src/trap/sched.rs @@ -70,6 +70,7 @@ fn next_runnable() -> types::id::ThreadId { if sip & (1 << CAUSE_S_TIMER) != 0 { // No thread is running, so there is no time slice to charge. crate::hw::timer::handle_interrupt(); + kern().fire_timers(); } if sip & (1 << CAUSE_S_EXTERNAL) != 0 { dispatch_external(); diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index f88594c..1fbb991 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -32,3 +32,4 @@ pub mod resource; pub mod scheduler; pub mod syscall; pub mod thread; +pub mod timer; diff --git a/crates/domain/src/syscall.rs b/crates/domain/src/syscall.rs index baf1661..8b3d1b7 100644 --- a/crates/domain/src/syscall.rs +++ b/crates/domain/src/syscall.rs @@ -4,6 +4,7 @@ use types::{ bounded::{CapSlot, PageCount}, mapping::PagePerms, permission::PermissionSet, + time::Ticks, }; use crate::message::Message; @@ -91,6 +92,18 @@ pub enum Syscall { RegionAddress { region: CapSlot, }, + /// Signals `notification` when the process behind `process` dies, by + /// exit, fault or kill. One watcher per process; a new one replaces it. + WatchDeath { + process: CapSlot, + notification: CapSlot, + }, + /// Signals `notification` once `GetTime` reaches `deadline`; replaces + /// the caller's timer, and a zero deadline cancels it. + SetTimer { + notification: CapSlot, + deadline: Ticks, + }, /// Removes a capability from the caller's own table. DropCap { slot: CapSlot, diff --git a/crates/domain/src/timer.rs b/crates/domain/src/timer.rs new file mode 100644 index 0000000..cce8731 --- /dev/null +++ b/crates/domain/src/timer.rs @@ -0,0 +1,67 @@ +//! Events the kernel reports by signalling a notification: a process's +//! death, a deadline passing. One timer per process. + +use types::{badge::Badge, generation::Generation, id::NotificationId, id::ProcessId, time::Ticks}; + +use crate::process::MAX_PROCESSES; + +#[cfg(test)] +#[path = "timer_tests.rs"] +mod tests; + +/// A notification as a capability named it; once the notification is +/// destroyed (its generation moves on) the target is dead. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Target { + pub notification: NotificationId, + pub generation: Generation, + pub badge: Badge, +} + +pub struct Timers { + deadlines: [Option<(Ticks, Target)>; MAX_PROCESSES], +} + +impl Timers { + pub const fn new() -> Self { + Self { + deadlines: [None; MAX_PROCESSES], + } + } + + /// Replaces `owner`'s timer. + pub fn set(&mut self, owner: ProcessId, deadline: Ticks, target: Target) -> bool { + match self.deadlines.get_mut(owner.raw() as usize) { + Some(slot) => { + *slot = Some((deadline, target)); + true + } + None => false, + } + } + + pub fn clear(&mut self, owner: ProcessId) { + if let Some(slot) = self.deadlines.get_mut(owner.raw() as usize) { + *slot = None; + } + } + + /// Removes and returns one timer whose deadline is at or before `now`. + pub fn take_expired(&mut self, now: Ticks) -> Option { + let slot = self + .deadlines + .iter_mut() + .find(|s| s.is_some_and(|(d, _)| d <= now))?; + slot.take().map(|(_, t)| t) + } + + pub fn earliest(&self) -> Option { + self.deadlines.iter().flatten().map(|(d, _)| *d).min() + } +} + +impl Default for Timers { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/domain/src/timer_tests.rs b/crates/domain/src/timer_tests.rs new file mode 100644 index 0000000..7c3ebb2 --- /dev/null +++ b/crates/domain/src/timer_tests.rs @@ -0,0 +1,37 @@ +use super::{Target, Timers}; +use types::{badge::Badge, generation::Generation, id::NotificationId, id::ProcessId, time::Ticks}; + +fn target(n: u16) -> Target { + Target { + notification: NotificationId::from_raw(n), + generation: Generation::initial(), + badge: Badge::NONE, + } +} + +fn pid(n: u16) -> ProcessId { + ProcessId::from_raw(n) +} + +#[test] +fn only_expired_timers_fire_and_each_fires_once() { + let mut t = Timers::new(); + t.set(pid(0), Ticks::from_raw(10), target(0)); + t.set(pid(1), Ticks::from_raw(30), target(1)); + assert_eq!(t.earliest(), Some(Ticks::from_raw(10))); + assert_eq!(t.take_expired(Ticks::from_raw(9)), None); + assert_eq!(t.take_expired(Ticks::from_raw(10)), Some(target(0))); + assert_eq!(t.take_expired(Ticks::from_raw(29)), None); + assert_eq!(t.earliest(), Some(Ticks::from_raw(30))); +} + +#[test] +fn setting_again_replaces_and_clear_cancels() { + let mut t = Timers::new(); + t.set(pid(2), Ticks::from_raw(10), target(0)); + t.set(pid(2), Ticks::from_raw(50), target(1)); + assert_eq!(t.take_expired(Ticks::from_raw(20)), None); + t.clear(pid(2)); + assert_eq!(t.earliest(), None); + assert!(!t.set(pid(500), Ticks::from_raw(1), target(0))); +} diff --git a/crates/init/src/selftest.rs b/crates/init/src/selftest.rs index ae3afda..63b1fcf 100644 --- a/crates/init/src/selftest.rs +++ b/crates/init/src/selftest.rs @@ -71,8 +71,12 @@ fn run_phases(storage: &mut Storage, scratch: &mut [u8]) -> Result<(), &'static syscall::map_region(page, VirtualAddress::from_raw(OBSERVE_VA), PagePerms::ReadWrite) .map_err(|_| "cannot map the counter page")?; + let events = syscall::create_notification().map_err(|_| "no notification")?; + timer_is_on_time(events)?; + // Revoke must unmap the page on every hart; the spinner then faults and dies. let spinner = start_spinner(storage, scratch, page)?; + syscall::watch_death(spinner, events).map_err(|_| "cannot watch the spinner")?; if !syscall::revoke(page) { return Err("revoke failed"); } @@ -80,18 +84,43 @@ fn run_phases(storage: &mut Storage, scratch: &mut [u8]) -> Result<(), &'static if syscall::kill(spinner) { return Err("the spinner survived losing its page"); } + if syscall::poll(events) != Ok(1) { + return Err("nobody heard the spinner fault"); + } libkos::println!("smptest: revoke stops every hart: ok"); // Kill must stop threads running on other harts before it returns. let spinner = start_spinner(storage, scratch, page)?; + syscall::watch_death(spinner, events).map_err(|_| "cannot watch the spinner")?; if !syscall::kill(spinner) { return Err("kill failed"); } check_stopped("a counter moved after its process was killed")?; + if syscall::poll(events) != Ok(1) { + return Err("nobody heard the spinner killed"); + } libkos::println!("smptest: kill stops every hart: ok"); Ok(()) } +/// A timer must wake its waiter at the deadline, not at the next scheduling +/// tick (a second away). +fn timer_is_on_time(events: CapSlot) -> Result<(), &'static str> { + let start = syscall::get_time().raw(); + let deadline = start + 50 * TICKS_PER_MS; + syscall::set_timer(events, types::time::Ticks::from_raw(deadline)).map_err(|_| "set_timer failed")?; + syscall::wait(events).map_err(|_| "wait failed")?; + let woke = syscall::get_time().raw(); + if woke < deadline { + return Err("the timer fired early"); + } + if woke - deadline > 20 * TICKS_PER_MS { + return Err("the timer fired late"); + } + libkos::println!("smptest: timer wakes on time: ok"); + Ok(()) +} + /// Runs the test, reports, and halts the machine. pub fn run(storage: &mut Storage, scratch: &mut [u8]) -> ! { match run_phases(storage, scratch) { diff --git a/crates/kernel/src/abi.rs b/crates/kernel/src/abi.rs index 23321c6..896970f 100644 --- a/crates/kernel/src/abi.rs +++ b/crates/kernel/src/abi.rs @@ -136,6 +136,14 @@ pub fn decode(nr: usize, a: &[usize; 6]) -> Option { SYS_REGION_ADDRESS => Syscall::RegionAddress { region: slot(a[0])?, }, + SYS_WATCH_DEATH => Syscall::WatchDeath { + process: slot(a[0])?, + notification: slot(a[1])?, + }, + SYS_SET_TIMER => Syscall::SetTimer { + notification: slot(a[0])?, + deadline: types::time::Ticks::from_raw(a[1] as u64), + }, SYS_DEBUG_PUTCHAR => Syscall::DebugPutchar { console: slot(a[0])?, byte: a[1] as u8, diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 0549eec..3980a74 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -77,6 +77,9 @@ pub struct Kernel { tree: domain::capability::tree::CapTree<{ captree::ALL_CAPS }>, /// Where each process's next heap search starts (see memory.rs). heap_cursors: [usize; MAX_PROCESSES], + /// Who to signal when each process dies (see syscall/ipc/events.rs). + death_watchers: [Option; MAX_PROCESSES], + timers: domain::timer::Timers, } impl Kernel

{ @@ -99,6 +102,8 @@ impl Kernel

{ homes: [types::id::CoreId::from_raw(0); MAX_THREADS], tree: domain::capability::tree::CapTree::new(), heap_cursors: [0; MAX_PROCESSES], + death_watchers: [None; MAX_PROCESSES], + timers: domain::timer::Timers::new(), } } @@ -133,6 +138,8 @@ impl Kernel

{ addr_of_mut!((*slot).homes).write([types::id::CoreId::from_raw(0); MAX_THREADS]); addr_of_mut!((*slot).tree).write(domain::capability::tree::CapTree::new()); addr_of_mut!((*slot).heap_cursors).write([0; MAX_PROCESSES]); + addr_of_mut!((*slot).death_watchers).write([None; MAX_PROCESSES]); + addr_of_mut!((*slot).timers).write(domain::timer::Timers::new()); } } diff --git a/crates/kernel/src/proofs/platform.rs b/crates/kernel/src/proofs/platform.rs index 359550d..51980a9 100644 --- a/crates/kernel/src/proofs/platform.rs +++ b/crates/kernel/src/proofs/platform.rs @@ -6,7 +6,7 @@ use types::{ address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress}, id::{CoreId, IrqId, ProcessId}, mapping::PagePerms, - time::{Ticks, TimerDuration}, + time::Ticks, }; pub struct KaniPlatform; @@ -43,7 +43,7 @@ impl Platform for KaniPlatform { kani::assume(false); loop {} } - fn set_timer(&mut self, _duration: TimerDuration) {} + fn wake_at(&mut self, _deadline: Ticks) {} fn current_ticks(&self) -> Ticks { Ticks::from_raw(0) } diff --git a/crates/kernel/src/syscall/ipc/events.rs b/crates/kernel/src/syscall/ipc/events.rs new file mode 100644 index 0000000..b05915a --- /dev/null +++ b/crates/kernel/src/syscall/ipc/events.rs @@ -0,0 +1,90 @@ +//! Events the kernel reports as notification signals: a watched process +//! dying, a timer's deadline passing (see `domain::timer`). + +use domain::{error::KernelError, resource::Resource, timer::Target}; +use platform::Platform; +use types::{bounded::CapSlot, id::ProcessId, permission::Permission, time::Ticks}; + +use crate::Kernel; + +impl Kernel

{ + /// Signalling a notification is the Write right on it. + fn target(&self, pid: ProcessId, slot: CapSlot) -> Result { + let (notification, badge, generation) = + self.notification_cap(pid, slot, Permission::Write)?; + Ok(Target { + notification, + generation, + badge, + }) + } + + /// Signals `target` unless its notification has died since. + fn fire(&mut self, target: Target) { + if self.notifications.generation(target.notification) == Some(target.generation) { + self.signal_notification(target.notification, target.badge); + } + } + + pub(crate) fn watch_death( + &mut self, + pid: ProcessId, + process: CapSlot, + notification: CapSlot, + ) -> Result<(), KernelError> { + let watched = match self + .check_cap(pid, process, Permission::Execute)? + .resource() + { + Resource::Process(p) => *p, + _ => return Err(KernelError::WrongObject), + }; + let target = self.target(pid, notification)?; + let slot = self + .death_watchers + .get_mut(watched.raw() as usize) + .ok_or(KernelError::InvalidProcess(watched))?; + *slot = Some(target); + Ok(()) + } + + /// A process is dying: tell its watcher, forget its own timer. + pub(crate) fn report_death(&mut self, pid: ProcessId) { + self.timers.clear(pid); + if let Some(target) = self + .death_watchers + .get_mut(pid.raw() as usize) + .and_then(Option::take) + { + self.fire(target); + } + } + + pub(crate) fn set_timer( + &mut self, + pid: ProcessId, + notification: CapSlot, + deadline: Ticks, + ) -> Result<(), KernelError> { + let target = self.target(pid, notification)?; + if deadline.raw() == 0 { + self.timers.clear(pid); + return Ok(()); + } + self.timers.set(pid, deadline, target); + self.fire_timers(); + Ok(()) + } + + /// Signals every timer that is due and has this core woken for the next + /// one. Call on every timer interrupt, whether or not a thread is running. + pub fn fire_timers(&mut self) { + let now = self.platform.current_ticks(); + while let Some(target) = self.timers.take_expired(now) { + self.fire(target); + } + if let Some(next) = self.timers.earliest() { + self.platform.wake_at(next); + } + } +} diff --git a/crates/kernel/src/syscall/ipc/mod.rs b/crates/kernel/src/syscall/ipc/mod.rs index 6a462c2..61a0a4c 100644 --- a/crates/kernel/src/syscall/ipc/mod.rs +++ b/crates/kernel/src/syscall/ipc/mod.rs @@ -1,6 +1,7 @@ //! Endpoint IPC, notifications and interrupt delivery (see docs/ipc.md). mod endpoint; +mod events; mod irq; mod notification; diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index 8a7b774..d40d1d4 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -102,6 +102,20 @@ impl Kernel

{ self.region_address_cap(pid, region) .map(|pa| SyscallResult::Value(pa.raw())), ), + Syscall::WatchDeath { + process, + notification, + } => complete( + self.watch_death(pid, process, notification) + .map(|()| SyscallResult::Ok), + ), + Syscall::SetTimer { + notification, + deadline, + } => complete( + self.set_timer(pid, notification, deadline) + .map(|()| SyscallResult::Ok), + ), Syscall::Delegate { slot, to_endpoint, diff --git a/crates/kernel/src/teardown.rs b/crates/kernel/src/teardown.rs index 174e925..c01a559 100644 --- a/crates/kernel/src/teardown.rs +++ b/crates/kernel/src/teardown.rs @@ -22,6 +22,7 @@ impl Kernel

{ Err(_) => return, }; + self.report_death(pid); self.cleanup_threads(&thread_ids); self.delete_all_caps(pid); self.destroy_endpoints_owned_by(pid); diff --git a/crates/kernel/src/tests/abi.rs b/crates/kernel/src/tests/abi.rs index 86e1168..2561ce4 100644 --- a/crates/kernel/src/tests/abi.rs +++ b/crates/kernel/src/tests/abi.rs @@ -42,6 +42,8 @@ fn every_defined_syscall_decodes() { SYS_CREATE_REGION, SYS_MAP_REGION, SYS_REGION_ADDRESS, + SYS_WATCH_DEATH, + SYS_SET_TIMER, ]; // slot 1, slot 2, small values, no cap let args = [1, 2, 1, 0, 0, NONE]; @@ -52,7 +54,7 @@ fn every_defined_syscall_decodes() { #[test] fn retired_and_unknown_numbers_do_not_decode() { - for nr in [3, 5, 36, 1000, usize::MAX] { + for nr in [3, 5, 38, 1000, usize::MAX] { assert!(decode(nr, &[0; 6]).is_none(), "{nr} decoded"); } } diff --git a/crates/kernel/src/tests/events.rs b/crates/kernel/src/tests/events.rs new file mode 100644 index 0000000..c15c069 --- /dev/null +++ b/crates/kernel/src/tests/events.rs @@ -0,0 +1,163 @@ +//! Death watches and timers: the kernel's two events, both delivered as +//! notification signals. + +use super::helpers::CHILD_LIMITS; +use super::*; +use crate::{SyscallOutcome, SyscallResult}; +use domain::fault::Fault; +use domain::resource::Resource; +use domain::syscall::Syscall; +use types::address::DataRef; +use types::bounded::{CapSlot, Priority}; +use types::time::{Ticks, TimeSlice}; + +fn child(k: &mut Kernel) -> CapSlot { + match k.handle_syscall(Syscall::CreateProcess { + create_cap: CapSlot::new(0).unwrap(), + image: DataRef::empty(), + limits: CHILD_LIMITS, + }) { + SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, + _ => panic!("create process"), + } +} + +/// Init with a child it watches, and the notification it watches with. +fn watching() -> (Kernel, ProcessId, CapSlot, CapSlot) { + let (mut k, init) = kernel_with_process(); + let proc_cap = child(&mut k); + let ntfn = k.create_notification_cap(init).unwrap(); + k.watch_death(init, proc_cap, ntfn).unwrap(); + (k, init, proc_cap, ntfn) +} + +#[test] +fn a_watcher_hears_a_kill() { + let (mut k, init, proc_cap, ntfn) = watching(); + assert_eq!(k.poll(init, ntfn).unwrap(), 0); + k.handle_syscall(Syscall::Kill { process: proc_cap }); + assert_eq!(k.poll(init, ntfn).unwrap(), 1); +} + +#[test] +fn a_watcher_hears_an_exit_or_a_fault() { + let (mut k, init, proc_cap, ntfn) = watching(); + let pid = k.process_of_cap_for_test(proc_cap); + k.destroy_process(pid); + assert_eq!(k.poll(init, ntfn).unwrap(), 1); + + let proc_cap = child(&mut k); + k.watch_death(init, proc_cap, ntfn).unwrap(); + let pid = k.process_of_cap_for_test(proc_cap); + let tid = k.spawn_thread_for_test(pid, Priority::DEFAULT, TimeSlice::DEFAULT); + k.handle_fault(tid, Fault::IllegalInstruction, 0); + assert_eq!(k.poll(init, ntfn).unwrap(), 1); +} + +#[test] +fn watching_needs_a_process_cap_and_a_notification_to_signal() { + let (mut k, init) = kernel_with_process(); + let proc_cap = child(&mut k); + let ntfn = k.create_notification_cap(init).unwrap(); + assert!(k.watch_death(init, ntfn, ntfn).is_err()); + assert!(k.watch_death(init, proc_cap, proc_cap).is_err()); + k.handle_syscall(Syscall::Kill { process: proc_cap }); + assert_eq!(k.poll(init, ntfn).unwrap(), 0); +} + +#[test] +fn a_dead_notification_is_never_signalled() { + let (mut k, init) = kernel_with_process(); + let watched = child(&mut k); + let watcher = child(&mut k); + let watcher_pid = k.process_of_cap_for_test(watcher); + // The watcher's own notification, which dies with it. + let ntfn = k.create_notification_cap(watcher_pid).unwrap(); + let theirs = watched_in(&mut k, watched, watcher_pid); + k.watch_death(watcher_pid, theirs, ntfn).unwrap(); + k.destroy_process(watcher_pid); + // A new notification may now reuse the id; the old watch must not hit it. + let fresh = k.create_notification_cap(init).unwrap(); + k.handle_syscall(Syscall::Kill { process: watched }); + assert_eq!(k.poll(init, fresh).unwrap(), 0); +} + +/// A copy of init's `slot` in `to`'s table. +fn watched_in(k: &mut Kernel, slot: CapSlot, to: ProcessId) -> CapSlot { + let pid = k.process_of_cap_for_test(slot); + k.grant_cap_for_test( + to, + Resource::Process(pid), + types::permission::PermissionSet::empty().with(types::permission::Permission::Execute), + ) +} + +#[test] +fn a_timer_fires_at_its_deadline_and_not_before() { + let (mut k, init) = kernel_with_process(); + let ntfn = k.create_notification_cap(init).unwrap(); + k.platform_mut().set_ticks(Ticks::from_raw(10)); + k.set_timer(init, ntfn, Ticks::from_raw(100)).unwrap(); + assert_eq!(k.platform_mut().wake_at, Some(Ticks::from_raw(100))); + k.platform_mut().set_ticks(Ticks::from_raw(99)); + k.fire_timers(); + assert_eq!(k.poll(init, ntfn).unwrap(), 0); + k.platform_mut().set_ticks(Ticks::from_raw(100)); + k.fire_timers(); + assert_eq!(k.poll(init, ntfn).unwrap(), 1); + // Once only. + k.fire_timers(); + assert_eq!(k.poll(init, ntfn).unwrap(), 0); +} + +#[test] +fn a_past_deadline_fires_at_once_and_zero_cancels() { + let (mut k, init) = kernel_with_process(); + let ntfn = k.create_notification_cap(init).unwrap(); + k.platform_mut().set_ticks(Ticks::from_raw(50)); + k.set_timer(init, ntfn, Ticks::from_raw(20)).unwrap(); + assert_eq!(k.poll(init, ntfn).unwrap(), 1); + k.set_timer(init, ntfn, Ticks::from_raw(60)).unwrap(); + k.set_timer(init, ntfn, Ticks::from_raw(0)).unwrap(); + k.platform_mut().set_ticks(Ticks::from_raw(70)); + k.fire_timers(); + assert_eq!(k.poll(init, ntfn).unwrap(), 0); +} + +#[test] +fn a_timer_dies_with_its_process() { + let (mut k, init) = kernel_with_process(); + let ntfn = k.create_notification_cap(init).unwrap(); + let proc_cap = child(&mut k); + let pid = k.process_of_cap_for_test(proc_cap); + let theirs = k.grant_cap_for_test( + pid, + Resource::Notification(notification_id(&k, init, ntfn)), + types::permission::PermissionSet::empty().with(types::permission::Permission::Write), + ); + k.platform_mut().set_ticks(Ticks::from_raw(0)); + k.set_timer(pid, theirs, Ticks::from_raw(10)).unwrap(); + k.destroy_process(pid); + k.platform_mut().set_ticks(Ticks::from_raw(20)); + k.fire_timers(); + assert_eq!(k.poll(init, ntfn).unwrap(), 0); +} + +fn notification_id( + k: &Kernel, + pid: ProcessId, + slot: CapSlot, +) -> types::id::NotificationId { + match k + .processes + .get(pid) + .unwrap() + .capabilities() + .get(slot) + .unwrap() + .resource() + { + Resource::Notification(id) => *id, + _ => panic!("not a notification"), + } +} diff --git a/crates/kernel/src/tests/mock_platform.rs b/crates/kernel/src/tests/mock_platform.rs index 69803ea..9f0bfc2 100644 --- a/crates/kernel/src/tests/mock_platform.rs +++ b/crates/kernel/src/tests/mock_platform.rs @@ -6,7 +6,7 @@ use types::{ address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress}, id::{CoreId, IrqId, ProcessId}, mapping::PagePerms, - time::{Ticks, TimerDuration}, + time::Ticks, }; const MAX_SPACES: usize = 64; @@ -31,6 +31,8 @@ pub struct MockPlatform { lies: BTreeMap<(u16, usize), usize>, /// Cores an IPI was sent to and that have not taken it yet. ipis: [bool; 8], + /// The last deadline the kernel asked to be woken at. + pub wake_at: Option, } impl MockPlatform { @@ -45,6 +47,7 @@ impl MockPlatform { zeroed: BTreeSet::new(), lies: BTreeMap::new(), ipis: [false; 8], + wake_at: None, } } @@ -172,7 +175,9 @@ impl Platform for MockPlatform { fn enable_user_mode(&mut self, _entry: EntryPoint, _stack: StackPointer) -> ! { panic!("enable_user_mode is not supported in tests") } - fn set_timer(&mut self, _duration: TimerDuration) {} + fn wake_at(&mut self, deadline: Ticks) { + self.wake_at = Some(deadline); + } fn current_ticks(&self) -> Ticks { self.ticks } diff --git a/crates/kernel/src/tests/mod.rs b/crates/kernel/src/tests/mod.rs index 473cab3..ac66e3f 100644 --- a/crates/kernel/src/tests/mod.rs +++ b/crates/kernel/src/tests/mod.rs @@ -7,6 +7,7 @@ mod board; mod budget; mod capability; mod captree_model; +mod events; mod exit; mod invariants; mod ipc; diff --git a/crates/libkos/src/syscall/notify.rs b/crates/libkos/src/syscall/notify.rs index b0b1f1b..e89f23a 100644 --- a/crates/libkos/src/syscall/notify.rs +++ b/crates/libkos/src/syscall/notify.rs @@ -56,3 +56,22 @@ pub fn ack_interrupt(irq: CapSlot) -> Result<()> { ) .map(|_| ()) } + +/// Signals `notification` when the process behind `process` dies. +pub fn watch_death(process: CapSlot, notification: CapSlot) -> Result<()> { + raw( + SYS_WATCH_DEATH, + [process.value() as usize, notification.value() as usize, 0, 0, 0, 0], + ) + .map(|_| ()) +} + +/// Signals `notification` once `get_time()` reaches `deadline`; replaces +/// this process's timer. A zero deadline cancels it. +pub fn set_timer(notification: CapSlot, deadline: types::time::Ticks) -> Result<()> { + raw( + SYS_SET_TIMER, + [notification.value() as usize, deadline.raw() as usize, 0, 0, 0, 0], + ) + .map(|_| ()) +} diff --git a/crates/platform/src/lib.rs b/crates/platform/src/lib.rs index 3c3bee7..abaad62 100644 --- a/crates/platform/src/lib.rs +++ b/crates/platform/src/lib.rs @@ -4,7 +4,7 @@ use types::{ address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress}, id::{CoreId, IrqId, ProcessId}, mapping::PagePerms, - time::{Ticks, TimerDuration}, + time::Ticks, }; /// Supplies frames for page tables while the platform builds an address @@ -54,7 +54,9 @@ pub trait Platform { fn zero_frame(&mut self, pa: PhysicalAddress); fn enable_user_mode(&mut self, entry: EntryPoint, stack: StackPointer) -> !; - fn set_timer(&mut self, duration: TimerDuration); + /// Makes sure this core takes a timer interrupt by `deadline`, on top + /// of its scheduling tick. + fn wake_at(&mut self, deadline: Ticks); fn current_ticks(&self) -> Ticks; fn mask_irq(&mut self, irq: IrqId); fn unmask_irq(&mut self, irq: IrqId); diff --git a/crates/types/src/syscall_abi.rs b/crates/types/src/syscall_abi.rs index 629b115..8dd3787 100644 --- a/crates/types/src/syscall_abi.rs +++ b/crates/types/src/syscall_abi.rs @@ -41,6 +41,8 @@ pub const SYS_SET_IRQ_NOTIFICATION: usize = 32; pub const SYS_CREATE_REGION: usize = 33; pub const SYS_MAP_REGION: usize = 34; pub const SYS_REGION_ADDRESS: usize = 35; +pub const SYS_WATCH_DEATH: usize = 36; +pub const SYS_SET_TIMER: usize = 37; /// `PagePerms` in a register. pub const PERMS_READ_ONLY: usize = 0; diff --git a/crates/types/src/time.rs b/crates/types/src/time.rs index 7b5ea7c..0bcb768 100644 --- a/crates/types/src/time.rs +++ b/crates/types/src/time.rs @@ -15,9 +15,6 @@ impl Ticks { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct TimerDuration(u32); - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TimeSlice { remaining: u32, diff --git a/docs/ipc.md b/docs/ipc.md index f8cd535..1a8f28c 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -185,7 +185,26 @@ pass to that parent, so their budgets return there in turn. Destroying an endpoint or notification wakes every thread blocked on it with `InvalidCap`. Destroying a thread removes it from every queue, binding and -reply slot; a dying process loses every IRQ route it set. +reply slot; a dying process loses every IRQ route it set and its timer. + +## Events + +Two things the kernel notices itself, and reports by signalling a +notification (with that notification capability's badge, or bit 0 if it +has none). Both need `Write` on the notification, the right to signal it. + +- `WatchDeath(process, notification)`: needs `Execute` on the process + capability. The notification is signalled when that process dies, whether + by `Exit`, a fault or `Kill`. One watcher per process; a new one replaces + the old. Badge the notification per child to tell children apart. +- `SetTimer(notification, deadline)`: signals once `GetTime` reaches + `deadline` (absolute, in `GetTime` ticks). One timer per process; setting + it again replaces it, a zero deadline cancels it, a past one fires at once. + The adapter programs the hardware for the earliest deadline, so a timer + fires on time rather than on the next scheduling tick. + +A target whose notification has since been destroyed is never signalled, +even if its id was reused. ## Register ABI (RISC-V)