From dd1fb36f366e3cd001e1604f4dd752824570f358 Mon Sep 17 00:00:00 2001 From: Dylan Reid Date: Mon, 18 May 2026 16:37:22 -0700 Subject: [PATCH] block: Add owned async I/O operations Add `AsyncIoOperation` and `AsyncIoCompletion` as the owned request and completion types that will be used to ensure buffers for async io outlive the operations that use them. Later commits will update the `AsyncIo` trait to expose apis using only these instead of raw iovecs. Signed-off-by: Dylan Reid --- block/src/async_io.rs | 4 + block/src/async_io/completion.rs | 42 ++++++ block/src/async_io/operation.rs | 239 +++++++++++++++++++++++++++++++ 3 files changed, 285 insertions(+) create mode 100644 block/src/async_io/completion.rs create mode 100644 block/src/async_io/operation.rs diff --git a/block/src/async_io.rs b/block/src/async_io.rs index 25361e334..b60b3fc92 100644 --- a/block/src/async_io.rs +++ b/block/src/async_io.rs @@ -4,10 +4,14 @@ use std::marker::PhantomData; use std::os::fd::{AsRawFd, OwnedFd, RawFd}; +mod completion; mod guest_memory_target; +mod operation; mod owned_io_buffer; +pub use completion::AsyncIoCompletion; pub use guest_memory_target::GuestMemoryTarget; +pub use operation::AsyncIoOperation; pub use owned_io_buffer::OwnedIoBuffer; use thiserror::Error; use vmm_sys_util::eventfd::EventFd; diff --git a/block/src/async_io/completion.rs b/block/src/async_io/completion.rs new file mode 100644 index 000000000..3f07c9e29 --- /dev/null +++ b/block/src/async_io/completion.rs @@ -0,0 +1,42 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause + +use super::{AsyncIoOperation, OwnedIoBuffer}; + +/// Completion returned by an owned async I/O backend. +/// +/// The completion carries the caller provided `user_data`, the result, +/// and any owned buffer that can now be dropped. +#[derive(Debug)] +pub struct AsyncIoCompletion { + /// Caller provided identifier associated with the submitted operation. + pub user_data: u64, + /// I/O result reported by the backend. + /// + /// Successful operations report a non-negative byte count. Failed + /// operations report a negative errno value. + pub result: i32, + /// The backing buffer that can now be dropped or re-used. + pub buffer: Option, +} + +impl AsyncIoCompletion { + /// Creates a completion from its parts. + pub fn new(user_data: u64, result: i32, buffer: Option) -> Self { + Self { + user_data, + result, + buffer, + } + } + + /// Creates a completion by consuming the operation that just completed. + /// + /// This returns ownership of any completion buffer carried by the + /// operation. + pub fn from_operation(op: AsyncIoOperation, result: i32) -> Self { + let user_data = op.user_data(); + Self::new(user_data, result, op.into_completion_buffer()) + } +} diff --git a/block/src/async_io/operation.rs b/block/src/async_io/operation.rs new file mode 100644 index 000000000..1ba6a42c7 --- /dev/null +++ b/block/src/async_io/operation.rs @@ -0,0 +1,239 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause + +use std::io; +use std::ops::Range; + +use super::{GuestMemoryTarget, OwnedIoBuffer}; + +/// A single async IO operation. +/// +/// Each operation owns or retains the memory target for the duration of the +/// operation so backends can submit it to the kernel or copy through safe helper +/// methods. +#[derive(Debug)] +pub enum AsyncIoOperation { + /// Read from disk into guest memory. + ReadToMemory { + /// Disk offset for the operation. + offset: libc::off_t, + /// Guest-memory destination. + target: GuestMemoryTarget, + /// Caller-provided completion identifier. + user_data: u64, + }, + /// Write from guest memory to disk. + WriteFromMemory { + /// Disk offset for the operation. + offset: libc::off_t, + /// Guest-memory source. + target: GuestMemoryTarget, + /// Caller-provided completion identifier. + user_data: u64, + }, + /// Read from disk into an owned host-memory buffer. + ReadToVec { + /// Disk offset for the operation. + offset: libc::off_t, + /// Owned destination buffer. + buffer: OwnedIoBuffer, + /// Caller-provided completion identifier. + user_data: u64, + }, + /// Write from an owned host-memory buffer to disk. + WriteFromVec { + /// Disk offset for the operation. + offset: libc::off_t, + /// Owned source buffer. + buffer: OwnedIoBuffer, + /// Caller-provided completion identifier. + user_data: u64, + }, +} + +impl AsyncIoOperation { + /// Creates an operation that reads from disk into guest memory. + pub fn read_to_memory(offset: libc::off_t, target: GuestMemoryTarget, user_data: u64) -> Self { + Self::ReadToMemory { + offset, + target, + user_data, + } + } + + /// Creates an operation that writes from guest memory to disk. + pub fn write_from_memory( + offset: libc::off_t, + target: GuestMemoryTarget, + user_data: u64, + ) -> Self { + Self::WriteFromMemory { + offset, + target, + user_data, + } + } + + /// Creates an operation that reads from disk into an owned buffer. + pub fn read_to_vec(offset: libc::off_t, buffer: OwnedIoBuffer, user_data: u64) -> Self { + Self::ReadToVec { + offset, + buffer, + user_data, + } + } + + /// Creates an operation that writes from an owned buffer to disk. + pub fn write_from_vec(offset: libc::off_t, buffer: OwnedIoBuffer, user_data: u64) -> Self { + Self::WriteFromVec { + offset, + buffer, + user_data, + } + } + + /// Returns the value provided at construction. + pub fn user_data(&self) -> u64 { + match self { + Self::ReadToMemory { user_data, .. } + | Self::WriteFromMemory { user_data, .. } + | Self::ReadToVec { user_data, .. } + | Self::WriteFromVec { user_data, .. } => *user_data, + } + } + + /// Returns the disk offset for this operation. + pub fn offset(&self) -> libc::off_t { + match self { + Self::ReadToMemory { offset, .. } + | Self::WriteFromMemory { offset, .. } + | Self::ReadToVec { offset, .. } + | Self::WriteFromVec { offset, .. } => *offset, + } + } + + /// Updates the disk offset for this operation. + pub fn set_offset(&mut self, new_offset: libc::off_t) { + match self { + Self::ReadToMemory { offset, .. } + | Self::WriteFromMemory { offset, .. } + | Self::ReadToVec { offset, .. } + | Self::WriteFromVec { offset, .. } => *offset = new_offset, + } + } + + /// Returns whether this operation reads from disk. + pub fn is_read(&self) -> bool { + matches!(self, Self::ReadToMemory { .. } | Self::ReadToVec { .. }) + } + + /// Returns the retained iovec array for kernel submission. + /// + /// The iovec pointers are valid while this operation is alive. + pub fn iovecs(&self) -> &[libc::iovec] { + match self { + Self::ReadToMemory { target, .. } | Self::WriteFromMemory { target, .. } => { + target.iovecs() + } + Self::ReadToVec { buffer, .. } | Self::WriteFromVec { buffer, .. } => buffer.iovecs(), + } + } + + /// Returns the total number of bytes described by the operation iovecs. + pub fn total_len(&self) -> usize { + match self { + Self::ReadToMemory { target, .. } | Self::WriteFromMemory { target, .. } => { + target.total_len() + } + Self::ReadToVec { buffer, .. } | Self::WriteFromVec { buffer, .. } => { + buffer.total_len() + } + } + } + + #[allow(dead_code)] + fn checked_range(total_len: usize, start: usize, len: usize) -> io::Result> { + if start <= total_len + && let Some(end) = start.checked_add(len) + && end <= total_len + { + return Ok(start..end); + } + + Err(io::Error::new( + io::ErrorKind::InvalidInput, + "async I/O buffer range out of bounds", + )) + } + + /// Copies bytes into a read operation at `start`. + #[allow(dead_code)] + pub(crate) fn write_bytes_at(&mut self, start: usize, data: &[u8]) -> io::Result<()> { + match self { + Self::ReadToMemory { target, .. } => { + target.write_bytes_at(start, data).map_err(io::Error::other) + } + Self::ReadToVec { buffer, .. } => { + let range = Self::checked_range(buffer.total_len(), start, data.len())?; + buffer.as_mut_slice()[range].copy_from_slice(data); + Ok(()) + } + Self::WriteFromMemory { .. } | Self::WriteFromVec { .. } => Err(io::Error::new( + io::ErrorKind::InvalidInput, + "cannot write into a write operation", + )), + } + } + + /// Fills a read operation with zeroes at `start`. + #[allow(dead_code)] + pub(crate) fn fill_zeroes_at(&mut self, start: usize, len: usize) -> io::Result<()> { + match self { + Self::ReadToMemory { target, .. } => { + target.fill_zeroes_at(start, len).map_err(io::Error::other) + } + Self::ReadToVec { buffer, .. } => { + let range = Self::checked_range(buffer.total_len(), start, len)?; + buffer.as_mut_slice()[range].fill(0); + Ok(()) + } + Self::WriteFromMemory { .. } | Self::WriteFromVec { .. } => Err(io::Error::new( + io::ErrorKind::InvalidInput, + "cannot write into a write operation", + )), + } + } + + /// Copies bytes out of a write operation at `start`. + #[allow(dead_code)] + pub(crate) fn read_bytes_at(&self, start: usize, data: &mut [u8]) -> io::Result<()> { + match self { + Self::WriteFromMemory { target, .. } => { + target.read_bytes_at(start, data).map_err(io::Error::other) + } + Self::WriteFromVec { buffer, .. } => { + let range = Self::checked_range(buffer.total_len(), start, data.len())?; + data.copy_from_slice(&buffer.as_slice()[range]); + Ok(()) + } + Self::ReadToMemory { .. } | Self::ReadToVec { .. } => Err(io::Error::new( + io::ErrorKind::InvalidInput, + "cannot read from a read operation", + )), + } + } + + /// Consumes the operation and returns the buffer needed by its completion. + /// + /// Only `ReadToVec` operations return a buffer because callers need the + /// data they read. + pub fn into_completion_buffer(self) -> Option { + match self { + Self::ReadToVec { buffer, .. } => Some(buffer), + Self::ReadToMemory { .. } + | Self::WriteFromMemory { .. } + | Self::WriteFromVec { .. } => None, + } + } +}