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:
@@ -5,7 +5,7 @@ use platform::Platform;
|
||||
use types::{
|
||||
address::PhysicalAddress,
|
||||
bounded::{CapSlot, Priority},
|
||||
id::{EndpointId, IrqId, ProcessId, ThreadId},
|
||||
id::{EndpointId, ProcessId, ThreadId},
|
||||
permission::PermissionSet,
|
||||
time::TimeSlice,
|
||||
};
|
||||
@@ -41,10 +41,6 @@ impl Kernel<MockPlatform> {
|
||||
tid
|
||||
}
|
||||
|
||||
pub fn register_irq_for_test(&mut self, irq: IrqId, thread: ThreadId) {
|
||||
let _ = self.irqs.register(irq, thread);
|
||||
}
|
||||
|
||||
pub fn block_thread_for_test(&mut self, tid: ThreadId, reason: domain::thread::BlockReason) {
|
||||
let core = self.platform.core_id();
|
||||
if let Ok(t) = self.threads.get_mut(tid) {
|
||||
@@ -128,6 +124,10 @@ impl Kernel<MockPlatform> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy_process_notifications_for_test(&mut self, owner: ProcessId) {
|
||||
self.destroy_notifications_owned_by(owner);
|
||||
}
|
||||
|
||||
pub fn thread_state_for_test(&self, tid: ThreadId) -> Option<ThreadState> {
|
||||
self.threads.get(tid).ok().map(|t| *t.state())
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
350
crates/kernel/src/tests/ipc_invariants.rs
Normal file
350
crates/kernel/src/tests/ipc_invariants.rs
Normal file
@@ -0,0 +1,350 @@
|
||||
//! Randomized check of the IPC state machine. Six threads in three
|
||||
//! processes send, call, receive, reply and signal at random over two
|
||||
//! endpoints and a notification; one process is periodically killed and
|
||||
//! re-created. After every step the kernel's queues, block reasons, reply
|
||||
//! slots and run queue must agree, and no message may be duplicated.
|
||||
|
||||
use super::*;
|
||||
use crate::syscall::{Delivery, DeliveryKind};
|
||||
use crate::{SyscallOutcome, SyscallResult};
|
||||
use domain::message::Message;
|
||||
use domain::resource::Resource;
|
||||
use domain::syscall::Syscall;
|
||||
use domain::thread::{BlockReason, ThreadState};
|
||||
use types::bounded::CapSlot;
|
||||
use types::id::{EndpointId, NotificationId, ThreadId};
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
|
||||
const STEPS: usize = 4000;
|
||||
const THREADS_PER_PROC: usize = 2;
|
||||
const PROCS: usize = 3;
|
||||
|
||||
struct Rng(u64);
|
||||
|
||||
impl Rng {
|
||||
fn next(&mut self) -> u64 {
|
||||
self.0 ^= self.0 << 13;
|
||||
self.0 ^= self.0 >> 7;
|
||||
self.0 ^= self.0 << 17;
|
||||
self.0
|
||||
}
|
||||
|
||||
fn below(&mut self, n: usize) -> usize {
|
||||
(self.next() % n as u64) as usize
|
||||
}
|
||||
}
|
||||
|
||||
fn all_perms() -> PermissionSet {
|
||||
PermissionSet::empty()
|
||||
.with(Permission::Read)
|
||||
.with(Permission::Write)
|
||||
.with(Permission::Execute)
|
||||
.with(Permission::Delegate)
|
||||
}
|
||||
|
||||
struct Proc {
|
||||
pid: ProcessId,
|
||||
threads: [ThreadId; THREADS_PER_PROC],
|
||||
eps: [CapSlot; 2],
|
||||
ntfn: CapSlot,
|
||||
}
|
||||
|
||||
struct World {
|
||||
k: Kernel<MockPlatform>,
|
||||
procs: [Proc; PROCS],
|
||||
eps: [EndpointId; 2],
|
||||
ntfn: NotificationId,
|
||||
next_id: usize,
|
||||
delivered: [bool; 2 * STEPS + 16],
|
||||
sent: [bool; 2 * STEPS + 16],
|
||||
calls_answered: usize,
|
||||
signals_seen: usize,
|
||||
ran: usize,
|
||||
rebirths: usize,
|
||||
}
|
||||
|
||||
impl World {
|
||||
fn new() -> Self {
|
||||
let (mut k, p0) = kernel_with_process();
|
||||
let t0 = k.current_thread().unwrap();
|
||||
let p1 = k.create_process_internal().unwrap();
|
||||
let p2 = k.create_process_internal().unwrap();
|
||||
// Endpoints and the notification belong to init, which never dies.
|
||||
let eps = [
|
||||
k.create_endpoint(p0).unwrap(),
|
||||
k.create_endpoint(p0).unwrap(),
|
||||
];
|
||||
let ntfn = k.create_notification(p0).unwrap();
|
||||
let mut procs = [p0, p1, p2].map(|pid| Proc {
|
||||
pid,
|
||||
threads: [t0; THREADS_PER_PROC],
|
||||
eps: [CapSlot::new(0).unwrap(); 2],
|
||||
ntfn: CapSlot::new(0).unwrap(),
|
||||
});
|
||||
for (i, p) in procs.iter_mut().enumerate() {
|
||||
Self::equip(&mut k, p, eps, ntfn, i == 0, t0);
|
||||
}
|
||||
Self {
|
||||
k,
|
||||
procs,
|
||||
eps,
|
||||
ntfn,
|
||||
next_id: 1,
|
||||
delivered: [false; 2 * STEPS + 16],
|
||||
sent: [false; 2 * STEPS + 16],
|
||||
calls_answered: 0,
|
||||
signals_seen: 0,
|
||||
ran: 0,
|
||||
rebirths: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn equip(
|
||||
k: &mut Kernel<MockPlatform>,
|
||||
p: &mut Proc,
|
||||
eps: [EndpointId; 2],
|
||||
ntfn: NotificationId,
|
||||
is_init: bool,
|
||||
init_thread: ThreadId,
|
||||
) {
|
||||
for j in 0..THREADS_PER_PROC {
|
||||
p.threads[j] = if is_init && j == 0 {
|
||||
init_thread
|
||||
} else {
|
||||
k.spawn_thread_for_test(p.pid, Priority::DEFAULT, TimeSlice::DEFAULT)
|
||||
};
|
||||
}
|
||||
for (e, ep) in eps.iter().enumerate() {
|
||||
p.eps[e] = k.grant_cap_for_test(p.pid, Resource::Endpoint(*ep), all_perms());
|
||||
}
|
||||
p.ntfn = k.grant_cap_for_test(p.pid, Resource::Notification(ntfn), all_perms());
|
||||
// The second thread of every process gets the notification bound.
|
||||
k.switch_to_thread_for_test(p.threads[1]);
|
||||
let _ = k.handle_syscall(Syscall::BindNotification {
|
||||
notification: p.ntfn,
|
||||
});
|
||||
}
|
||||
|
||||
fn fresh_id(&mut self) -> usize {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
self.sent[id] = true;
|
||||
id
|
||||
}
|
||||
|
||||
fn record(&mut self, d: &Delivery) {
|
||||
match d.kind {
|
||||
DeliveryKind::Signal => self.signals_seen += 1,
|
||||
_ => {
|
||||
let id = d.words[0];
|
||||
assert!(
|
||||
id < self.sent.len() && self.sent[id],
|
||||
"delivered id {id} was never sent"
|
||||
);
|
||||
assert!(!self.delivered[id], "message {id} delivered twice");
|
||||
self.delivered[id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn record_outcome(&mut self, outcome: &SyscallOutcome) {
|
||||
if let SyscallOutcome::Complete(SyscallResult::Received(d)) = outcome {
|
||||
let d = *d;
|
||||
self.record(&d);
|
||||
}
|
||||
}
|
||||
|
||||
fn runnable(&self) -> impl Iterator<Item = (usize, ThreadId)> + '_ {
|
||||
self.procs.iter().enumerate().flat_map(move |(pi, p)| {
|
||||
p.threads
|
||||
.into_iter()
|
||||
.filter(|&t| {
|
||||
matches!(
|
||||
self.k.thread_state_for_test(t),
|
||||
Some(ThreadState::Ready | ThreadState::Running(_))
|
||||
)
|
||||
})
|
||||
.map(move |t| (pi, t))
|
||||
})
|
||||
}
|
||||
|
||||
/// Kills process `pi` and brings it back with fresh threads and caps.
|
||||
fn reincarnate(&mut self, pi: usize) {
|
||||
self.k.destroy_process(self.procs[pi].pid);
|
||||
self.procs[pi].pid = self.k.create_process_internal().unwrap();
|
||||
let t0 = self.procs[0].threads[0];
|
||||
Self::equip(
|
||||
&mut self.k,
|
||||
&mut self.procs[pi],
|
||||
self.eps,
|
||||
self.ntfn,
|
||||
false,
|
||||
t0,
|
||||
);
|
||||
self.rebirths += 1;
|
||||
}
|
||||
|
||||
fn step(&mut self, rng: &mut Rng) {
|
||||
let count = self.runnable().count();
|
||||
if count == 0 || rng.below(100) == 0 {
|
||||
// Random clients deadlock; killing someone is what breaks it.
|
||||
self.reincarnate(1 + rng.below(PROCS - 1));
|
||||
return;
|
||||
}
|
||||
let pick = rng.below(count);
|
||||
let (pi, tid) = self.runnable().nth(pick).unwrap();
|
||||
self.ran += 1;
|
||||
self.k.switch_to_thread_for_test(tid);
|
||||
// What the adapter would hand the thread when it resumes.
|
||||
if let Some(SyscallResult::Received(d)) = self.k.take_pending_result(tid) {
|
||||
self.record(&d);
|
||||
}
|
||||
let ep = self.procs[pi].eps[rng.below(2)];
|
||||
let ntfn = self.procs[pi].ntfn;
|
||||
|
||||
let call = match rng.below(9) {
|
||||
0 => Syscall::Send {
|
||||
endpoint: ep,
|
||||
message: Message::from_words([self.fresh_id(), 0, 0, 0]),
|
||||
},
|
||||
1 | 2 => Syscall::Call {
|
||||
endpoint: ep,
|
||||
message: Message::from_words([self.fresh_id(), 1, 0, 0]),
|
||||
},
|
||||
3 | 4 => Syscall::Receive { endpoint: ep },
|
||||
5 => Syscall::Reply {
|
||||
message: Message::from_words([self.fresh_id(), 2, 0, 0]),
|
||||
},
|
||||
6 => Syscall::ReplyRecv {
|
||||
endpoint: ep,
|
||||
message: Message::from_words([self.fresh_id(), 2, 0, 0]),
|
||||
},
|
||||
7 => Syscall::Signal { notification: ntfn },
|
||||
_ => match rng.below(2) {
|
||||
0 => Syscall::Wait { notification: ntfn },
|
||||
_ => Syscall::Poll { notification: ntfn },
|
||||
},
|
||||
};
|
||||
let is_reply = matches!(call, Syscall::Reply { .. } | Syscall::ReplyRecv { .. });
|
||||
let had_reply = self.k.ipc[tid.raw() as usize].reply_to.is_some();
|
||||
let outcome = self.k.handle_syscall(call);
|
||||
if is_reply && had_reply {
|
||||
self.calls_answered += 1;
|
||||
}
|
||||
self.record_outcome(&outcome);
|
||||
}
|
||||
|
||||
fn check(&self) {
|
||||
let k = &self.k;
|
||||
for p in &self.procs {
|
||||
for &t in &p.threads {
|
||||
let Some(state) = k.thread_state_for_test(t) else {
|
||||
continue;
|
||||
};
|
||||
let ipc = &k.ipc[t.raw() as usize];
|
||||
let queued_on = |e: EndpointId| {
|
||||
let ep = k.endpoints.get(e).unwrap();
|
||||
(ep.is_sender(t), ep.is_receiver(t))
|
||||
};
|
||||
let in_any_queue = self.eps.iter().any(|&e| {
|
||||
let (s, r) = queued_on(e);
|
||||
s || r
|
||||
}) || k.notifications.get(self.ntfn).unwrap().is_waiter(t);
|
||||
|
||||
match state {
|
||||
ThreadState::Blocked(BlockReason::Sending(e) | BlockReason::Calling(e)) => {
|
||||
assert_eq!(
|
||||
queued_on(e),
|
||||
(true, false),
|
||||
"{t:?} blocked sending but not queued"
|
||||
);
|
||||
assert!(
|
||||
ipc.outgoing.is_some(),
|
||||
"{t:?} queued sender without a message"
|
||||
);
|
||||
}
|
||||
ThreadState::Blocked(BlockReason::Receiving(e)) => {
|
||||
assert_eq!(
|
||||
queued_on(e),
|
||||
(false, true),
|
||||
"{t:?} blocked receiving but not queued"
|
||||
);
|
||||
}
|
||||
ThreadState::Blocked(BlockReason::WaitingNotification(n)) => {
|
||||
assert!(k.notifications.get(n).unwrap().is_waiter(t));
|
||||
}
|
||||
ThreadState::Blocked(BlockReason::AwaitingReply) => {
|
||||
assert!(!in_any_queue, "{t:?} awaiting reply but still queued");
|
||||
let me = crate::syscall::ThreadRef {
|
||||
tid: t,
|
||||
generation: k.threads.generation(t).unwrap(),
|
||||
};
|
||||
let holders = self
|
||||
.procs
|
||||
.iter()
|
||||
.flat_map(|p| p.threads)
|
||||
.filter(|&s| k.thread_state_for_test(s).is_some())
|
||||
.filter(|s| k.ipc[s.raw() as usize].reply_to == Some(me))
|
||||
.count();
|
||||
assert_eq!(holders, 1, "{t:?} awaits a reply nobody can send");
|
||||
}
|
||||
ThreadState::Ready | ThreadState::Running(_) | ThreadState::Dead => {
|
||||
assert!(!in_any_queue, "{t:?} runnable but queued on an IPC object");
|
||||
assert!(ipc.outgoing.is_none());
|
||||
}
|
||||
}
|
||||
let blocked = matches!(state, ThreadState::Blocked(_));
|
||||
assert_eq!(
|
||||
k.scheduler.is_queued(t),
|
||||
matches!(state, ThreadState::Ready),
|
||||
"{t:?} in state {state:?} has wrong run-queue membership"
|
||||
);
|
||||
if blocked {
|
||||
assert_ne!(k.current_thread(), Some(t));
|
||||
}
|
||||
|
||||
// A live reply slot always points at a thread waiting for it.
|
||||
if let Some(r) = ipc.reply_to
|
||||
&& k.threads.generation(r.tid) == Some(r.generation)
|
||||
{
|
||||
assert_eq!(
|
||||
k.thread_state_for_test(r.tid),
|
||||
Some(ThreadState::Blocked(BlockReason::AwaitingReply)),
|
||||
"{t:?} holds a reply for {:?} which is not waiting",
|
||||
r.tid
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for &e in &self.eps {
|
||||
let ep = k.endpoints.get(e).unwrap();
|
||||
assert!(!(ep.has_waiting_sender() && ep.has_waiting_receiver()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipc_state_machine_holds_under_random_operations() {
|
||||
for seed in [3u64, 0xC0FFEE, 0xFEED_FACE, 99, 123_456_789] {
|
||||
let mut world = World::new();
|
||||
let mut rng = Rng(seed);
|
||||
for _ in 0..STEPS {
|
||||
world.step(&mut rng);
|
||||
world.check();
|
||||
}
|
||||
assert!(
|
||||
world.calls_answered > 20,
|
||||
"seed {seed:#x}: too few replies: answered={} signals={} delivered={} ran={}",
|
||||
world.calls_answered,
|
||||
world.signals_seen,
|
||||
world.delivered.iter().filter(|d| **d).count(),
|
||||
world.ran
|
||||
);
|
||||
assert!(
|
||||
world.rebirths > 5,
|
||||
"seed {seed:#x}: kill/re-create barely exercised"
|
||||
);
|
||||
assert!(world.signals_seen > 20, "seed {seed:#x}: too few signals");
|
||||
assert!(world.delivered.iter().filter(|d| **d).count() > 200);
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,10 @@ use crate::{SyscallOutcome, SyscallResult};
|
||||
use domain::message::Message;
|
||||
use domain::resource::Resource;
|
||||
use domain::syscall::Syscall;
|
||||
use domain::thread::{BlockReason, ThreadState};
|
||||
use domain::thread::ThreadState;
|
||||
use types::address::DataRef;
|
||||
use types::bounded::CapSlot;
|
||||
use types::id::{IrqId, ThreadId};
|
||||
use types::id::ThreadId;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
|
||||
fn create_process(k: &mut Kernel<MockPlatform>) -> CapSlot {
|
||||
@@ -177,57 +177,6 @@ fn endpoint_slots_are_reclaimed_when_processes_die() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupt_routing_does_not_survive_the_owning_thread() {
|
||||
let (mut k, pid_a) = kernel_with_process();
|
||||
let tid_a = k.current_thread().unwrap();
|
||||
let cap_b = create_process(&mut k);
|
||||
let pid_b = ProcessId::from_raw(1);
|
||||
let tid_b = k.spawn_thread_for_test(pid_b, Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||
let irq = IrqId::from_raw(10);
|
||||
k.register_irq_for_test(irq, tid_b);
|
||||
|
||||
k.switch_to_thread_for_test(tid_a);
|
||||
k.handle_syscall(Syscall::Kill { process: cap_b });
|
||||
|
||||
// A new thread reuses B's thread id and happens to be blocked.
|
||||
let reused = k.spawn_thread_for_test(pid_a, Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||
assert_eq!(reused, tid_b);
|
||||
k.block_thread_for_test(reused, BlockReason::Receiving(CapSlot::new(1).unwrap()));
|
||||
|
||||
assert_eq!(
|
||||
k.handle_interrupt(irq),
|
||||
None,
|
||||
"IRQ delivered to an unrelated thread"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupt_numbers_beyond_the_table_are_ignored() {
|
||||
let (mut k, _) = kernel_with_process();
|
||||
// The PLIC can report sources up to 1023.
|
||||
assert_eq!(k.handle_interrupt(IrqId::from_raw(1000)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exiting_a_thread_releases_its_interrupt_route() {
|
||||
let (mut k, pid_a) = kernel_with_process();
|
||||
let tid_a = k.current_thread().unwrap();
|
||||
let irq = IrqId::from_raw(10);
|
||||
k.register_irq_for_test(irq, tid_a);
|
||||
k.spawn_thread_for_test(pid_a, Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||
|
||||
assert!(matches!(
|
||||
k.handle_syscall(Syscall::ExitThread),
|
||||
SyscallOutcome::TerminateCaller
|
||||
));
|
||||
|
||||
let reused = k.spawn_thread_for_test(pid_a, Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||
assert_eq!(reused, tid_a);
|
||||
k.block_thread_for_test(reused, BlockReason::Receiving(CapSlot::new(1).unwrap()));
|
||||
assert_eq!(k.handle_interrupt(irq), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_cap_frees_the_slot() {
|
||||
let (mut k, pid_a) = kernel_with_process();
|
||||
@@ -253,3 +202,43 @@ fn killing_yourself_terminates_the_caller() {
|
||||
assert!(matches!(outcome, SyscallOutcome::TerminateCaller));
|
||||
assert!(!k.process_exists(pid_a));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exiting_a_thread_unbinds_its_notification() {
|
||||
let (mut k, pid_a) = kernel_with_process();
|
||||
let tid_a = k.current_thread().unwrap();
|
||||
let n = k.create_notification(pid_a).unwrap();
|
||||
let rw = PermissionSet::empty()
|
||||
.with(Permission::Read)
|
||||
.with(Permission::Write);
|
||||
let ncap = k.grant_cap_for_test(pid_a, Resource::Notification(n), rw);
|
||||
assert!(!is_error(&k.handle_syscall(Syscall::BindNotification {
|
||||
notification: ncap
|
||||
})));
|
||||
let other = k.spawn_thread_for_test(pid_a, Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||
assert!(matches!(
|
||||
k.handle_syscall(Syscall::ExitThread),
|
||||
SyscallOutcome::TerminateCaller
|
||||
));
|
||||
|
||||
// A new thread reuses the id and blocks in an unrelated receive.
|
||||
let reused = k.spawn_thread_for_test(pid_a, Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||
assert_eq!(reused, tid_a);
|
||||
let ep = k.create_endpoint_for_test();
|
||||
let recv = k.grant_cap_for_test(pid_a, Resource::Endpoint(ep), ipc_perms());
|
||||
k.switch_to_thread_for_test(reused);
|
||||
assert!(matches!(
|
||||
k.handle_syscall(Syscall::Receive { endpoint: recv }),
|
||||
SyscallOutcome::BlockCaller(_)
|
||||
));
|
||||
|
||||
k.switch_to_thread_for_test(other);
|
||||
k.handle_syscall(Syscall::Signal { notification: ncap });
|
||||
assert!(
|
||||
matches!(
|
||||
k.thread_state_for_test(reused),
|
||||
Some(ThreadState::Blocked(_))
|
||||
),
|
||||
"signal reached a thread that never bound the notification"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ mod capability;
|
||||
mod exit;
|
||||
mod invariants;
|
||||
mod ipc;
|
||||
mod ipc_invariants;
|
||||
mod isolation;
|
||||
mod lifecycle;
|
||||
mod lifetimes;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use domain::scheduler::SchedulerDecision;
|
||||
use types::id::{CoreId, IrqId, ThreadId};
|
||||
use types::id::CoreId;
|
||||
use types::time::TimeSlice;
|
||||
|
||||
#[test]
|
||||
@@ -30,26 +30,3 @@ fn tick_preempts_when_time_expired_and_other_thread_ready() {
|
||||
let d2 = k.handle_tick(CoreId::from_raw(0));
|
||||
assert!(matches!(d2, SchedulerDecision::Preempt(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupt_unknown_irq_returns_none() {
|
||||
let (mut k, _) = kernel_with_process();
|
||||
assert!(k.handle_interrupt(IrqId::from_raw(99)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupt_unblocks_registered_thread() {
|
||||
let (mut k, _) = kernel_with_process();
|
||||
let tid = ThreadId::from_raw(0);
|
||||
let irq = IrqId::from_raw(5);
|
||||
|
||||
// Register and block the thread on this IRQ
|
||||
k.register_irq_for_test(irq, tid);
|
||||
k.block_thread_for_test(
|
||||
tid,
|
||||
domain::thread::BlockReason::WaitingForInterrupt(types::bounded::CapSlot::new(0).unwrap()),
|
||||
);
|
||||
|
||||
let result = k.handle_interrupt(irq);
|
||||
assert_eq!(result, Some(tid));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user