mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
block: Implement write_zeroes and punch_hole for AIO backend
The AIO block backend advertises VIRTIO_BLK_F_WRITE_ZEROES and VIRTIO_BLK_F_DISCARD to guests because the filesystem probe (supports_sparse_operations) returns true on ext4/XFS. However, RawFileAsyncAio::write_zeroes() and punch_hole() return errors because Linux AIO (io_submit) has no IOCB command for fallocate. When io_uring is unavailable (e.g. io_uring_disabled=2, a common security hardening on enterprise Linux), Cloud Hypervisor falls back to the AIO backend. The guest negotiates the feature, issues WRITE_ZEROES requests, and gets I/O errors. Implement write_zeroes and punch_hole using synchronous libc::fallocate() calls, matching the pattern used by the sync backend (RawFileSync). A VecDeque-based completion list signals results to the caller via the existing eventfd mechanism. Unit tests mirror the existing raw_sync.rs test suite. Integration tests add AIO-specific variants of the discard and fstrim tests using _disable_io_uring=on. Signed-off-by: Emir Beganovic <beganovic.emir@gmail.com>
This commit is contained in:
committed by
Rob Bradford
parent
8671e193ff
commit
623af62743
@@ -5,6 +5,7 @@
|
||||
// Copyright © 2023 Crusoe Energy Systems LLC
|
||||
//
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::File;
|
||||
use std::io::{Seek, SeekFrom};
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
@@ -73,6 +74,7 @@ pub struct RawFileAsyncAio {
|
||||
ctx: aio::IoContext,
|
||||
eventfd: EventFd,
|
||||
alignment: u64,
|
||||
completion_list: VecDeque<(u64, i32)>,
|
||||
}
|
||||
|
||||
impl RawFileAsyncAio {
|
||||
@@ -85,6 +87,7 @@ impl RawFileAsyncAio {
|
||||
ctx,
|
||||
eventfd,
|
||||
alignment: SECTOR_SIZE,
|
||||
completion_list: VecDeque::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -168,6 +171,11 @@ impl AsyncIo for RawFileAsyncAio {
|
||||
}
|
||||
|
||||
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
||||
// Drain synchronous completions first (from punch_hole/write_zeroes).
|
||||
if let Some(completed) = self.completion_list.pop_front() {
|
||||
return Some(completed);
|
||||
}
|
||||
|
||||
let mut events: [aio::IoEvent; 1] = [aio::IoEvent::default()];
|
||||
let rc = self.ctx.get_events(0, &mut events, None).unwrap();
|
||||
if rc == 0 {
|
||||
@@ -177,15 +185,231 @@ impl AsyncIo for RawFileAsyncAio {
|
||||
}
|
||||
}
|
||||
|
||||
fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
|
||||
Err(AsyncIoError::PunchHole(std::io::Error::other(
|
||||
"punch_hole not supported with AIO backend",
|
||||
)))
|
||||
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
|
||||
// Linux AIO has no IOCB command for fallocate, so perform the operation
|
||||
// synchronously and signal completion via the completion list, matching
|
||||
// the pattern used by the sync backend (RawFileSync).
|
||||
const FALLOC_FL_PUNCH_HOLE: i32 = 0x02;
|
||||
const FALLOC_FL_KEEP_SIZE: i32 = 0x01;
|
||||
let mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE;
|
||||
|
||||
// SAFETY: FFI call with valid arguments
|
||||
let result = unsafe {
|
||||
libc::fallocate(
|
||||
self.fd as libc::c_int,
|
||||
mode,
|
||||
offset as libc::off_t,
|
||||
length as libc::off_t,
|
||||
)
|
||||
};
|
||||
if result < 0 {
|
||||
return Err(AsyncIoError::PunchHole(std::io::Error::last_os_error()));
|
||||
}
|
||||
|
||||
self.completion_list.push_back((user_data, result));
|
||||
self.eventfd.write(1).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
|
||||
Err(AsyncIoError::WriteZeroes(std::io::Error::other(
|
||||
"write_zeroes not supported with AIO backend",
|
||||
)))
|
||||
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
|
||||
// Linux AIO has no IOCB command for fallocate, so perform the operation
|
||||
// synchronously and signal completion via the completion list, matching
|
||||
// the pattern used by the sync backend (RawFileSync).
|
||||
const FALLOC_FL_ZERO_RANGE: i32 = 0x10;
|
||||
const FALLOC_FL_KEEP_SIZE: i32 = 0x01;
|
||||
let mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE;
|
||||
|
||||
// SAFETY: FFI call with valid arguments
|
||||
let result = unsafe {
|
||||
libc::fallocate(
|
||||
self.fd as libc::c_int,
|
||||
mode,
|
||||
offset as libc::off_t,
|
||||
length as libc::off_t,
|
||||
)
|
||||
};
|
||||
if result < 0 {
|
||||
return Err(AsyncIoError::WriteZeroes(std::io::Error::last_os_error()));
|
||||
}
|
||||
|
||||
self.completion_list.push_back((user_data, result));
|
||||
self.eventfd.write(1).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_tests {
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
|
||||
use vmm_sys_util::tempfile::TempFile;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_punch_hole() {
|
||||
let temp_file = TempFile::new().unwrap();
|
||||
let mut file = temp_file.into_file();
|
||||
|
||||
// Write 4MB of data
|
||||
let data = vec![0xAA; 4 * 1024 * 1024];
|
||||
file.write_all(&data).unwrap();
|
||||
file.sync_all().unwrap();
|
||||
|
||||
// Create async IO instance
|
||||
let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap();
|
||||
|
||||
// Punch hole in the middle (1MB at offset 1MB)
|
||||
let offset = 1024 * 1024;
|
||||
let length = 1024 * 1024;
|
||||
async_io.punch_hole(offset, length, 1).unwrap();
|
||||
|
||||
// Check completion
|
||||
let (user_data, result) = async_io.next_completed_request().unwrap();
|
||||
assert_eq!(user_data, 1);
|
||||
assert_eq!(result, 0);
|
||||
|
||||
// Verify the hole reads as zeros
|
||||
file.seek(SeekFrom::Start(offset)).unwrap();
|
||||
let mut read_buf = vec![0; length as usize];
|
||||
file.read_exact(&mut read_buf).unwrap();
|
||||
assert!(
|
||||
read_buf.iter().all(|&b| b == 0),
|
||||
"Punched hole should read as zeros"
|
||||
);
|
||||
|
||||
// Verify data before hole is intact
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut read_buf = vec![0; 1024];
|
||||
file.read_exact(&mut read_buf).unwrap();
|
||||
assert!(
|
||||
read_buf.iter().all(|&b| b == 0xAA),
|
||||
"Data before hole should be intact"
|
||||
);
|
||||
|
||||
// Verify data after hole is intact
|
||||
file.seek(SeekFrom::Start(offset + length)).unwrap();
|
||||
let mut read_buf = vec![0; 1024];
|
||||
file.read_exact(&mut read_buf).unwrap();
|
||||
assert!(
|
||||
read_buf.iter().all(|&b| b == 0xAA),
|
||||
"Data after hole should be intact"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_zeroes() {
|
||||
let temp_file = TempFile::new().unwrap();
|
||||
let mut file = temp_file.into_file();
|
||||
|
||||
// Write 4MB of data
|
||||
let data = vec![0xBB; 4 * 1024 * 1024];
|
||||
file.write_all(&data).unwrap();
|
||||
file.sync_all().unwrap();
|
||||
|
||||
// Create async IO instance
|
||||
let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap();
|
||||
|
||||
// Write zeros in the middle (512KB at offset 2MB)
|
||||
let offset = 2 * 1024 * 1024;
|
||||
let length = 512 * 1024;
|
||||
let write_zeroes_result = async_io.write_zeroes(offset, length, 2);
|
||||
|
||||
// FALLOC_FL_ZERO_RANGE might not be supported on all filesystems (e.g., tmpfs)
|
||||
// If it fails with ENOTSUP, skip the test
|
||||
if let Err(AsyncIoError::WriteZeroes(ref e)) = write_zeroes_result
|
||||
&& (e.raw_os_error() == Some(libc::EOPNOTSUPP)
|
||||
|| e.raw_os_error() == Some(libc::ENOTSUP))
|
||||
{
|
||||
eprintln!(
|
||||
"Skipping test_write_zeroes: filesystem doesn't support FALLOC_FL_ZERO_RANGE"
|
||||
);
|
||||
return;
|
||||
}
|
||||
write_zeroes_result.unwrap();
|
||||
|
||||
// Check completion
|
||||
let (user_data, result) = async_io.next_completed_request().unwrap();
|
||||
assert_eq!(user_data, 2);
|
||||
assert_eq!(result, 0);
|
||||
|
||||
// Verify the zeroed region reads as zeros
|
||||
file.seek(SeekFrom::Start(offset)).unwrap();
|
||||
let mut read_buf = vec![0; length as usize];
|
||||
file.read_exact(&mut read_buf).unwrap();
|
||||
assert!(
|
||||
read_buf.iter().all(|&b| b == 0),
|
||||
"Zeroed region should read as zeros"
|
||||
);
|
||||
|
||||
// Verify data before zeroed region is intact
|
||||
file.seek(SeekFrom::Start(offset - 1024)).unwrap();
|
||||
let mut read_buf = vec![0; 1024];
|
||||
file.read_exact(&mut read_buf).unwrap();
|
||||
assert!(
|
||||
read_buf.iter().all(|&b| b == 0xBB),
|
||||
"Data before zeroed region should be intact"
|
||||
);
|
||||
|
||||
// Verify data after zeroed region is intact
|
||||
file.seek(SeekFrom::Start(offset + length)).unwrap();
|
||||
let mut read_buf = vec![0; 1024];
|
||||
file.read_exact(&mut read_buf).unwrap();
|
||||
assert!(
|
||||
read_buf.iter().all(|&b| b == 0xBB),
|
||||
"Data after zeroed region should be intact"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_punch_hole_multiple_operations() {
|
||||
let temp_file = TempFile::new().unwrap();
|
||||
let mut file = temp_file.into_file();
|
||||
|
||||
// Write 8MB of data
|
||||
let data = vec![0xCC; 8 * 1024 * 1024];
|
||||
file.write_all(&data).unwrap();
|
||||
file.sync_all().unwrap();
|
||||
|
||||
// Create async IO instance
|
||||
let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap();
|
||||
|
||||
// Punch multiple holes
|
||||
async_io.punch_hole(1024 * 1024, 512 * 1024, 10).unwrap();
|
||||
async_io
|
||||
.punch_hole(3 * 1024 * 1024, 512 * 1024, 11)
|
||||
.unwrap();
|
||||
async_io
|
||||
.punch_hole(5 * 1024 * 1024, 512 * 1024, 12)
|
||||
.unwrap();
|
||||
|
||||
// Check all completions
|
||||
let (user_data, result) = async_io.next_completed_request().unwrap();
|
||||
assert_eq!(user_data, 10);
|
||||
assert_eq!(result, 0);
|
||||
|
||||
let (user_data, result) = async_io.next_completed_request().unwrap();
|
||||
assert_eq!(user_data, 11);
|
||||
assert_eq!(result, 0);
|
||||
|
||||
let (user_data, result) = async_io.next_completed_request().unwrap();
|
||||
assert_eq!(user_data, 12);
|
||||
assert_eq!(result, 0);
|
||||
|
||||
// Verify all holes read as zeros
|
||||
file.seek(SeekFrom::Start(1024 * 1024)).unwrap();
|
||||
let mut read_buf = vec![0; 512 * 1024];
|
||||
file.read_exact(&mut read_buf).unwrap();
|
||||
assert!(read_buf.iter().all(|&b| b == 0));
|
||||
|
||||
file.seek(SeekFrom::Start(3 * 1024 * 1024)).unwrap();
|
||||
file.read_exact(&mut read_buf).unwrap();
|
||||
assert!(read_buf.iter().all(|&b| b == 0));
|
||||
|
||||
file.seek(SeekFrom::Start(5 * 1024 * 1024)).unwrap();
|
||||
file.read_exact(&mut read_buf).unwrap();
|
||||
assert!(read_buf.iter().all(|&b| b == 0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7532,6 +7532,24 @@ mod common_parallel {
|
||||
extra_create_args: &[&str],
|
||||
expect_discard_success: bool,
|
||||
verify_disk: bool,
|
||||
) {
|
||||
_test_virtio_block_discard_with_backend(
|
||||
format_name,
|
||||
qemu_img_format,
|
||||
extra_create_args,
|
||||
expect_discard_success,
|
||||
verify_disk,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
fn _test_virtio_block_discard_with_backend(
|
||||
format_name: &str,
|
||||
qemu_img_format: &str,
|
||||
extra_create_args: &[&str],
|
||||
expect_discard_success: bool,
|
||||
verify_disk: bool,
|
||||
disable_io_uring: bool,
|
||||
) {
|
||||
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
|
||||
let guest = Guest::new(Box::new(disk_config));
|
||||
@@ -7574,9 +7592,14 @@ mod common_parallel {
|
||||
)
|
||||
.as_str(),
|
||||
format!(
|
||||
"path={},num_queues=4,image_type={}",
|
||||
"path={},num_queues=4,image_type={}{}",
|
||||
test_disk_path.to_str().unwrap(),
|
||||
format_name.to_lowercase()
|
||||
format_name.to_lowercase(),
|
||||
if disable_io_uring {
|
||||
",_disable_io_uring=on"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
)
|
||||
.as_str(),
|
||||
])
|
||||
@@ -7754,6 +7777,11 @@ mod common_parallel {
|
||||
_test_virtio_block_discard("raw", "raw", &[], true, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_virtio_block_discard_raw_aio() {
|
||||
_test_virtio_block_discard_with_backend("raw", "raw", &[], true, false, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_virtio_block_discard_unsupported_vhd() {
|
||||
_test_virtio_block_discard("vhd", "vpc", &["-o", "subformat=fixed"], false, false);
|
||||
@@ -8056,6 +8084,24 @@ mod common_parallel {
|
||||
extra_create_args: &[&str],
|
||||
expect_fstrim_success: bool,
|
||||
verify_disk: bool,
|
||||
) {
|
||||
_test_virtio_block_fstrim_with_backend(
|
||||
format_name,
|
||||
qemu_img_format,
|
||||
extra_create_args,
|
||||
expect_fstrim_success,
|
||||
verify_disk,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
fn _test_virtio_block_fstrim_with_backend(
|
||||
format_name: &str,
|
||||
qemu_img_format: &str,
|
||||
extra_create_args: &[&str],
|
||||
expect_fstrim_success: bool,
|
||||
verify_disk: bool,
|
||||
disable_io_uring: bool,
|
||||
) {
|
||||
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
|
||||
let guest = Guest::new(Box::new(disk_config));
|
||||
@@ -8101,9 +8147,14 @@ mod common_parallel {
|
||||
)
|
||||
.as_str(),
|
||||
format!(
|
||||
"path={},num_queues=4,image_type={}",
|
||||
"path={},num_queues=4,image_type={}{}",
|
||||
test_disk_path.to_str().unwrap(),
|
||||
format_name.to_lowercase()
|
||||
format_name.to_lowercase(),
|
||||
if disable_io_uring {
|
||||
",_disable_io_uring=on"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
)
|
||||
.as_str(),
|
||||
])
|
||||
@@ -8242,6 +8293,11 @@ mod common_parallel {
|
||||
_test_virtio_block_fstrim("raw", "raw", &[], true, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_virtio_block_fstrim_raw_aio() {
|
||||
_test_virtio_block_fstrim_with_backend("raw", "raw", &[], true, false, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_virtio_block_fstrim_unsupported_vhd() {
|
||||
_test_virtio_block_fstrim("vhd", "vpc", &["-o", "subformat=fixed"], false, false);
|
||||
|
||||
Reference in New Issue
Block a user