From 6ffcb2402c004d92bf227d1d0430de65828f0d55 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 31 Oct 2026 18:01:59 +0100 Subject: [PATCH] a call sends a whole message and takes the next one back SYS_MESSAGE_CALL is SYS_MESSAGE_SEND and then a blocking SYS_MESSAGE_RECV on the same connection, in one syscall. A refused send answers as a send does with nothing sent; once the request is queued the receive answers as a receive does, into the same two windows. A protocol whose peer answers each request in turn makes the next message its answer. The request carries no handles: an error could come from either half, so a caller could not know whether they had moved. The call no longer tells the scheduler that its caller parks next: #1111 dropped WakeCause::parks_next (e938fe3e5), and #1129's waker-LLC affinity carries the waker on every wake instead. What is left is one syscall entry and exit per round trip. Consumers: the file server's client (toyos::fs's request and reply) and osbench's ipc.reqrep rows, whose request's last frame and answer's first are one call. Connection::call takes the answer straight into the caller's buffer, as recv_into does. messages_are_whole checks that a call takes the answer its peer sends, with the answer's handle, split across both windows. Co-Authored-By: Claude Opus 5.5 --- kernel/src/syscall/dispatch.rs | 15 ++++++- kernel/src/syscall/ipc.rs | 9 +++++ sdk/abi/src/syscall.rs | 39 ++++++++++++++++++- sdk/toyos/src/fs.rs | 9 ++--- sdk/toyos/src/ipc.rs | 14 +++++++ tests/guest/osbench/src/ipc.rs | 15 +++---- tests/guest/osbench/src/sys/linux.rs | 6 +++ tests/guest/osbench/src/sys/toyos.rs | 15 +++++++ .../rust-tests/src/bin/messages_are_whole.rs | 28 ++++++++++++- 9 files changed, 133 insertions(+), 17 deletions(-) diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index af6dffa7937..b27ebfce7e7 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -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, }; @@ -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::(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), diff --git a/kernel/src/syscall/ipc.rs b/kernel/src/syscall/ipc.rs index 0d54af17cc7..162b94762f8 100644 --- a/kernel/src/syscall/ipc.rs +++ b/kernel/src/syscall/ipc.rs @@ -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]; diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index b0531cf3bc7..ed9467a3e24 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -347,6 +347,9 @@ 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. @@ -354,13 +357,13 @@ pub const SYS_MESSAGE_RECV: u64 = 152; /// **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 @@ -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 { + 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 diff --git a/sdk/toyos/src/fs.rs b/sdk/toyos/src/fs.rs index f40ac570dae..7bcce45d46a 100644 --- a/sdk/toyos/src/fs.rs +++ b/sdk/toyos/src/fs.rs @@ -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::() }>::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::()]; + 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; diff --git a/sdk/toyos/src/ipc.rs b/sdk/toyos/src/ipc.rs index 9269a67cca4..5fc150466ce 100644 --- a/sdk/toyos/src/ipc.rs +++ b/sdk/toyos/src/ipc.rs @@ -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::>::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 diff --git a/tests/guest/osbench/src/ipc.rs b/tests/guest/osbench/src/ipc.rs index 5d338adb98c..b437b36c24f 100644 --- a/tests/guest/osbench/src/ipc.rs +++ b/tests/guest/osbench/src/ipc.rs @@ -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; @@ -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> { + let buf = vec![0x42u8; bytes]; + let run = || -> std::io::Result> { 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)))) diff --git a/tests/guest/osbench/src/sys/linux.rs b/tests/guest/osbench/src/sys/linux.rs index 2297415bf23..503d22370ae 100644 --- a/tests/guest/osbench/src/sys/linux.rs +++ b/tests/guest/osbench/src/sys/linux.rs @@ -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. diff --git a/tests/guest/osbench/src/sys/toyos.rs b/tests/guest/osbench/src/sys/toyos.rs index 8895614bfaa..7963b19c0fc 100644 --- a/tests/guest/osbench/src/sys/toyos.rs +++ b/tests/guest/osbench/src/sys/toyos.rs @@ -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 { diff --git a/tests/guest/rust-tests/src/bin/messages_are_whole.rs b/tests/guest/rust-tests/src/bin/messages_are_whole.rs index 36c3d66a8a8..f6cea70f131 100644 --- a/tests/guest/rust-tests/src/bin/messages_are_whole.rs +++ b/tests/guest/rust-tests/src/bin/messages_are_whole.rs @@ -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 @@ -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; @@ -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"); } @@ -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"); +}