From 4c7e2b83c1d7c153485ea204dfe4f0cee58316fe Mon Sep 17 00:00:00 2001 From: Anatol Belski Date: Wed, 17 Jun 2026 22:06:20 +0200 Subject: [PATCH] block: aligned: Query direct alignment in AlignedFile Move the statx STATX_DIOALIGN probe out of DiskTopology into a free probe_direct_alignment helper keyed on a raw fd. The helper gates on the O_DIRECT open flag and returns the kernel reported alignment only when direct I/O is in effect, and None otherwise. DiskTopology::probe keeps the same call path and result. AlignedFile::new now determines its O_DIRECT block alignment from probe_direct_alignment instead of trial reads at 512 and 4096, falling back to SECTOR_SIZE when the kernel does not report a value. This matches how the raw and fixed VHD workers determine alignment, so all backends agree on one source of truth. Signed-off-by: Anatol Belski --- block/src/aligned_file.rs | 29 ++++------ block/src/lib.rs | 113 +++++++++++++++++++------------------- 2 files changed, 68 insertions(+), 74 deletions(-) diff --git a/block/src/aligned_file.rs b/block/src/aligned_file.rs index 1a30cd938..c629b9299 100644 --- a/block/src/aligned_file.rs +++ b/block/src/aligned_file.rs @@ -5,11 +5,10 @@ use std::fs::File; use std::io; use std::os::unix::fs::FileExt; +use std::os::unix::io::AsRawFd; use crate::aligned_buffer::AlignedBuffer; - -/// O_DIRECT block-alignment candidates, smallest first. -const BLK_ALIGNMENTS: [usize; 2] = [512, 4096]; +use crate::{SECTOR_SIZE, probe_direct_alignment}; /// True when `buf_ptr`/`len`/`offset` already satisfy `alignment` /// (`alignment == 0` means no O_DIRECT, so everything is "aligned"). @@ -32,21 +31,13 @@ pub(crate) struct AlignedFile { } impl AlignedFile { - /// Wrap `file`, probing the O_DIRECT block alignment when `direct_io`. + /// Wrap `file`, querying the O_DIRECT block alignment when `direct_io`. pub fn new(file: File, direct_io: bool) -> Self { - let mut alignment = 0; - if direct_io { - for align in &BLK_ALIGNMENTS { - // Probe: an aligned read at `align` succeeds (a short read at - // EOF still counts) iff the fd accepts that O_DIRECT block size. - let ok = AlignedBuffer::new(*align as u64, *align, *align) - .is_ok_and(|mut b| b.read_from(&file).is_ok()); - if ok { - alignment = *align; - break; - } - } - } + let alignment = if direct_io { + probe_direct_alignment(file.as_raw_fd()).unwrap_or(SECTOR_SIZE) as usize + } else { + 0 + }; AlignedFile { file, alignment } } @@ -130,8 +121,8 @@ mod tests { #[test] fn new_probes_alignment_and_accessors() { let tf = pattern_file(8192); - // Non-O_DIRECT tempfile: an aligned read at `align` still succeeds, - // so the probe selects 512 (exercises new + probe). + // A tempfile is not O_DIRECT, so probe_direct_alignment reports + // None and new() falls back to SECTOR_SIZE (512). let mut af = AlignedFile::new(tf.as_file().try_clone().unwrap(), true); assert_eq!(af.alignment(), 512); let _ = af.file(); diff --git a/block/src/lib.rs b/block/src/lib.rs index 9f83929e8..7b7332155 100644 --- a/block/src/lib.rs +++ b/block/src/lib.rs @@ -324,6 +324,63 @@ pub(crate) fn is_block_device(fd: RawFd) -> bool { ret == 0 && stat.st_mode & S_IFMT == S_IFBLK } +/// Returns the kernel reported direct I/O alignment for `fd`, or `None` +/// when `fd` was not opened with O_DIRECT. +/// +/// When O_DIRECT is set, uses `statx(STATX_DIOALIGN)` (Linux >= 6.1) to obtain +/// the exact memory and offset alignment the kernel requires for direct I/O on +/// this specific fd. Unlike `fstatvfs().f_bsize`, which only returns the +/// filesystem's preferred I/O block size, `STATX_DIOALIGN` reports the true per +/// fd direct I/O constraint accounting for the filesystem, underlying block +/// device, and any stacking such as loop or device mapper. Falls back to +/// [`SECTOR_SIZE`] when the kernel does not report a value. +pub(crate) fn probe_direct_alignment(fd: RawFd) -> Option { + // SAFETY: fcntl(F_GETFL) is always safe on a valid fd. + let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) }; + if flags < 0 || (flags & libc::O_DIRECT) == 0 { + return None; + } + + // The libc crate does not expose statx / STATX_DIOALIGN on all targets, + // for example musl, so define the constant and a minimal repr(C) struct + // locally and invoke the syscall directly. + const STATX_DIOALIGN: u32 = 0x2000; + + // Minimal statx layout, only the needed fields, everything else is + // padding. + #[repr(C)] + struct Statx { + stx_mask: u32, + _pad: [u8; 148], + stx_dio_mem_align: u32, + stx_dio_offset_align: u32, + _pad2: [u8; 96], + } + + let mut stx = mem::MaybeUninit::::zeroed(); + // SAFETY: FFI syscall with valid fd and correctly sized buffer. + let ret = unsafe { + libc::syscall( + libc::SYS_statx, + fd, + c"".as_ptr(), + libc::AT_EMPTY_PATH, + STATX_DIOALIGN, + stx.as_mut_ptr(), + ) + }; + if ret == 0 { + // SAFETY: statx succeeded, the struct is fully initialized. + let stx = unsafe { stx.assume_init() }; + if stx.stx_mask & STATX_DIOALIGN != 0 && stx.stx_dio_mem_align > 0 { + return Some(cmp::max(stx.stx_dio_mem_align, stx.stx_dio_offset_align) as u64); + } + } + + debug!("O_DIRECT alignment query failed, falling back to default {SECTOR_SIZE}"); + Some(SECTOR_SIZE) +} + /// Probe whether the file/device supports punch hole and zero range pub fn probe_sparse_support(file: &File) -> bool { let fd = file.as_raw_fd(); @@ -617,66 +674,12 @@ impl DiskTopology { Ok(block_size) } - /// Query the O_DIRECT alignment requirement for a regular file. - /// - /// Uses `statx(STATX_DIOALIGN)` (Linux >= 6.1) to obtain the exact - /// memory and offset alignment the kernel requires for direct I/O on - /// this specific file. Unlike `fstatvfs().f_bsize`, which only returns - /// the filesystem's preferred I/O block size, `STATX_DIOALIGN` reports - /// the true per-file DIO constraints accounting for the filesystem, - /// underlying block device, and any stacking (loop, dm, etc.). - fn query_file_alignment(f: &File) -> u64 { - // The libc crate does not expose statx / STATX_DIOALIGN on all - // targets (e.g. musl), so define the constant and a minimal repr(C) - // struct locally and invoke the syscall directly. - const STATX_DIOALIGN: u32 = 0x2000; - - // Minimal statx layout, only the needed fields, - // everything else is padding. - #[repr(C)] - struct Statx { - stx_mask: u32, - _pad: [u8; 148], - stx_dio_mem_align: u32, - stx_dio_offset_align: u32, - _pad2: [u8; 96], - } - - let mut stx = mem::MaybeUninit::::zeroed(); - // SAFETY: FFI syscall with valid fd and correctly sized buffer. - let ret = unsafe { - libc::syscall( - libc::SYS_statx, - f.as_raw_fd(), - c"".as_ptr(), - libc::AT_EMPTY_PATH, - STATX_DIOALIGN, - stx.as_mut_ptr(), - ) - }; - if ret == 0 { - // SAFETY: statx succeeded, the struct is fully initialized. - let stx = unsafe { stx.assume_init() }; - if stx.stx_mask & STATX_DIOALIGN != 0 && stx.stx_dio_mem_align > 0 { - let align = cmp::max(stx.stx_dio_mem_align, stx.stx_dio_offset_align) as u64; - debug!("statx(STATX_DIOALIGN) returned alignment {align}"); - return align; - } - } - - debug!("O_DIRECT alignment query failed, falling back to default {SECTOR_SIZE}"); - SECTOR_SIZE - } - pub fn probe(f: &File) -> io::Result { if !is_block_device(f.as_raw_fd()) { // For regular files opened with O_DIRECT, the logical block size // must reflect the filesystem DIO alignment so the guest issues // correctly sized I/O. - // SAFETY: fcntl(F_GETFL) is always safe on a valid fd. - let flags = unsafe { libc::fcntl(f.as_raw_fd(), libc::F_GETFL) }; - if flags >= 0 && (flags & libc::O_DIRECT) != 0 { - let alignment = Self::query_file_alignment(f); + if let Some(alignment) = probe_direct_alignment(f.as_raw_fd()) { return Ok(DiskTopology { logical_block_size: alignment, physical_block_size: alignment,