Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion kernel/src/syscall/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use super::io::{
};
use super::ipc::{
Incoming, Outgoing,
sys_accept, sys_inbox_setup, sys_message_recv, sys_message_send,
sys_accept, sys_inbox_setup, sys_message_call, sys_message_recv, sys_message_send,
sys_inbox_submit, sys_namespace_build, sys_namespace_open, sys_pipe,
sys_port_badge, sys_port_create, sys_port_mint, sys_shm_create, sys_shm_map, sys_shm_sub,
};
Expand Down Expand Up @@ -466,6 +466,19 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) ->
let Some(mut into) = into else { return bad_addr };
sys_message_recv(RawHandle(a1 as u32), &mut into, args.flags & MESSAGE_NONBLOCK != 0)
}
SYS_MESSAGE_CALL => {
let Ok(args) = ctx.copy_in::<MessageCall>(UserAddr::new(a2)) else { return bad_addr };
// A call's request moves no handles: a refusal from either half
// would leave its caller not knowing whether they moved.
if args.send.flags != 0 || args.recv.flags != 0 || args.send.count != 0 {
return SyscallError::InvalidArgument.to_u64();
}
let (Ok(out), Ok(into)) = (outgoing(&ctx, &args.send), incoming(&ctx, &args.recv)) else {
return SyscallError::InvalidArgument.to_u64();
};
let (Some(out), Some(mut into)) = (out, into) else { return bad_addr };
sys_message_call(RawHandle(a1 as u32), &out, &mut into)
}
SYS_SHM_CREATE => sys_shm_create(a1, a2, RawHandle(a3 as u32)),
SYS_SHM_MAP => sys_shm_map(RawHandle(a1 as u32), a2),
SYS_SHM_SUB => sys_shm_sub(RawHandle(a1 as u32), a2, a3),
Expand Down
9 changes: 9 additions & 0 deletions kernel/src/syscall/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,15 @@ pub(super) fn sys_message_recv(conn_h: RawHandle, into: &mut Incoming<'_>, nonbl
recv(conn_h, into, nonblock)
}

/// Send, then take the next message on the same connection
/// ([`toyos_abi::syscall::message_call`]).
pub(super) fn sys_message_call(conn_h: RawHandle, out: &Outgoing<'_>, into: &mut Incoming<'_>) -> u64 {
match send(conn_h, out, false) {
Ok(()) => recv(conn_h, into, false),
Err(word) => word,
}
}

/// [`sys_message_send`]'s work, or the word its refusal answers.
fn send(conn_h: RawHandle, out: &Outgoing<'_>, nonblock: bool) -> Result<(), u64> {
let mut wanted = [RawHandle(0); MAX_TRANSFER_HANDLES];
Expand Down
39 changes: 37 additions & 2 deletions sdk/abi/src/syscall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,20 +347,23 @@ pub const SYS_EVENT_TAKE: u64 = 150;
pub const SYS_HANDLE_WAIT: u64 = 151;
/// Take a connection's next whole message. See [`message_recv`].
pub const SYS_MESSAGE_RECV: u64 = 152;
/// Send a whole message, then take the next one on the same connection. See
/// [`message_call`].
pub const SYS_MESSAGE_CALL: u64 = 153;

/// Bins in the per-process syscall profile — one for every number this ABI
/// issues, and one at the end for every number it does not.
///
/// **The profile's parts sum to its total, and that is the whole requirement.**
/// A bin array narrower than the ABI reaches drops calls out of the line while
/// the total goes on counting them.
pub const SYSCALL_PROFILE_BINS: usize = 154;
pub const SYSCALL_PROFILE_BINS: usize = 155;

/// Where a number this ABI does not issue is counted. Merging is a degradation
/// a reader can see in the line; dropping is one nobody can.
pub const SYSCALL_PROFILE_OTHER: usize = SYSCALL_PROFILE_BINS - 1;

const _: () = assert!(SYS_MESSAGE_RECV < SYSCALL_PROFILE_OTHER as u64);
const _: () = assert!(SYS_MESSAGE_CALL < SYSCALL_PROFILE_OTHER as u64);

pub const WNOHANG: u64 = 1;
/// Every bit `SYS_PROCESS_WAIT`'s flag word defines; a wait carrying another
Expand Down Expand Up @@ -2214,6 +2217,38 @@ pub fn message_recv(
check(syscall(SYS_MESSAGE_RECV, conn.0 as u64, &args as *const MessageRecv as u64, 0, 0)).map(Received::decode)
}

crate::user_safe! {
/// Arguments for `SYS_MESSAGE_CALL`, passed as a single pointer. Neither
/// half takes a flag: a call waits for room and for its answer. The
/// request carries no handles: `send.count` other than zero is
/// `InvalidArgument`.
#[derive(Clone, Copy)]
pub struct MessageCall {
pub send: MessageSend,
pub recv: MessageRecv,
}
}

/// [`message_send`] and then [`message_recv`] on the same `conn`, in one
/// call: the request, and the next message the peer sends, which a protocol
/// whose peer answers each request in turn makes its answer.
///
/// A refused send
/// answers as [`message_send`] does with nothing sent; once the request is
/// queued, the receive answers as [`message_recv`] does, the request sent
/// whatever it answers. **The request carries no handles**: an error could
/// come from either half, so a caller could not know whether they had moved.
pub fn message_call(
conn: RawHandle,
bytes: &[u8],
head: &mut [u8],
answer: &mut [u8],
answer_handles: &mut [RawHandle],
) -> Result<Received, SyscallError> {
let args = MessageCall { send: MessageSend::new(bytes, &[], false), recv: MessageRecv::new(head, answer, answer_handles, false) };
check(syscall(SYS_MESSAGE_CALL, conn.0 as u64, &args as *const MessageCall as u64, 0, 0)).map(Received::decode)
}

/// Make a shared-memory region of zeroed memory and answer a handle to it.
///
/// Fallible: a size the kernel cannot express in whole pages is
Expand Down
9 changes: 4 additions & 5 deletions sdk/toyos/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -827,14 +827,13 @@ impl Dir {
self.tag = self.tag.wrapping_add(1);
request.tag = self.tag;
let conn = self.conn.as_ref().expect("connected above");
let mut rx = FrameRx::<{ core::mem::size_of::<Reply>() }>::new();
let answered = conn.send(op, &request).map_err(transport).and_then(|()| {
let frame = rx.recv(conn).map_err(transport)?;
let reply: Reply = crate::ipc::decode_payload(rx.payload(frame.payload_len)).map_err(transport)?;
let mut answer = [0u8; core::mem::size_of::<Reply>()];
let answered = conn.call(op, crate::ipc::as_bytes(&request), &mut answer).map_err(transport).and_then(|(frame, batch)| {
let reply: Reply = crate::ipc::decode_payload(&answer[..frame.payload_len]).map_err(transport)?;
if reply.tag != request.tag {
return Err(Error::Malformed);
}
Ok((frame.msg_type, reply, rx.take_batch()))
Ok((frame.msg_type, reply, batch))
});
if answered.is_err() {
self.conn = None;
Expand Down
14 changes: 14 additions & 0 deletions sdk/toyos/src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,20 @@ impl Connection {
let mut msg_type = [0; TYPE_LEN];
receive(self.as_handle(), &mut msg_type, payload, false).map_err(Refused::blocking)
}

/// Send a request and take the frame the peer sends next straight into
/// `payload`, in one kernel call ([`syscall::message_call`]): for a
/// protocol whose peer answers each request in turn, its answer. A
/// refused request sends nothing; once it is sent, the answer fails as
/// [`Self::recv_into`] does.
pub fn call(&self, msg_type: u32, request: &[u8], payload: &mut [u8]) -> Result<(Frame, Batch), IpcError> {
let mut frame = core::mem::MaybeUninit::<Wire<[u8; MAX_FRAME_LEN as usize]>>::uninit();
let sent = framed(&mut frame, msg_type, request).ok_or(IpcError::TooLarge)?;
let mut answer_type = [0; TYPE_LEN];
let mut handles = [toyos_abi::HANDLE_INVALID; syscall::MAX_TRANSFER_HANDLES];
let got = syscall::message_call(self.as_handle().raw(), sent, &mut answer_type, payload, &mut handles);
taken(got, answer_type, handles).map_err(Refused::blocking)
}
}

// Free functions, for a connection a caller holds by something other than a
Expand Down
15 changes: 8 additions & 7 deletions tests/guest/osbench/src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
//! request/reply every service speaks; on Linux a Unix stream socket. **The
//! mechanisms differ by design**, and that difference is what is measured.
//! A ToyOS message past one frame (8 KiB) goes as consecutive frames, which
//! `frames_each_way` counts; a Linux one is one write.
//! `frames_each_way` counts; a Linux one is one write. A ToyOS request's last
//! frame and its answer's first are one call (`SYS_MESSAGE_CALL`); a Linux
//! request is a write and its answer a read.
//!
//! The child is this program as `osbench ipc-serve`. Each phase starts with a
//! control message, `[bytes, rounds, mode]` as three little-endian `u32`s;
Expand Down Expand Up @@ -72,20 +74,19 @@ pub fn suite() {
// 200 at 8 B and 4 KiB, 2,000 after 100 at 64 KiB.
for (bytes, warmup, samples) in [(8usize, 200u32, 5000u32), (4096, 200, 5000), (65536, 100, 2000)] {
let bench = format!("ipc.reqrep.{bytes}");
let mut buf = vec![0x42u8; bytes];
let mut run = || -> std::io::Result<Samples<'_>> {
let buf = vec![0x42u8; bytes];
let run = || -> std::io::Result<Samples<'_>> {
control(&peer, bytes as u32, warmup + samples, ECHO)?;
let mut reply = vec![0u8; bytes];
for _ in 0..warmup {
peer.send(&buf)?;
peer.recv(&mut buf)?;
peer.call(&buf, &mut reply)?;
}
crate::clock::settle();
let before = sys::counts();
let mut ns = Vec::with_capacity(samples as usize);
for _ in 0..samples {
let t = Instant::now();
peer.send(&buf)?;
peer.recv(&mut buf)?;
peer.call(&buf, &mut reply)?;
ns.push(ns_since(t));
}
Ok((ns, Counts::per_op(&before, u64::from(samples))))
Expand Down
6 changes: 6 additions & 0 deletions tests/guest/osbench/src/sys/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,12 @@ pub mod ipc {
(&self.0).read_exact(buf)
}

/// `request`, then its answer: a stream has no call of its own.
pub fn call(&self, request: &[u8], reply: &mut [u8]) -> io::Result<()> {
self.send(request)?;
self.recv(reply)
}

/// `data` and `token` in one `sendmsg`, then this process's copy
/// closed, so the descriptor ends up the peer's alone as a ToyOS
/// handle moved would.
Expand Down
15 changes: 15 additions & 0 deletions tests/guest/osbench/src/sys/toyos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,21 @@ pub mod ipc {
}
}

/// `request` and its answer of `reply.len()` bytes, the answer's
/// first frame taken in the same call as the request's last, straight
/// into `reply`.
pub fn call(&self, request: &[u8], reply: &mut [u8]) -> io::Result<()> {
let last = request.len().saturating_sub(1) / FRAME * FRAME;
for chunk in request[..last].chunks(FRAME) {
self.0.send_bytes(1, chunk).map_err(|e| failed("send", e))?;
}
let (frame, _) = self.0.call(1, &request[last..], reply).map_err(|e| failed("call", e))?;
if frame.payload_len < reply.len() {
self.recv(&mut reply[frame.payload_len..])?;
}
Ok(())
}

/// One frame into the front of `buf`, straight from the kernel: the
/// bytes it held.
fn take(&self, buf: &mut [u8]) -> io::Result<usize> {
Expand Down
28 changes: 26 additions & 2 deletions tests/guest/rust-tests/src/bin/messages_are_whole.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! A connection carries whole messages: each is taken whole, in the order it
//! was sent, with exactly the handles it carried, or refused with nothing of
//! it moved. What a host test of the queue cannot show is the kernel's own
//! boundary: the syscalls' refusals, the handles moving between tables and
//! the bounds as a sender meets them.
//! boundary: the syscalls' refusals, the handles moving between tables, the
//! bounds as a sender meets them, and a call's send and receive in one.
//!
//! - Three messages arrive in order, each with its own bytes and handles.
//! - A buffer a byte or a handle short is refused, and the message stays
Expand All @@ -13,6 +13,7 @@
//! - Plain reads and writes of a connection are refused.
//! - A receive after the peer has gone takes what it sent, then answers
//! `Gone`; a send to a peer gone answers `Gone`.
//! - A call sends and takes the answer the peer sends back, its handle with it.

use toyos::ipc::Connection;
use toyos::poller::OTHER_END_GONE;
Expand Down Expand Up @@ -43,6 +44,8 @@ fn main() {
bytes_alone_are_refused(pair());
println!("messages_are_whole: plain reads and writes were refused");
gone_after_what_was_sent(pair());
println!("messages_are_whole: a peer's leaving came after its messages");
a_call_takes_the_answer(pair());
println!("messages_are_whole: ok");
}

Expand Down Expand Up @@ -186,3 +189,24 @@ fn gone_after_what_was_sent((client, server): (Connection, Connection)) {
assert_eq!(recv(&server).err(), Some(SyscallError::Gone), "after the last message, the leaving");
assert_eq!(send(&server, b"x", vec![], false), Err(SyscallError::Gone), "a send to a peer gone");
}

fn a_call_takes_the_answer((client, server): (Connection, Connection)) {
let peer = std::thread::spawn(move || {
let mut bytes = [0u8; 16];
let mut handles = [RawHandle(0); MAX_TRANSFER_HANDLES];
let got = syscall::message_recv(server.as_handle().raw(), &mut [], &mut bytes, &mut handles, false).expect("the request");
assert_eq!(&bytes[..got.len], b"ask", "the request came cut");
let (h, seen) = token();
send(&server, b"answer", vec![h], false).expect("the answer");
seen
});
let (mut head, mut answer) = ([0u8; 3], [0u8; 16]);
let mut handles = [RawHandle(0); MAX_TRANSFER_HANDLES];
let got = syscall::message_call(client.as_handle().raw(), b"ask", &mut head, &mut answer, &mut handles).expect("the call's answer");
assert_eq!((got.len, got.handles), (6, 1), "the call took another answer");
assert_eq!((&head, &answer[..3]), (b"ans", &b"wer"[..]), "the call's answer was not split across its two places");
let seen = peer.join().expect("the peer");
// SAFETY: installed for this call.
drop(unsafe { OwnedHandle::from_raw(handles[0]) });
assert!(gone(&seen), "the answer's handle is not the one the peer sent");
}