// Copyright © 2021 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::fs::File; use std::io::{self, Error}; use std::os::unix::fs::FileTypeExt; 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 log::warn; use vmm_sys_util::eventfd::EventFd; use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFileError}; use crate::error::{BlockError, BlockErrorKind, BlockResult}; use crate::{ BatchRequest, DiskTopology, RequestType, SECTOR_SIZE, disk_file, probe_sparse_support, query_device_size, }; #[derive(Debug)] pub struct RawFileDisk { file: File, } impl RawFileDisk { pub fn new(file: File) -> Self { RawFileDisk { file } } } impl disk_file::DiskSize for RawFileDisk { fn logical_size(&self) -> BlockResult { query_device_size(&self.file) .map(|(logical_size, _)| logical_size) .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Size(e))) } } impl disk_file::PhysicalSize for RawFileDisk { fn physical_size(&self) -> BlockResult { query_device_size(&self.file) .map(|(_, physical_size)| physical_size) .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Size(e))) } } impl disk_file::DiskFd for RawFileDisk { fn fd(&self) -> BorrowedDiskFd<'_> { BorrowedDiskFd::new(self.file.as_raw_fd()) } } impl disk_file::Geometry for RawFileDisk { fn topology(&self) -> DiskTopology { DiskTopology::probe(&self.file).unwrap_or_else(|_| { warn!("Unable to get device topology. Using default topology"); DiskTopology::default() }) } } impl disk_file::SparseCapable for RawFileDisk { fn supports_sparse_operations(&self) -> bool { probe_sparse_support(&self.file) } } impl disk_file::Resizable for RawFileDisk { fn resize(&mut self, size: u64) -> BlockResult<()> { let fd_metadata = self .file .metadata() .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e)))?; if fd_metadata.file_type().is_block_device() { // Block devices cannot be resized via ftruncate - they are resized // externally (LVM, losetup -c, etc.). Verify the size matches. let (actual_size, _) = query_device_size(&self.file) .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e)))?; if actual_size != size { return Err(BlockError::new( BlockErrorKind::Io, DiskFileError::ResizeError(io::Error::other(format!( "Block device size {actual_size} does not match requested size {size}" ))), )); } Ok(()) } else { self.file .set_len(size) .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e))) } } } impl disk_file::DiskFile for RawFileDisk {} impl disk_file::AsyncDiskFile for RawFileDisk { fn try_clone(&self) -> BlockResult> { let file = self .file .try_clone() .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Clone(e)))?; Ok(Box::new(RawFileDisk { file })) } fn new_async_io(&self, ring_depth: u32) -> BlockResult> { let mut raw = RawFileAsync::new(self.file.as_raw_fd(), ring_depth) .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::NewAsyncIo(e)))?; raw.alignment = DiskTopology::probe(&self.file).map_or(SECTOR_SIZE, |t| t.logical_block_size); Ok(Box::new(raw) as Box) } } pub struct RawFileAsync { fd: RawFd, io_uring: IoUring, eventfd: EventFd, alignment: u64, } impl RawFileAsync { pub fn new(fd: RawFd, ring_depth: u32) -> std::io::Result { let io_uring = IoUring::new(ring_depth)?; let eventfd = EventFd::new(libc::EFD_NONBLOCK)?; // 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())?; Ok(RawFileAsync { fd, io_uring, eventfd, alignment: SECTOR_SIZE, }) } } 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(|_| AsyncIoError::ReadVectored(Error::other("Submission queue is full")))?; }; // 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(|_| AsyncIoError::WriteVectored(Error::other("Submission queue is full")))?; }; // 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) -> 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(|_| AsyncIoError::Fsync(Error::other("Submission queue is full")))?; }; // 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; 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(|_| { AsyncIoError::ReadVectored(Error::other("Submission queue is full")) })?; }; 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(|_| { AsyncIoError::WriteVectored(Error::other("Submission queue is full")) })?; }; 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<()> { 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<()> { 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(()) } }