feat(kernel): WatchDeath + SetTimer events as notification signals; adapter wakes at earliest deadline
This commit is contained in:
@@ -1,4 +1,10 @@
|
|||||||
|
use core::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
use crate::board;
|
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 {
|
pub fn read_time() -> u64 {
|
||||||
let time: u64;
|
let time: u64;
|
||||||
@@ -21,15 +27,27 @@ fn sbi_set_timer(stime: u64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn init() {
|
pub fn init() {
|
||||||
let now = read_time();
|
set_next(read_time() + board::TICK_INTERVAL);
|
||||||
sbi_set_timer(now + board::TICK_INTERVAL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_next(deadline: u64) {
|
pub fn set_next(deadline: u64) {
|
||||||
|
if let Some(next) = NEXT.get(hart_id()) {
|
||||||
|
next.store(deadline, Ordering::Relaxed);
|
||||||
|
}
|
||||||
sbi_set_timer(deadline);
|
sbi_set_timer(deadline);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_interrupt() {
|
/// Goes off at `deadline` too, if that is before the next tick.
|
||||||
let now = read_time();
|
pub fn wake_at(deadline: u64) {
|
||||||
sbi_set_timer(now + board::TICK_INTERVAL);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ use types::{
|
|||||||
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
|
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
|
||||||
id::{CoreId, IrqId, ProcessId},
|
id::{CoreId, IrqId, ProcessId},
|
||||||
mapping::PagePerms,
|
mapping::PagePerms,
|
||||||
time::{Ticks, TimerDuration},
|
time::Ticks,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::board;
|
|
||||||
use crate::hw::{plic::Plic, timer};
|
use crate::hw::{plic::Plic, timer};
|
||||||
use crate::mmu;
|
use crate::mmu;
|
||||||
use crate::smp;
|
use crate::smp;
|
||||||
@@ -136,9 +135,8 @@ impl Platform for RiscVPlatform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_timer(&mut self, _duration: TimerDuration) {
|
fn wake_at(&mut self, deadline: Ticks) {
|
||||||
let now = timer::read_time();
|
timer::wake_at(deadline.raw());
|
||||||
timer::set_next(now + board::TICK_INTERVAL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_ticks(&self) -> Ticks {
|
fn current_ticks(&self) -> Ticks {
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ fn handle_interrupt(frame: &mut TrapFrame, cause: usize) {
|
|||||||
CAUSE_S_TIMER => {
|
CAUSE_S_TIMER => {
|
||||||
crate::hw::timer::handle_interrupt();
|
crate::hw::timer::handle_interrupt();
|
||||||
let kern = kern();
|
let kern = kern();
|
||||||
|
kern.fire_timers();
|
||||||
if let Some(current_tid) = kern.current_thread() {
|
if let Some(current_tid) = kern.current_thread() {
|
||||||
let decision = kern.handle_tick(this_core());
|
let decision = kern.handle_tick(this_core());
|
||||||
if let domain::scheduler::SchedulerDecision::Preempt(next) = decision {
|
if let domain::scheduler::SchedulerDecision::Preempt(next) = decision {
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ fn next_runnable() -> types::id::ThreadId {
|
|||||||
if sip & (1 << CAUSE_S_TIMER) != 0 {
|
if sip & (1 << CAUSE_S_TIMER) != 0 {
|
||||||
// No thread is running, so there is no time slice to charge.
|
// No thread is running, so there is no time slice to charge.
|
||||||
crate::hw::timer::handle_interrupt();
|
crate::hw::timer::handle_interrupt();
|
||||||
|
kern().fire_timers();
|
||||||
}
|
}
|
||||||
if sip & (1 << CAUSE_S_EXTERNAL) != 0 {
|
if sip & (1 << CAUSE_S_EXTERNAL) != 0 {
|
||||||
dispatch_external();
|
dispatch_external();
|
||||||
|
|||||||
@@ -32,3 +32,4 @@ pub mod resource;
|
|||||||
pub mod scheduler;
|
pub mod scheduler;
|
||||||
pub mod syscall;
|
pub mod syscall;
|
||||||
pub mod thread;
|
pub mod thread;
|
||||||
|
pub mod timer;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use types::{
|
|||||||
bounded::{CapSlot, PageCount},
|
bounded::{CapSlot, PageCount},
|
||||||
mapping::PagePerms,
|
mapping::PagePerms,
|
||||||
permission::PermissionSet,
|
permission::PermissionSet,
|
||||||
|
time::Ticks,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::message::Message;
|
use crate::message::Message;
|
||||||
@@ -91,6 +92,18 @@ pub enum Syscall {
|
|||||||
RegionAddress {
|
RegionAddress {
|
||||||
region: CapSlot,
|
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.
|
/// Removes a capability from the caller's own table.
|
||||||
DropCap {
|
DropCap {
|
||||||
slot: CapSlot,
|
slot: CapSlot,
|
||||||
|
|||||||
67
crates/domain/src/timer.rs
Normal file
67
crates/domain/src/timer.rs
Normal file
@@ -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<Target> {
|
||||||
|
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<Ticks> {
|
||||||
|
self.deadlines.iter().flatten().map(|(d, _)| *d).min()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Timers {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
37
crates/domain/src/timer_tests.rs
Normal file
37
crates/domain/src/timer_tests.rs
Normal file
@@ -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)));
|
||||||
|
}
|
||||||
@@ -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)
|
syscall::map_region(page, VirtualAddress::from_raw(OBSERVE_VA), PagePerms::ReadWrite)
|
||||||
.map_err(|_| "cannot map the counter page")?;
|
.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.
|
// Revoke must unmap the page on every hart; the spinner then faults and dies.
|
||||||
let spinner = start_spinner(storage, scratch, page)?;
|
let spinner = start_spinner(storage, scratch, page)?;
|
||||||
|
syscall::watch_death(spinner, events).map_err(|_| "cannot watch the spinner")?;
|
||||||
if !syscall::revoke(page) {
|
if !syscall::revoke(page) {
|
||||||
return Err("revoke failed");
|
return Err("revoke failed");
|
||||||
}
|
}
|
||||||
@@ -80,18 +84,43 @@ fn run_phases(storage: &mut Storage, scratch: &mut [u8]) -> Result<(), &'static
|
|||||||
if syscall::kill(spinner) {
|
if syscall::kill(spinner) {
|
||||||
return Err("the spinner survived losing its page");
|
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");
|
libkos::println!("smptest: revoke stops every hart: ok");
|
||||||
|
|
||||||
// Kill must stop threads running on other harts before it returns.
|
// Kill must stop threads running on other harts before it returns.
|
||||||
let spinner = start_spinner(storage, scratch, page)?;
|
let spinner = start_spinner(storage, scratch, page)?;
|
||||||
|
syscall::watch_death(spinner, events).map_err(|_| "cannot watch the spinner")?;
|
||||||
if !syscall::kill(spinner) {
|
if !syscall::kill(spinner) {
|
||||||
return Err("kill failed");
|
return Err("kill failed");
|
||||||
}
|
}
|
||||||
check_stopped("a counter moved after its process was killed")?;
|
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");
|
libkos::println!("smptest: kill stops every hart: ok");
|
||||||
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.
|
/// Runs the test, reports, and halts the machine.
|
||||||
pub fn run(storage: &mut Storage, scratch: &mut [u8]) -> ! {
|
pub fn run(storage: &mut Storage, scratch: &mut [u8]) -> ! {
|
||||||
match run_phases(storage, scratch) {
|
match run_phases(storage, scratch) {
|
||||||
|
|||||||
@@ -136,6 +136,14 @@ pub fn decode(nr: usize, a: &[usize; 6]) -> Option<Syscall> {
|
|||||||
SYS_REGION_ADDRESS => Syscall::RegionAddress {
|
SYS_REGION_ADDRESS => Syscall::RegionAddress {
|
||||||
region: slot(a[0])?,
|
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 {
|
SYS_DEBUG_PUTCHAR => Syscall::DebugPutchar {
|
||||||
console: slot(a[0])?,
|
console: slot(a[0])?,
|
||||||
byte: a[1] as u8,
|
byte: a[1] as u8,
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ pub struct Kernel<P: Platform> {
|
|||||||
tree: domain::capability::tree::CapTree<{ captree::ALL_CAPS }>,
|
tree: domain::capability::tree::CapTree<{ captree::ALL_CAPS }>,
|
||||||
/// Where each process's next heap search starts (see memory.rs).
|
/// Where each process's next heap search starts (see memory.rs).
|
||||||
heap_cursors: [usize; MAX_PROCESSES],
|
heap_cursors: [usize; MAX_PROCESSES],
|
||||||
|
/// Who to signal when each process dies (see syscall/ipc/events.rs).
|
||||||
|
death_watchers: [Option<domain::timer::Target>; MAX_PROCESSES],
|
||||||
|
timers: domain::timer::Timers,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: Platform> Kernel<P> {
|
impl<P: Platform> Kernel<P> {
|
||||||
@@ -99,6 +102,8 @@ impl<P: Platform> Kernel<P> {
|
|||||||
homes: [types::id::CoreId::from_raw(0); MAX_THREADS],
|
homes: [types::id::CoreId::from_raw(0); MAX_THREADS],
|
||||||
tree: domain::capability::tree::CapTree::new(),
|
tree: domain::capability::tree::CapTree::new(),
|
||||||
heap_cursors: [0; MAX_PROCESSES],
|
heap_cursors: [0; MAX_PROCESSES],
|
||||||
|
death_watchers: [None; MAX_PROCESSES],
|
||||||
|
timers: domain::timer::Timers::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +138,8 @@ impl<P: Platform> Kernel<P> {
|
|||||||
addr_of_mut!((*slot).homes).write([types::id::CoreId::from_raw(0); MAX_THREADS]);
|
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).tree).write(domain::capability::tree::CapTree::new());
|
||||||
addr_of_mut!((*slot).heap_cursors).write([0; MAX_PROCESSES]);
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use types::{
|
|||||||
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
|
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
|
||||||
id::{CoreId, IrqId, ProcessId},
|
id::{CoreId, IrqId, ProcessId},
|
||||||
mapping::PagePerms,
|
mapping::PagePerms,
|
||||||
time::{Ticks, TimerDuration},
|
time::Ticks,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct KaniPlatform;
|
pub struct KaniPlatform;
|
||||||
@@ -43,7 +43,7 @@ impl Platform for KaniPlatform {
|
|||||||
kani::assume(false);
|
kani::assume(false);
|
||||||
loop {}
|
loop {}
|
||||||
}
|
}
|
||||||
fn set_timer(&mut self, _duration: TimerDuration) {}
|
fn wake_at(&mut self, _deadline: Ticks) {}
|
||||||
fn current_ticks(&self) -> Ticks {
|
fn current_ticks(&self) -> Ticks {
|
||||||
Ticks::from_raw(0)
|
Ticks::from_raw(0)
|
||||||
}
|
}
|
||||||
|
|||||||
90
crates/kernel/src/syscall/ipc/events.rs
Normal file
90
crates/kernel/src/syscall/ipc/events.rs
Normal file
@@ -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<P: Platform> Kernel<P> {
|
||||||
|
/// Signalling a notification is the Write right on it.
|
||||||
|
fn target(&self, pid: ProcessId, slot: CapSlot) -> Result<Target, KernelError> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
//! Endpoint IPC, notifications and interrupt delivery (see docs/ipc.md).
|
//! Endpoint IPC, notifications and interrupt delivery (see docs/ipc.md).
|
||||||
|
|
||||||
mod endpoint;
|
mod endpoint;
|
||||||
|
mod events;
|
||||||
mod irq;
|
mod irq;
|
||||||
mod notification;
|
mod notification;
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,20 @@ impl<P: Platform> Kernel<P> {
|
|||||||
self.region_address_cap(pid, region)
|
self.region_address_cap(pid, region)
|
||||||
.map(|pa| SyscallResult::Value(pa.raw())),
|
.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 {
|
Syscall::Delegate {
|
||||||
slot,
|
slot,
|
||||||
to_endpoint,
|
to_endpoint,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ impl<P: Platform> Kernel<P> {
|
|||||||
Err(_) => return,
|
Err(_) => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.report_death(pid);
|
||||||
self.cleanup_threads(&thread_ids);
|
self.cleanup_threads(&thread_ids);
|
||||||
self.delete_all_caps(pid);
|
self.delete_all_caps(pid);
|
||||||
self.destroy_endpoints_owned_by(pid);
|
self.destroy_endpoints_owned_by(pid);
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ fn every_defined_syscall_decodes() {
|
|||||||
SYS_CREATE_REGION,
|
SYS_CREATE_REGION,
|
||||||
SYS_MAP_REGION,
|
SYS_MAP_REGION,
|
||||||
SYS_REGION_ADDRESS,
|
SYS_REGION_ADDRESS,
|
||||||
|
SYS_WATCH_DEATH,
|
||||||
|
SYS_SET_TIMER,
|
||||||
];
|
];
|
||||||
// slot 1, slot 2, small values, no cap
|
// slot 1, slot 2, small values, no cap
|
||||||
let args = [1, 2, 1, 0, 0, NONE];
|
let args = [1, 2, 1, 0, 0, NONE];
|
||||||
@@ -52,7 +54,7 @@ fn every_defined_syscall_decodes() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn retired_and_unknown_numbers_do_not_decode() {
|
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");
|
assert!(decode(nr, &[0; 6]).is_none(), "{nr} decoded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
163
crates/kernel/src/tests/events.rs
Normal file
163
crates/kernel/src/tests/events.rs
Normal file
@@ -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<MockPlatform>) -> 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<MockPlatform>, 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<MockPlatform>, 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<MockPlatform>,
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ use types::{
|
|||||||
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
|
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
|
||||||
id::{CoreId, IrqId, ProcessId},
|
id::{CoreId, IrqId, ProcessId},
|
||||||
mapping::PagePerms,
|
mapping::PagePerms,
|
||||||
time::{Ticks, TimerDuration},
|
time::Ticks,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_SPACES: usize = 64;
|
const MAX_SPACES: usize = 64;
|
||||||
@@ -31,6 +31,8 @@ pub struct MockPlatform {
|
|||||||
lies: BTreeMap<(u16, usize), usize>,
|
lies: BTreeMap<(u16, usize), usize>,
|
||||||
/// Cores an IPI was sent to and that have not taken it yet.
|
/// Cores an IPI was sent to and that have not taken it yet.
|
||||||
ipis: [bool; 8],
|
ipis: [bool; 8],
|
||||||
|
/// The last deadline the kernel asked to be woken at.
|
||||||
|
pub wake_at: Option<Ticks>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockPlatform {
|
impl MockPlatform {
|
||||||
@@ -45,6 +47,7 @@ impl MockPlatform {
|
|||||||
zeroed: BTreeSet::new(),
|
zeroed: BTreeSet::new(),
|
||||||
lies: BTreeMap::new(),
|
lies: BTreeMap::new(),
|
||||||
ipis: [false; 8],
|
ipis: [false; 8],
|
||||||
|
wake_at: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +175,9 @@ impl Platform for MockPlatform {
|
|||||||
fn enable_user_mode(&mut self, _entry: EntryPoint, _stack: StackPointer) -> ! {
|
fn enable_user_mode(&mut self, _entry: EntryPoint, _stack: StackPointer) -> ! {
|
||||||
panic!("enable_user_mode is not supported in tests")
|
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 {
|
fn current_ticks(&self) -> Ticks {
|
||||||
self.ticks
|
self.ticks
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod board;
|
|||||||
mod budget;
|
mod budget;
|
||||||
mod capability;
|
mod capability;
|
||||||
mod captree_model;
|
mod captree_model;
|
||||||
|
mod events;
|
||||||
mod exit;
|
mod exit;
|
||||||
mod invariants;
|
mod invariants;
|
||||||
mod ipc;
|
mod ipc;
|
||||||
|
|||||||
@@ -56,3 +56,22 @@ pub fn ack_interrupt(irq: CapSlot) -> Result<()> {
|
|||||||
)
|
)
|
||||||
.map(|_| ())
|
.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(|_| ())
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use types::{
|
|||||||
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
|
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
|
||||||
id::{CoreId, IrqId, ProcessId},
|
id::{CoreId, IrqId, ProcessId},
|
||||||
mapping::PagePerms,
|
mapping::PagePerms,
|
||||||
time::{Ticks, TimerDuration},
|
time::Ticks,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Supplies frames for page tables while the platform builds an address
|
/// 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 zero_frame(&mut self, pa: PhysicalAddress);
|
||||||
|
|
||||||
fn enable_user_mode(&mut self, entry: EntryPoint, stack: StackPointer) -> !;
|
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 current_ticks(&self) -> Ticks;
|
||||||
fn mask_irq(&mut self, irq: IrqId);
|
fn mask_irq(&mut self, irq: IrqId);
|
||||||
fn unmask_irq(&mut self, irq: IrqId);
|
fn unmask_irq(&mut self, irq: IrqId);
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ pub const SYS_SET_IRQ_NOTIFICATION: usize = 32;
|
|||||||
pub const SYS_CREATE_REGION: usize = 33;
|
pub const SYS_CREATE_REGION: usize = 33;
|
||||||
pub const SYS_MAP_REGION: usize = 34;
|
pub const SYS_MAP_REGION: usize = 34;
|
||||||
pub const SYS_REGION_ADDRESS: usize = 35;
|
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.
|
/// `PagePerms` in a register.
|
||||||
pub const PERMS_READ_ONLY: usize = 0;
|
pub const PERMS_READ_ONLY: usize = 0;
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ impl Ticks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct TimerDuration(u32);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct TimeSlice {
|
pub struct TimeSlice {
|
||||||
remaining: u32,
|
remaining: u32,
|
||||||
|
|||||||
21
docs/ipc.md
21
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
|
Destroying an endpoint or notification wakes every thread blocked on it with
|
||||||
`InvalidCap`. Destroying a thread removes it from every queue, binding and
|
`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)
|
## Register ABI (RISC-V)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user