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:
2026-10-26 20:27:38 +01:00
parent d43421e33e
commit 76f10ff1f1
46 changed files with 3339 additions and 1296 deletions

View File

@@ -1,4 +1,4 @@
use types::{bounded::CapSlot, generation::Generation, permission::PermissionSet};
use types::{badge::Badge, bounded::CapSlot, generation::Generation, permission::PermissionSet};
pub mod authority;
@@ -11,10 +11,12 @@ mod tests;
// Sized so that every representable `CapSlot` indexes a real slot.
pub const MAX_CAPS_PER_PROCESS: usize = CapSlot::MAX as usize + 1;
#[derive(Clone)]
pub struct Capability {
resource: Resource,
permissions: PermissionSet,
generation: Generation,
badge: Badge,
}
impl Capability {
@@ -23,9 +25,20 @@ impl Capability {
resource,
permissions,
generation,
badge: Badge::NONE,
}
}
/// The same capability with `badge` stamped on it.
pub fn with_badge(mut self, badge: Badge) -> Self {
self.badge = badge;
self
}
pub fn badge(&self) -> Badge {
self.badge
}
pub fn resource(&self) -> &Resource {
&self.resource
}

View File

@@ -57,6 +57,14 @@ impl Endpoint {
self.waiting_receivers.pop()
}
pub fn is_sender(&self, thread: ThreadId) -> bool {
self.waiting_senders.contains(thread)
}
pub fn is_receiver(&self, thread: ThreadId) -> bool {
self.waiting_receivers.contains(thread)
}
pub fn has_waiting_sender(&self) -> bool {
!self.waiting_senders.is_empty()
}

View File

@@ -19,21 +19,21 @@ fn test_endpoint() -> Endpoint {
#[test]
fn empty_queue_pop_returns_none() {
let mut queue = WaitQueue::new();
let mut queue: WaitQueue = WaitQueue::new();
assert!(queue.pop().is_none());
assert!(queue.is_empty());
}
#[test]
fn push_then_pop_returns_same_thread() {
let mut queue = WaitQueue::new();
let mut queue: WaitQueue = WaitQueue::new();
queue.push(thread(1)).unwrap();
assert_eq!(queue.pop(), Some(thread(1)));
}
#[test]
fn pop_order_is_fifo() {
let mut queue = WaitQueue::new();
let mut queue: WaitQueue = WaitQueue::new();
queue.push(thread(1)).unwrap();
queue.push(thread(2)).unwrap();
queue.push(thread(3)).unwrap();
@@ -46,7 +46,7 @@ fn pop_order_is_fifo() {
#[test]
fn full_queue_rejects_push() {
let mut queue = WaitQueue::new();
let mut queue: WaitQueue = WaitQueue::new();
for i in 0..MAX_WAITERS {
assert!(queue.push(thread(i as u16)).is_some());
}
@@ -55,7 +55,7 @@ fn full_queue_rejects_push() {
#[test]
fn is_empty_after_drain() {
let mut queue = WaitQueue::new();
let mut queue: WaitQueue = WaitQueue::new();
queue.push(thread(1)).unwrap();
queue.push(thread(2)).unwrap();
queue.pop();
@@ -219,3 +219,40 @@ fn table_destroy_reuses_slot() {
let new_id = table.create(owner()).unwrap();
assert_eq!(id, new_id);
}
#[test]
fn wait_queue_is_fifo_even_after_a_pop_leaves_a_hole() {
let mut q: WaitQueue = WaitQueue::new();
q.push(thread(1)).unwrap();
q.push(thread(2)).unwrap();
assert_eq!(q.pop(), Some(thread(1)));
q.push(thread(3)).unwrap();
assert_eq!(q.pop(), Some(thread(2)), "thread 3 jumped the queue");
assert_eq!(q.pop(), Some(thread(3)));
}
#[test]
fn wait_queue_keeps_order_around_a_removed_thread() {
let mut q: WaitQueue = WaitQueue::new();
for n in 1..=3 {
q.push(thread(n)).unwrap();
}
assert!(q.remove_thread(thread(2)));
q.push(thread(4)).unwrap();
assert_eq!(q.pop(), Some(thread(1)));
assert_eq!(q.pop(), Some(thread(3)));
assert_eq!(q.pop(), Some(thread(4)));
assert_eq!(q.pop(), None);
}
#[test]
fn wait_queue_kani_trace_push_push_remove() {
let mut q: WaitQueue<3> = WaitQueue::new();
q.push(thread(3)).unwrap();
q.push(thread(2)).unwrap();
assert!(q.remove_thread(thread(3)));
assert!(!q.remove_thread(thread(0)));
assert!(!q.remove_thread(thread(3)));
assert_eq!(q.pop(), Some(thread(2)));
assert_eq!(q.pop(), None);
}

View File

@@ -2,51 +2,127 @@ use types::id::ThreadId;
pub const MAX_WAITERS: usize = 16;
pub struct WaitQueue {
entries: [Option<ThreadId>; MAX_WAITERS],
count: u8,
/// Bounded FIFO of blocked threads. Order is preserved across pops and
/// removals, so no waiter can be overtaken.
pub struct WaitQueue<const N: usize = MAX_WAITERS> {
entries: [Option<ThreadId>; N],
len: usize,
}
impl WaitQueue {
impl<const N: usize> WaitQueue<N> {
pub fn new() -> Self {
Self {
entries: [None; MAX_WAITERS],
count: 0,
entries: [None; N],
len: 0,
}
}
pub fn push(&mut self, thread: ThreadId) -> Option<()> {
let slot = self.entries.iter_mut().find(|s| s.is_none())?;
*slot = Some(thread);
self.count += 1;
if self.len == N || self.contains(thread) {
return None;
}
self.entries[self.len] = Some(thread);
self.len += 1;
Some(())
}
pub fn pop(&mut self) -> Option<ThreadId> {
let slot = self.entries.iter_mut().find(|s| s.is_some())?;
let thread = slot.take();
self.count -= 1;
thread
let head = self.entries[0]?;
self.close_gap(0);
Some(head)
}
pub fn remove_thread(&mut self, thread: ThreadId) -> bool {
for slot in &mut self.entries {
if *slot == Some(thread) {
*slot = None;
self.count -= 1;
return true;
match self.entries[..self.len]
.iter()
.position(|e| *e == Some(thread))
{
Some(i) => {
self.close_gap(i);
true
}
None => false,
}
false
}
pub fn contains(&self, thread: ThreadId) -> bool {
self.entries[..self.len].contains(&Some(thread))
}
pub fn is_empty(&self) -> bool {
self.count == 0
self.len == 0
}
fn close_gap(&mut self, i: usize) {
for j in i..self.len - 1 {
self.entries[j] = self.entries[j + 1];
}
self.len -= 1;
self.entries[self.len] = None;
}
}
impl Default for WaitQueue {
impl<const N: usize> Default for WaitQueue<N> {
fn default() -> Self {
Self::new()
}
}
#[cfg(kani)]
mod proofs {
use super::*;
/// Against a simple reference model (a plain array kept in arrival
/// order), any sequence of pushes, pops and removals behaves the same:
/// FIFO order, no duplicates, nothing lost.
#[kani::proof]
#[kani::unwind(7)]
fn behaves_like_a_fifo_without_duplicates() {
const CAP: usize = 3;
let mut q: WaitQueue<CAP> = WaitQueue::new();
let mut model = [0u16; CAP];
let mut len = 0usize;
for _ in 0..5 {
let t: u16 = kani::any::<u16>() % 4;
match kani::any::<u8>() % 3 {
0 => {
let ok = q.push(ThreadId::from_raw(t)).is_some();
let dup = model[..len].contains(&t);
assert!(ok == (!dup && len < CAP));
if ok {
model[len] = t;
len += 1;
}
}
1 => {
let got = q.pop();
if len == 0 {
assert!(got.is_none());
} else {
assert!(got == Some(ThreadId::from_raw(model[0])));
for j in 0..len - 1 {
model[j] = model[j + 1];
}
len -= 1;
}
}
_ => {
let removed = q.remove_thread(ThreadId::from_raw(t));
match model[..len].iter().position(|&m| m == t) {
Some(i) => {
assert!(removed);
for j in i..len - 1 {
model[j] = model[j + 1];
}
len -= 1;
}
None => assert!(!removed),
}
}
}
assert!(q.len == len);
assert!(q.is_empty() == (len == 0));
}
}
}

View File

@@ -33,4 +33,9 @@ pub enum KernelError {
InvalidRegion,
/// The object a capability would refer to no longer exists.
NoSuchObject,
/// The capability names the wrong kind of object for this operation.
WrongObject,
/// `Reply` with an empty reply slot, or a call whose reply was dropped.
NoReply,
InvalidArgument,
}

View File

@@ -1,6 +1,8 @@
use types::id::{IrqId, ThreadId};
use crate::error::KernelError;
use types::{
badge::Badge,
generation::Generation,
id::{IrqId, NotificationId},
};
#[cfg(test)]
#[path = "tests.rs"]
@@ -8,47 +10,47 @@ mod tests;
const MAX_IRQS: usize = 256;
/// Where an interrupt is delivered: a signal on a notification, through a
/// capability snapshot (generation and badge) taken when the route was set.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IrqRoute {
pub notification: NotificationId,
pub generation: Generation,
pub badge: Badge,
}
pub struct IrqRoutingTable {
owners: [Option<ThreadId>; MAX_IRQS],
routes: [Option<IrqRoute>; MAX_IRQS],
}
impl IrqRoutingTable {
pub fn new() -> Self {
Self {
owners: [None; MAX_IRQS],
routes: [None; MAX_IRQS],
}
}
pub fn register(&mut self, irq: IrqId, thread: ThreadId) -> Result<(), KernelError> {
let slot = self
.owners
.get_mut(irq.raw() as usize)
.ok_or(KernelError::InvalidIrq(irq))?;
if slot.is_some() {
return Err(KernelError::IrqAlreadyOwned(irq));
/// Routes `irq`, replacing any previous route. Fails for lines beyond
/// the table.
pub fn set(&mut self, irq: IrqId, route: IrqRoute) -> bool {
match self.routes.get_mut(irq.raw() as usize) {
Some(slot) => {
*slot = Some(route);
true
}
None => false,
}
*slot = Some(thread);
Ok(())
}
pub fn unregister(&mut self, irq: IrqId) {
if let Some(slot) = self.owners.get_mut(irq.raw() as usize) {
pub fn clear(&mut self, irq: IrqId) {
if let Some(slot) = self.routes.get_mut(irq.raw() as usize) {
*slot = None;
}
}
/// Drops every route to `thread`, e.g. when it dies.
pub fn unregister_thread(&mut self, thread: ThreadId) {
for slot in &mut self.owners {
if *slot == Some(thread) {
*slot = None;
}
}
}
/// The thread an interrupt is routed to; `None` for unknown lines.
pub fn owner(&self, irq: IrqId) -> Option<ThreadId> {
*self.owners.get(irq.raw() as usize)?
/// The route for `irq`; `None` for unknown or unrouted lines.
pub fn route(&self, irq: IrqId) -> Option<IrqRoute> {
*self.routes.get(irq.raw() as usize)?
}
}

View File

@@ -1,50 +1,43 @@
use super::*;
use types::id::{IrqId, ThreadId};
use types::badge::Badge;
use types::generation::Generation;
use types::id::{IrqId, NotificationId};
fn irq(id: u16) -> IrqId {
IrqId::from_raw(id)
}
fn thread(id: u16) -> ThreadId {
ThreadId::from_raw(id)
fn route(n: u16) -> IrqRoute {
IrqRoute {
notification: NotificationId::from_raw(n),
generation: Generation::initial(),
badge: Badge::new(1 << n),
}
}
#[test]
fn new_table_has_no_owners() {
let table = IrqRoutingTable::new();
assert!(table.owner(irq(0)).is_none());
assert!(table.owner(irq(10)).is_none());
fn new_table_routes_nothing() {
let t = IrqRoutingTable::new();
assert_eq!(t.route(IrqId::from_raw(10)), None);
}
#[test]
fn register_and_lookup() {
let mut table = IrqRoutingTable::new();
table.register(irq(5), thread(1)).unwrap();
assert_eq!(table.owner(irq(5)), Some(thread(1)));
fn set_then_route() {
let mut t = IrqRoutingTable::new();
assert!(t.set(IrqId::from_raw(10), route(3)));
assert_eq!(t.route(IrqId::from_raw(10)), Some(route(3)));
}
#[test]
fn register_already_owned_fails() {
let mut table = IrqRoutingTable::new();
table.register(irq(5), thread(1)).unwrap();
assert!(table.register(irq(5), thread(2)).is_err());
fn set_replaces_and_clear_removes() {
let mut t = IrqRoutingTable::new();
t.set(IrqId::from_raw(10), route(3));
t.set(IrqId::from_raw(10), route(4));
assert_eq!(t.route(IrqId::from_raw(10)), Some(route(4)));
t.clear(IrqId::from_raw(10));
assert_eq!(t.route(IrqId::from_raw(10)), None);
}
#[test]
fn unregister_then_reregister() {
let mut table = IrqRoutingTable::new();
table.register(irq(5), thread(1)).unwrap();
table.unregister(irq(5));
assert!(table.owner(irq(5)).is_none());
table.register(irq(5), thread(2)).unwrap();
assert_eq!(table.owner(irq(5)), Some(thread(2)));
}
#[test]
fn different_irqs_independent() {
let mut table = IrqRoutingTable::new();
table.register(irq(0), thread(10)).unwrap();
table.register(irq(1), thread(20)).unwrap();
assert_eq!(table.owner(irq(0)), Some(thread(10)));
assert_eq!(table.owner(irq(1)), Some(thread(20)));
fn lines_beyond_the_table_are_rejected_without_panicking() {
let mut t = IrqRoutingTable::new();
assert!(!t.set(IrqId::from_raw(1000), route(1)));
assert_eq!(t.route(IrqId::from_raw(1000)), None);
t.clear(IrqId::from_raw(1000));
}

View File

@@ -11,6 +11,7 @@ pub mod generations;
pub mod irq;
pub mod memory;
pub mod message;
pub mod notification;
pub mod process;
pub mod resource;
pub mod scheduler;

View File

@@ -5,26 +5,26 @@ use types::bounded::CapSlot;
mod tests;
pub const MAX_MESSAGE_WORDS: usize = 4;
pub const MAX_CAP_TRANSFERS: usize = 4;
/// Four words and at most one capability (named by the sender's slot).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Message {
words: [usize; MAX_MESSAGE_WORDS],
caps: [Option<CapSlot>; MAX_CAP_TRANSFERS],
cap: Option<CapSlot>,
}
impl Message {
pub fn empty() -> Self {
Self {
words: [0; MAX_MESSAGE_WORDS],
caps: [None; MAX_CAP_TRANSFERS],
}
Self::from_words([0; MAX_MESSAGE_WORDS])
}
pub fn from_words(words: [usize; MAX_MESSAGE_WORDS]) -> Self {
Self {
words,
caps: [None; MAX_CAP_TRANSFERS],
}
Self { words, cap: None }
}
pub fn with_cap(mut self, slot: CapSlot) -> Self {
self.cap = Some(slot);
self
}
pub fn words(&self) -> &[usize; MAX_MESSAGE_WORDS] {
@@ -35,42 +35,7 @@ impl Message {
self.words[index]
}
pub fn caps(&self) -> &[Option<CapSlot>; MAX_CAP_TRANSFERS] {
&self.caps
}
pub fn cap(&self, index: usize) -> Option<CapSlot> {
self.caps[index]
}
pub fn builder() -> MessageBuilder {
MessageBuilder {
message: Message::empty(),
}
}
}
pub struct MessageBuilder {
message: Message,
}
impl MessageBuilder {
pub fn word(mut self, index: usize, value: usize) -> Self {
self.message.words[index] = value;
self
}
pub fn words(mut self, words: [usize; MAX_MESSAGE_WORDS]) -> Self {
self.message.words = words;
self
}
pub fn cap(mut self, index: usize, slot: CapSlot) -> Self {
self.message.caps[index] = Some(slot);
self
}
pub fn build(self) -> Message {
self.message
pub fn cap(&self) -> Option<CapSlot> {
self.cap
}
}

View File

@@ -1,78 +1,23 @@
use super::*;
use types::bounded::CapSlot;
fn slot(n: u8) -> CapSlot {
CapSlot::new(n).unwrap()
}
// --- Construction ---
#[test]
fn empty_message_has_zeroed_words_and_no_caps() {
fn empty_message_has_zeroed_words_and_no_cap() {
let msg = Message::empty();
assert_eq!(msg.words(), &[0; MAX_MESSAGE_WORDS]);
for i in 0..MAX_CAP_TRANSFERS {
assert!(msg.cap(i).is_none());
}
assert!(msg.cap().is_none());
}
#[test]
fn from_words_sets_words_and_no_caps() {
fn from_words_keeps_every_word() {
let msg = Message::from_words([10, 20, 30, 40]);
assert_eq!(msg.word(0), 10);
assert_eq!(msg.word(1), 20);
assert_eq!(msg.word(2), 30);
assert_eq!(msg.word(3), 40);
assert!(msg.cap(0).is_none());
}
// --- Accessors ---
#[test]
fn words_returns_full_array() {
let msg = Message::from_words([1, 2, 3, 4]);
assert_eq!(msg.words(), &[1, 2, 3, 4]);
assert_eq!(msg.words(), &[10, 20, 30, 40]);
assert!(msg.cap().is_none());
}
#[test]
fn caps_returns_full_array() {
let msg = Message::empty();
assert_eq!(msg.caps(), &[None; MAX_CAP_TRANSFERS]);
}
// --- Builder ---
#[test]
fn builder_sets_individual_words() {
let msg = Message::builder().word(0, 42).word(2, 99).build();
assert_eq!(msg.word(0), 42);
assert_eq!(msg.word(1), 0);
assert_eq!(msg.word(2), 99);
assert_eq!(msg.word(3), 0);
}
#[test]
fn builder_sets_caps() {
let msg = Message::builder().cap(0, slot(5)).cap(1, slot(10)).build();
assert_eq!(msg.cap(0), Some(slot(5)));
assert_eq!(msg.cap(1), Some(slot(10)));
assert!(msg.cap(2).is_none());
}
#[test]
fn builder_sets_words_and_caps_together() {
let msg = Message::builder()
.word(0, 100)
.word(1, 200)
.cap(0, slot(3))
.build();
assert_eq!(msg.word(0), 100);
assert_eq!(msg.word(1), 200);
assert_eq!(msg.cap(0), Some(slot(3)));
}
#[test]
fn builder_with_all_words() {
let msg = Message::builder().words([11, 22, 33, 44]).build();
assert_eq!(msg.words(), &[11, 22, 33, 44]);
fn with_cap_attaches_one_capability() {
let msg = Message::from_words([1, 2, 3, 4]).with_cap(CapSlot::new(5).unwrap());
assert_eq!(msg.cap(), CapSlot::new(5));
assert_eq!(msg.word(3), 4);
}

View 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()
}
}

View 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);
}

View 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);
}

View File

@@ -22,7 +22,7 @@ fn dead_thread_rejects_all_transitions() {
assert!(matches!(thread.state(), ThreadState::Dead));
assert!(thread.set_running(CoreId::from_raw(0)).is_err());
assert!(thread.block(BlockReason::WaitingAny).is_err());
assert!(thread.block(BlockReason::AwaitingReply).is_err());
assert!(thread.unblock().is_err());
assert!(thread.preempt().is_err());
assert!(thread.exit().is_err());
@@ -36,7 +36,7 @@ fn only_running_thread_can_block() {
let mut thread = test_thread(Priority::new(prio_val).unwrap());
assert!(matches!(thread.state(), ThreadState::Ready));
assert!(thread.block(BlockReason::WaitingAny).is_err());
assert!(thread.block(BlockReason::AwaitingReply).is_err());
}
#[kani::proof]
@@ -44,7 +44,7 @@ fn blocked_thread_cannot_run_directly() {
let mut thread = test_thread(Priority::new(16).unwrap());
thread.set_running(CoreId::from_raw(0)).unwrap();
thread.block(BlockReason::WaitingAny).unwrap();
thread.block(BlockReason::AwaitingReply).unwrap();
assert!(thread.set_running(CoreId::from_raw(0)).is_err());
}

View File

@@ -1,9 +1,10 @@
use types::id::{EndpointId, IrqId, ProcessId, RegionId};
use types::id::{EndpointId, IrqId, NotificationId, ProcessId, RegionId};
#[derive(Clone)]
pub enum Resource {
Memory(RegionId),
Endpoint(EndpointId),
Notification(NotificationId),
Process(ProcessId),
Interrupt(IrqId),
ProcessCreate,

View File

@@ -30,6 +30,13 @@ impl MultiCoreScheduler {
}
}
/// Whether `thread` waits in some core's run queue.
pub fn is_queued(&self, thread: ThreadId) -> bool {
self.cores[..self.num_cores as usize]
.iter()
.any(|c| c.is_queued(thread))
}
pub fn current(&self, core: CoreId) -> Option<ThreadId> {
self.cores[core.raw() as usize].current()
}

View File

@@ -29,6 +29,10 @@ impl PerCoreScheduler {
self.queue.add(thread, priority);
}
pub fn is_queued(&self, thread: ThreadId) -> bool {
self.queue.contains(thread)
}
pub fn remove(&mut self, thread: ThreadId) {
self.queue.remove(thread);
}

View File

@@ -1,42 +1,12 @@
use types::{
address::{DataRef, EntryPoint, StackPointer, VirtualAddress},
badge::Badge,
bounded::{CapSlot, PageCount},
permission::PermissionSet,
};
use crate::message::Message;
pub struct WaitSet {
endpoints: [Option<CapSlot>; 4],
interrupts: [Option<CapSlot>; 4],
}
impl WaitSet {
pub fn new(endpoints: [Option<CapSlot>; 4], interrupts: [Option<CapSlot>; 4]) -> Self {
Self {
endpoints,
interrupts,
}
}
pub fn endpoints(&self) -> &[Option<CapSlot>; 4] {
&self.endpoints
}
pub fn interrupts(&self) -> &[Option<CapSlot>; 4] {
&self.interrupts
}
pub fn is_empty(&self) -> bool {
self.endpoints.iter().all(|s| s.is_none()) && self.interrupts.iter().all(|s| s.is_none())
}
}
pub enum WakeReason {
MessageReady(CapSlot),
InterruptFired(CapSlot),
}
pub enum Syscall {
Send {
endpoint: CapSlot,
@@ -50,15 +20,47 @@ pub enum Syscall {
to_endpoint: CapSlot,
permissions: PermissionSet,
},
WaitForInterrupt {
/// Send, then block for the reply (docs/ipc.md).
Call {
endpoint: CapSlot,
message: Message,
},
/// Answer the thread in the caller's reply slot.
Reply {
message: Message,
},
/// Reply (if a reply is pending), then receive.
ReplyRecv {
endpoint: CapSlot,
message: Message,
},
/// Copy an unbadged endpoint/notification cap with a badge and
/// narrower permissions into the caller's own table.
Mint {
slot: CapSlot,
badge: Badge,
permissions: PermissionSet,
},
CreateNotification,
Signal {
notification: CapSlot,
},
Wait {
notification: CapSlot,
},
Poll {
notification: CapSlot,
},
BindNotification {
notification: CapSlot,
},
SetIrqNotification {
irq: CapSlot,
notification: CapSlot,
},
AcknowledgeInterrupt {
irq: CapSlot,
},
WaitAny {
sources: WaitSet,
},
CreateProcess {
create_cap: CapSlot,
image: DataRef,

View File

@@ -1,10 +1,11 @@
use types::{
bounded::{CapSlot, Priority},
id::{CoreId, ProcessId, ThreadId},
bounded::Priority,
generation::Generation,
id::{CoreId, EndpointId, NotificationId, ProcessId, ThreadId},
time::TimeSlice,
};
use crate::error::KernelError;
use crate::{error::KernelError, generations::SlotGenerations};
#[cfg(test)]
#[path = "tests.rs"]
@@ -80,10 +81,16 @@ impl SavedContext {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockReason {
Sending(CapSlot),
Receiving(CapSlot),
WaitingForInterrupt(CapSlot),
WaitingAny,
/// Queued as a sender on the endpoint.
Sending(EndpointId),
/// Queued as a `Call`er on the endpoint; will then await the reply.
Calling(EndpointId),
/// Queued as a receiver on the endpoint.
Receiving(EndpointId),
/// Delivered a `Call`; waiting for the server's reply.
AwaitingReply,
/// Blocked in `Wait` on the notification.
WaitingNotification(NotificationId),
}
impl Thread {
@@ -180,6 +187,17 @@ impl Thread {
}
}
/// Keeps a blocked thread blocked, waiting for something else.
pub fn rewait(&mut self, reason: BlockReason) -> Result<(), KernelError> {
match self.state {
ThreadState::Blocked(_) => {
self.state = ThreadState::Blocked(reason);
Ok(())
}
_ => Err(KernelError::InvalidStateTransition(self.id)),
}
}
pub fn unblock(&mut self) -> Result<(), KernelError> {
match self.state {
ThreadState::Blocked(_) => {
@@ -215,15 +233,28 @@ pub const MAX_THREADS: usize = 256;
pub struct ThreadTable {
threads: [Option<Thread>; MAX_THREADS],
generations: SlotGenerations<MAX_THREADS>,
}
impl ThreadTable {
pub fn new() -> Self {
Self {
threads: [const { None }; MAX_THREADS],
generations: SlotGenerations::new(),
}
}
/// Current generation of a live thread, so references to it (reply
/// slots) can detect that the id was reused.
pub fn generation(&self, id: ThreadId) -> Option<Generation> {
self.get(id).ok()?;
self.generations.current(id.raw() as usize)
}
fn free_slot(&self) -> Option<usize> {
(0..MAX_THREADS).find(|&i| self.threads[i].is_none() && self.generations.is_usable(i))
}
pub fn spawn(
&mut self,
process: ProcessId,
@@ -231,11 +262,7 @@ impl ThreadTable {
time_slice: TimeSlice,
core_affinity: Option<CoreId>,
) -> Result<ThreadId, KernelError> {
let idx = self
.threads
.iter()
.position(|s| s.is_none())
.ok_or(KernelError::TooManyThreads)?;
let idx = self.free_slot().ok_or(KernelError::TooManyThreads)?;
let id = ThreadId::from_raw(idx as u16);
self.threads[idx] = Some(Thread::new(
@@ -256,11 +283,7 @@ impl ThreadTable {
core_affinity: Option<CoreId>,
context: SavedContext,
) -> Result<ThreadId, KernelError> {
let idx = self
.threads
.iter()
.position(|s| s.is_none())
.ok_or(KernelError::TooManyThreads)?;
let idx = self.free_slot().ok_or(KernelError::TooManyThreads)?;
let id = ThreadId::from_raw(idx as u16);
self.threads[idx] = Some(Thread::with_context(
@@ -292,10 +315,13 @@ impl ThreadTable {
pub fn destroy(&mut self, id: ThreadId) -> Result<Thread, KernelError> {
let idx = id.raw() as usize;
self.threads
let thread = self
.threads
.get_mut(idx)
.and_then(|s| s.take())
.ok_or(KernelError::InvalidThread(id))
.ok_or(KernelError::InvalidThread(id))?;
self.generations.advance(idx);
Ok(thread)
}
pub fn threads_for_process(&self, process: ProcessId) -> impl Iterator<Item = &Thread> {

View File

@@ -1,6 +1,6 @@
use super::*;
use types::bounded::Priority;
use types::id::{CoreId, ProcessId, ThreadId};
use types::id::{CoreId, EndpointId, ProcessId, ThreadId};
use types::time::TimeSlice;
fn thread(id: u16) -> ThreadId {
@@ -65,10 +65,10 @@ fn ready_to_running() {
fn running_to_blocked() {
let mut t = test_thread(0, 0);
t.set_running(core(0)).unwrap();
t.block(BlockReason::WaitingAny).unwrap();
t.block(BlockReason::AwaitingReply).unwrap();
assert!(matches!(
t.state(),
ThreadState::Blocked(BlockReason::WaitingAny)
ThreadState::Blocked(BlockReason::AwaitingReply)
));
}
@@ -76,7 +76,7 @@ fn running_to_blocked() {
fn blocked_to_ready() {
let mut t = test_thread(0, 0);
t.set_running(core(0)).unwrap();
t.block(BlockReason::WaitingAny).unwrap();
t.block(BlockReason::AwaitingReply).unwrap();
t.unblock().unwrap();
assert!(matches!(t.state(), ThreadState::Ready));
}
@@ -105,7 +105,7 @@ fn dead_cannot_transition() {
t.set_running(core(0)).unwrap();
t.exit().unwrap();
assert!(t.set_running(core(0)).is_err());
assert!(t.block(BlockReason::WaitingAny).is_err());
assert!(t.block(BlockReason::AwaitingReply).is_err());
assert!(t.unblock().is_err());
assert!(t.preempt().is_err());
assert!(t.exit().is_err());
@@ -114,14 +114,14 @@ fn dead_cannot_transition() {
#[test]
fn ready_cannot_block() {
let mut t = test_thread(0, 0);
assert!(t.block(BlockReason::WaitingAny).is_err());
assert!(t.block(BlockReason::AwaitingReply).is_err());
}
#[test]
fn blocked_cannot_run_directly() {
let mut t = test_thread(0, 0);
t.set_running(core(0)).unwrap();
t.block(BlockReason::WaitingAny).unwrap();
t.block(BlockReason::AwaitingReply).unwrap();
assert!(t.set_running(core(0)).is_err());
}
@@ -141,7 +141,7 @@ fn ready_cannot_exit() {
fn blocked_cannot_preempt() {
let mut t = test_thread(0, 0);
t.set_running(core(0)).unwrap();
t.block(BlockReason::WaitingAny).unwrap();
t.block(BlockReason::AwaitingReply).unwrap();
assert!(t.preempt().is_err());
}
@@ -237,3 +237,32 @@ fn table_destroy_reuses_slot() {
.unwrap();
assert_eq!(id, new_id);
}
#[test]
fn a_blocked_thread_can_change_what_it_waits_for() {
let mut t = Thread::new(
ThreadId::from_raw(1),
ProcessId::from_raw(0),
Priority::DEFAULT,
TimeSlice::DEFAULT,
None,
);
t.set_running(CoreId::from_raw(0)).unwrap();
t.block(BlockReason::Calling(EndpointId::from_raw(0)))
.unwrap();
t.rewait(BlockReason::AwaitingReply).unwrap();
assert_eq!(*t.state(), ThreadState::Blocked(BlockReason::AwaitingReply));
}
#[test]
fn only_blocked_threads_can_rewait() {
let mut t = Thread::new(
ThreadId::from_raw(1),
ProcessId::from_raw(0),
Priority::DEFAULT,
TimeSlice::DEFAULT,
None,
);
assert!(t.rewait(BlockReason::AwaitingReply).is_err());
assert_eq!(*t.state(), ThreadState::Ready);
}