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>
This commit is contained in:
Wei Liu
2026-05-12 16:07:27 +00:00
committed by Bo Chen
parent 12919dbce9
commit 2fe775fce2
6 changed files with 185 additions and 79 deletions

View File

@@ -11,13 +11,15 @@ use vmm_sys_util::eventfd::EventFd;
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
use crate::error::{BlockError, BlockErrorKind, BlockResult};
use crate::{BatchRequest, RequestType, SECTOR_SIZE};
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 {
@@ -34,13 +36,32 @@ impl RawFileAsync {
.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 {
@@ -235,6 +256,18 @@ impl AsyncIo for RawFileAsync {
}
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;
@@ -260,6 +293,14 @@ impl AsyncIo for RawFileAsync {
}
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;