Files
cloud-hypervisor/block/src/raw_async.rs
Wei Liu 2fe775fce2 block: use BLKDISCARD/BLKZEROOUT ioctls for block devices
Some block devices (ZFS volume) may require BLKDISCARD and BLKZEROOUT
ioctls for discard and write_zeroes operations respectively.

There is no good way to probe whether fallocate is supported on a block
device. Arguably, punch_hole and write_zeroes are rare. Instead of
having a complex scheme for the IO uring backend, we force it to always
use ioctls. The code can be changed if the synchronized ioctls become a
performance issue.

Changes:
- Detect block devices at construction time
- Use BLKDISCARD ioctl for punch_hole (discard) on block devices
- Use BLKZEROOUT ioctl for write_zeroes on block devices
- Add BLKDISCARD/BLKZEROOUT to VirtioBlock seccomp whitelist
- Keep fallocate() path for regular files (no behavior change)
- Consolidate some helper functions to the new sparse module

Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-05-14 22:35:02 +00:00

328 lines
11 KiB
Rust

// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::io::Error;
use std::os::unix::io::{AsRawFd, RawFd};
use io_uring::{IoUring, opcode, types};
use libc::{FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE};
use vmm_sys_util::eventfd::EventFd;
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
use crate::error::{BlockError, BlockErrorKind, BlockResult};
use crate::sparse::{blkdiscard, blkzeroout};
use crate::{BatchRequest, RequestType, SECTOR_SIZE, is_block_device};
pub struct RawFileAsync {
fd: RawFd,
io_uring: IoUring,
eventfd: EventFd,
alignment: u64,
is_block_device: bool,
}
impl RawFileAsync {
pub fn new(fd: RawFd, ring_depth: u32) -> BlockResult<Self> {
let io_uring =
IoUring::new(ring_depth).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
let eventfd =
EventFd::new(libc::EFD_NONBLOCK).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
// Register the io_uring eventfd that will notify when something in
// the completion queue is ready.
io_uring
.submitter()
.register_eventfd(eventfd.as_raw_fd())
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
let is_block_device = is_block_device(fd);
Ok(RawFileAsync {
fd,
io_uring,
eventfd,
alignment: SECTOR_SIZE,
is_block_device,
})
}
/// Queue an `IORING_OP_NOP` carrying `user_data` so a synchronously
/// completed operation (e.g. a BLK* ioctl) is reaped through the normal
/// io_uring completion path.
fn submit_nop(&mut self, user_data: u64) -> Result<(), Error> {
let (submitter, mut sq, _) = self.io_uring.split();
// SAFETY: Nop carries no buffer; only `user_data` is consumed by the
// kernel.
unsafe {
sq.push(&opcode::Nop::new().build().user_data(user_data))
.map_err(|e| Error::other(format!("Submission queue is full: {e:?}")))?;
};
sq.sync();
submitter.submit()?;
Ok(())
}
}
impl AsyncIo for RawFileAsync {
fn notifier(&self) -> &EventFd {
&self.eventfd
}
fn alignment(&self) -> u64 {
self.alignment
}
fn read_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
let (submitter, mut sq, _) = self.io_uring.split();
// SAFETY: we know the file descriptor is valid and we
// relied on vm-memory to provide the buffer address.
unsafe {
sq.push(
&opcode::Readv::new(types::Fd(self.fd), iovecs.as_ptr(), iovecs.len() as u32)
.offset(offset.try_into().unwrap())
.build()
.user_data(user_data),
)
.map_err(|e| {
AsyncIoError::ReadVectored(Error::other(format!("Submission queue is full: {e:?}")))
})?;
};
// Update the submission queue and submit new operations to the
// io_uring instance.
sq.sync();
submitter.submit().map_err(AsyncIoError::ReadVectored)?;
Ok(())
}
fn write_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
let (submitter, mut sq, _) = self.io_uring.split();
// SAFETY: we know the file descriptor is valid and we
// relied on vm-memory to provide the buffer address.
unsafe {
sq.push(
&opcode::Writev::new(types::Fd(self.fd), iovecs.as_ptr(), iovecs.len() as u32)
.offset(offset.try_into().unwrap())
.build()
.user_data(user_data),
)
.map_err(|e| {
AsyncIoError::WriteVectored(Error::other(format!(
"Submission queue is full: {e:?}"
)))
})?;
};
// Update the submission queue and submit new operations to the
// io_uring instance.
sq.sync();
submitter.submit().map_err(AsyncIoError::WriteVectored)?;
Ok(())
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
if let Some(user_data) = user_data {
let (submitter, mut sq, _) = self.io_uring.split();
// SAFETY: we know the file descriptor is valid.
unsafe {
sq.push(
&opcode::Fsync::new(types::Fd(self.fd))
.build()
.user_data(user_data),
)
.map_err(|e| {
AsyncIoError::Fsync(Error::other(format!("Submission queue is full: {e:?}")))
})?;
};
// Update the submission queue and submit new operations to the
// io_uring instance.
sq.sync();
submitter.submit().map_err(AsyncIoError::Fsync)?;
} else {
// SAFETY: FFI call with a valid fd
unsafe { libc::fsync(self.fd) };
}
Ok(())
}
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
self.io_uring
.completion()
.next()
.map(|entry| (entry.user_data(), entry.result()))
}
fn batch_requests_enabled(&self) -> bool {
true
}
fn submit_batch_requests(&mut self, batch_request: &[BatchRequest]) -> AsyncIoResult<()> {
if !self.batch_requests_enabled() {
return Ok(());
}
let (submitter, mut sq, _) = self.io_uring.split();
let mut submitted = false;
// Refuse the whole batch if it can't fit in the SQ to avoid having to unroll a partially
// successful push.
if batch_request.len() > sq.capacity() - sq.len() {
return Err(AsyncIoError::SubmitBatchRequests(Error::other(
"io_uring submission queue is full",
)));
}
for req in batch_request {
match req.request_type {
RequestType::In => {
// SAFETY: we know the file descriptor is valid and we
// relied on vm-memory to provide the buffer address.
unsafe {
sq.push(
&opcode::Readv::new(
types::Fd(self.fd),
req.iovecs.as_ptr(),
req.iovecs.len() as u32,
)
.offset(req.offset as u64)
.build()
.user_data(req.user_data),
)
.map_err(|e| {
AsyncIoError::ReadVectored(Error::other(format!(
"Submission queue is full: {e:?}"
)))
})?;
};
submitted = true;
}
RequestType::Out => {
// SAFETY: we know the file descriptor is valid and we
// relied on vm-memory to provide the buffer address.
unsafe {
sq.push(
&opcode::Writev::new(
types::Fd(self.fd),
req.iovecs.as_ptr(),
req.iovecs.len() as u32,
)
.offset(req.offset as u64)
.build()
.user_data(req.user_data),
)
.map_err(|e| {
AsyncIoError::WriteVectored(Error::other(format!(
"Submission queue is full: {e:?}"
)))
})?;
};
submitted = true;
}
_ => {
unreachable!("Unexpected batch request type: {:?}", req.request_type)
}
}
}
// Only submit if we actually queued something
if submitted {
// Update the submission queue and submit new operations to the
// io_uring instance.
sq.sync();
submitter
.submit()
.map_err(AsyncIoError::SubmitBatchRequests)?;
}
Ok(())
}
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
// Some block devices don't support fallocate(). Use ioctl instead. The assumption is that
// this happens rarely and we don't need to introduce unnecessary complexity by submitting
// a fallocate request, reaping ENOTSUPP in the completion routine, and reissuing the
// request with an ioctl.
if self.is_block_device {
blkdiscard(self.fd, offset, length).map_err(AsyncIoError::PunchHole)?;
// Deliver the completion through the normal io_uring path by
// queuing a NOP carrying `user_data`. The registered eventfd will
// fire when it completes, just like any other request.
return self.submit_nop(user_data).map_err(AsyncIoError::PunchHole);
}
let (submitter, mut sq, _) = self.io_uring.split();
let mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE;
// SAFETY: The file descriptor is known to be valid.
unsafe {
sq.push(
&opcode::Fallocate::new(types::Fd(self.fd), length)
.offset(offset)
.mode(mode)
.build()
.user_data(user_data),
)
.map_err(|e| {
AsyncIoError::PunchHole(Error::other(format!("Submission queue is full: {e:?}")))
})?;
};
sq.sync();
submitter.submit().map_err(AsyncIoError::PunchHole)?;
Ok(())
}
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
// Same rationale as punch_hole().
if self.is_block_device {
blkzeroout(self.fd, offset, length).map_err(AsyncIoError::WriteZeroes)?;
return self
.submit_nop(user_data)
.map_err(AsyncIoError::WriteZeroes);
}
let (submitter, mut sq, _) = self.io_uring.split();
let mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE;
// SAFETY: The file descriptor is known to be valid.
unsafe {
sq.push(
&opcode::Fallocate::new(types::Fd(self.fd), length)
.offset(offset)
.mode(mode)
.build()
.user_data(user_data),
)
.map_err(|e| {
AsyncIoError::WriteZeroes(Error::other(format!("Submission queue is full: {e:?}")))
})?;
};
sq.sync();
submitter.submit().map_err(AsyncIoError::WriteZeroes)?;
Ok(())
}
}