feat(kernel): seL4-style IPC, notifications, status ABI
- badges + Mint, Call/Reply/ReplyRecv w/ gen-checked reply slot - cap transfer (1/msg, copy, needs Delegate, rechecked at delivery) - notifications: signal/wait/poll/bind, irq -> notification routing - WaitAny/WaitForInterrupt removed; a0=status, ErrorCode - libkos: single ecall primitive, compat wrappers + typed new api - fix: trap return clobbered user t0 on every trap - fix: wait queues FIFO, delegate keeps badge, fault cap checked - docs/ipc.md spec, ipc tests, ipc fuzz, kani ntfn + waitqueue
This commit is contained in:
186
crates/domain/src/notification/mod.rs
Normal file
186
crates/domain/src/notification/mod.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
//! Asynchronous signals: a word of bits, a FIFO of waiters and at most one
|
||||
//! bound thread. See docs/ipc.md.
|
||||
|
||||
use types::{
|
||||
badge::Badge,
|
||||
generation::Generation,
|
||||
id::{NotificationId, ProcessId, ThreadId},
|
||||
};
|
||||
|
||||
use crate::{endpoint::wait_queue::WaitQueue, generations::SlotGenerations};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
|
||||
#[cfg(kani)]
|
||||
#[path = "proofs.rs"]
|
||||
mod proofs;
|
||||
|
||||
pub const MAX_NOTIFICATIONS: usize = 128;
|
||||
|
||||
/// What the kernel must do after a signal.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SignalEffect {
|
||||
/// Wake this blocked `Wait`er with these bits.
|
||||
WakeWaiter(ThreadId, usize),
|
||||
/// Wake the bound thread out of its `Receive` with these bits.
|
||||
WakeBound(ThreadId, usize),
|
||||
/// Nobody to wake; the bits were stored.
|
||||
Stored,
|
||||
}
|
||||
|
||||
pub struct Notification {
|
||||
id: NotificationId,
|
||||
owner: ProcessId,
|
||||
bits: usize,
|
||||
waiters: WaitQueue,
|
||||
bound: Option<ThreadId>,
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
pub fn new(id: NotificationId, owner: ProcessId) -> Self {
|
||||
Self {
|
||||
id,
|
||||
owner,
|
||||
bits: 0,
|
||||
waiters: WaitQueue::new(),
|
||||
bound: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> NotificationId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn owner(&self) -> ProcessId {
|
||||
self.owner
|
||||
}
|
||||
|
||||
pub fn bound(&self) -> Option<ThreadId> {
|
||||
self.bound
|
||||
}
|
||||
|
||||
/// Delivers a signal. `bound_is_receiving` says whether the bound
|
||||
/// thread is currently blocked in `Receive` and can be woken.
|
||||
pub fn signal(&mut self, badge: Badge, bound_is_receiving: bool) -> SignalEffect {
|
||||
let bits = if badge.is_none() { 1 } else { badge.raw() };
|
||||
if let Some(waiter) = self.waiters.pop() {
|
||||
return SignalEffect::WakeWaiter(waiter, bits);
|
||||
}
|
||||
if let (Some(bound), true) = (self.bound, bound_is_receiving) {
|
||||
return SignalEffect::WakeBound(bound, bits);
|
||||
}
|
||||
self.bits |= bits;
|
||||
SignalEffect::Stored
|
||||
}
|
||||
|
||||
/// Returns and clears the pending bits.
|
||||
pub fn take_bits(&mut self) -> usize {
|
||||
core::mem::take(&mut self.bits)
|
||||
}
|
||||
|
||||
pub fn has_bits(&self) -> bool {
|
||||
self.bits != 0
|
||||
}
|
||||
|
||||
pub fn is_waiter(&self, thread: ThreadId) -> bool {
|
||||
self.waiters.contains(thread)
|
||||
}
|
||||
|
||||
pub fn add_waiter(&mut self, thread: ThreadId) -> bool {
|
||||
self.waiters.push(thread).is_some()
|
||||
}
|
||||
|
||||
pub fn bind(&mut self, thread: ThreadId) -> bool {
|
||||
if self.bound.is_some() {
|
||||
return false;
|
||||
}
|
||||
self.bound = Some(thread);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn unbind(&mut self, thread: ThreadId) {
|
||||
if self.bound == Some(thread) {
|
||||
self.bound = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Forgets `thread` as waiter and as bound thread.
|
||||
pub fn remove_thread(&mut self, thread: ThreadId) {
|
||||
self.waiters.remove_thread(thread);
|
||||
self.unbind(thread);
|
||||
}
|
||||
|
||||
/// Drains every waiter, e.g. when the notification is destroyed.
|
||||
pub fn pop_waiter(&mut self) -> Option<ThreadId> {
|
||||
self.waiters.pop()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NotificationTable {
|
||||
slots: [Option<Notification>; MAX_NOTIFICATIONS],
|
||||
generations: SlotGenerations<MAX_NOTIFICATIONS>,
|
||||
}
|
||||
|
||||
impl NotificationTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
slots: [const { None }; MAX_NOTIFICATIONS],
|
||||
generations: SlotGenerations::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(&mut self, owner: ProcessId) -> Option<NotificationId> {
|
||||
let idx = (0..MAX_NOTIFICATIONS)
|
||||
.find(|&i| self.slots[i].is_none() && self.generations.is_usable(i))?;
|
||||
let id = NotificationId::from_raw(idx as u16);
|
||||
self.slots[idx] = Some(Notification::new(id, owner));
|
||||
Some(id)
|
||||
}
|
||||
|
||||
pub fn get(&self, id: NotificationId) -> Option<&Notification> {
|
||||
self.slots.get(id.raw() as usize)?.as_ref()
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, id: NotificationId) -> Option<&mut Notification> {
|
||||
self.slots.get_mut(id.raw() as usize)?.as_mut()
|
||||
}
|
||||
|
||||
pub fn generation(&self, id: NotificationId) -> Option<Generation> {
|
||||
self.get(id)?;
|
||||
self.generations.current(id.raw() as usize)
|
||||
}
|
||||
|
||||
pub fn revoke(&mut self, id: NotificationId) {
|
||||
if self.get(id).is_some() {
|
||||
self.generations.advance(id.raw() as usize);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy(&mut self, id: NotificationId) -> Option<Notification> {
|
||||
let n = self.slots.get_mut(id.raw() as usize)?.take()?;
|
||||
self.generations.advance(id.raw() as usize);
|
||||
Some(n)
|
||||
}
|
||||
|
||||
pub fn ids_owned_by(&self, owner: ProcessId) -> impl Iterator<Item = NotificationId> + '_ {
|
||||
self.slots
|
||||
.iter()
|
||||
.flatten()
|
||||
.filter(move |n| n.owner == owner)
|
||||
.map(|n| n.id)
|
||||
}
|
||||
|
||||
pub fn remove_thread_from_all(&mut self, thread: ThreadId) {
|
||||
for n in self.slots.iter_mut().flatten() {
|
||||
n.remove_thread(thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NotificationTable {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
43
crates/domain/src/notification/proofs.rs
Normal file
43
crates/domain/src/notification/proofs.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
//! Signals are never lost: every signal either wakes exactly one thread
|
||||
//! with its bits, or leaves those bits set for the next `Wait`.
|
||||
|
||||
use super::*;
|
||||
use types::badge::Badge;
|
||||
use types::id::{NotificationId, ProcessId, ThreadId};
|
||||
|
||||
#[kani::proof]
|
||||
#[kani::unwind(5)]
|
||||
fn a_signal_is_delivered_or_stored() {
|
||||
let mut n = Notification::new(NotificationId::from_raw(0), ProcessId::from_raw(0));
|
||||
n.bits = kani::any();
|
||||
if kani::any() {
|
||||
n.add_waiter(ThreadId::from_raw(kani::any::<u16>() % 4));
|
||||
}
|
||||
if kani::any() {
|
||||
n.bind(ThreadId::from_raw(kani::any::<u16>() % 4));
|
||||
}
|
||||
let before = n.bits;
|
||||
let badge = Badge::new(kani::any());
|
||||
let expected = if badge.is_none() { 1 } else { badge.raw() };
|
||||
|
||||
match n.signal(badge, kani::any()) {
|
||||
SignalEffect::WakeWaiter(_, bits) | SignalEffect::WakeBound(_, bits) => {
|
||||
assert!(bits == expected);
|
||||
assert!(n.bits == before, "woken signals are not also stored");
|
||||
}
|
||||
SignalEffect::Stored => {
|
||||
assert!(n.bits == before | expected);
|
||||
assert!(n.bits & expected == expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
#[kani::unwind(5)]
|
||||
fn take_bits_returns_everything_and_clears() {
|
||||
let mut n = Notification::new(NotificationId::from_raw(0), ProcessId::from_raw(0));
|
||||
let bits: usize = kani::any();
|
||||
n.bits = bits;
|
||||
assert!(n.take_bits() == bits);
|
||||
assert!(n.bits == 0);
|
||||
}
|
||||
116
crates/domain/src/notification/tests.rs
Normal file
116
crates/domain/src/notification/tests.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use super::*;
|
||||
use types::badge::Badge;
|
||||
use types::id::{NotificationId, ProcessId, ThreadId};
|
||||
|
||||
fn ntfn() -> Notification {
|
||||
Notification::new(NotificationId::from_raw(0), ProcessId::from_raw(0))
|
||||
}
|
||||
|
||||
fn t(n: u16) -> ThreadId {
|
||||
ThreadId::from_raw(n)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signal_with_nobody_waiting_stores_the_badge() {
|
||||
let mut n = ntfn();
|
||||
assert_eq!(n.signal(Badge::new(0b100), false), SignalEffect::Stored);
|
||||
assert_eq!(n.signal(Badge::new(0b001), false), SignalEffect::Stored);
|
||||
assert_eq!(n.take_bits(), 0b101);
|
||||
assert_eq!(n.take_bits(), 0, "taking clears");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbadged_signal_sets_bit_zero() {
|
||||
let mut n = ntfn();
|
||||
n.signal(Badge::NONE, false);
|
||||
assert_eq!(n.take_bits(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signal_wakes_the_first_waiter_with_its_badge() {
|
||||
let mut n = ntfn();
|
||||
assert!(n.add_waiter(t(1)));
|
||||
assert!(n.add_waiter(t(2)));
|
||||
assert_eq!(
|
||||
n.signal(Badge::new(8), false),
|
||||
SignalEffect::WakeWaiter(t(1), 8)
|
||||
);
|
||||
assert_eq!(n.take_bits(), 0, "delivered bits are not also stored");
|
||||
assert_eq!(
|
||||
n.signal(Badge::new(4), false),
|
||||
SignalEffect::WakeWaiter(t(2), 4)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signal_wakes_a_receiving_bound_thread_when_no_waiter() {
|
||||
let mut n = ntfn();
|
||||
assert!(n.bind(t(5)));
|
||||
assert_eq!(
|
||||
n.signal(Badge::new(2), true),
|
||||
SignalEffect::WakeBound(t(5), 2)
|
||||
);
|
||||
assert_eq!(n.signal(Badge::new(2), false), SignalEffect::Stored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn waiters_take_priority_over_the_bound_thread() {
|
||||
let mut n = ntfn();
|
||||
n.bind(t(5));
|
||||
n.add_waiter(t(1));
|
||||
assert_eq!(
|
||||
n.signal(Badge::new(2), true),
|
||||
SignalEffect::WakeWaiter(t(1), 2)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_one_thread_can_be_bound() {
|
||||
let mut n = ntfn();
|
||||
assert!(n.bind(t(1)));
|
||||
assert!(!n.bind(t(2)));
|
||||
n.unbind(t(1));
|
||||
assert!(n.bind(t(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_waiter_is_not_woken() {
|
||||
let mut n = ntfn();
|
||||
n.add_waiter(t(1));
|
||||
n.remove_thread(t(1));
|
||||
assert_eq!(n.signal(Badge::new(1), false), SignalEffect::Stored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_thread_also_unbinds() {
|
||||
let mut n = ntfn();
|
||||
n.bind(t(1));
|
||||
n.remove_thread(t(1));
|
||||
assert_eq!(n.bound(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_generations_follow_slot_reuse() {
|
||||
let mut table = NotificationTable::new();
|
||||
let owner = ProcessId::from_raw(3);
|
||||
let id = table.create(owner).unwrap();
|
||||
let g = table.generation(id).unwrap();
|
||||
assert!(table.destroy(id).is_some());
|
||||
assert_eq!(table.generation(id), None);
|
||||
let again = table.create(owner).unwrap();
|
||||
assert_eq!(again, id);
|
||||
assert!(!g.is_valid_against(&table.generation(again).unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_lists_notifications_by_owner() {
|
||||
let mut table = NotificationTable::new();
|
||||
let a = table.create(ProcessId::from_raw(1)).unwrap();
|
||||
table.create(ProcessId::from_raw(2)).unwrap();
|
||||
let mut n = 0;
|
||||
for id in table.ids_owned_by(ProcessId::from_raw(1)) {
|
||||
assert_eq!(id, a);
|
||||
n += 1;
|
||||
}
|
||||
assert_eq!(n, 1);
|
||||
}
|
||||
Reference in New Issue
Block a user