| //! Driver for VirtIO block devices. |
| |
| use crate::hal::Hal; |
| use crate::queue::VirtQueue; |
| use crate::transport::Transport; |
| use crate::volatile::{volread, Volatile}; |
| use crate::{Error, Result}; |
| use bitflags::bitflags; |
| use log::info; |
| use zerocopy::{AsBytes, FromBytes}; |
| |
| const QUEUE: u16 = 0; |
| const QUEUE_SIZE: u16 = 16; |
| |
| /// Driver for a VirtIO block device. |
| /// |
| /// This is a simple virtual block device, e.g. disk. |
| /// |
| /// Read and write requests (and other exotic requests) are placed in the queue and serviced |
| /// (probably out of order) by the device except where noted. |
| /// |
| /// # Example |
| /// |
| /// ``` |
| /// # use virtio_drivers::{Error, Hal}; |
| /// # use virtio_drivers::transport::Transport; |
| /// use virtio_drivers::device::blk::{VirtIOBlk, SECTOR_SIZE}; |
| /// |
| /// # fn example<HalImpl: Hal, T: Transport>(transport: T) -> Result<(), Error> { |
| /// let mut disk = VirtIOBlk::<HalImpl, _>::new(transport)?; |
| /// |
| /// println!("VirtIO block device: {} kB", disk.capacity() * SECTOR_SIZE as u64 / 2); |
| /// |
| /// // Read sector 0 and then copy it to sector 1. |
| /// let mut buf = [0; SECTOR_SIZE]; |
| /// disk.read_block(0, &mut buf)?; |
| /// disk.write_block(1, &buf)?; |
| /// # Ok(()) |
| /// # } |
| /// ``` |
| pub struct VirtIOBlk<H: Hal, T: Transport> { |
| transport: T, |
| queue: VirtQueue<H, { QUEUE_SIZE as usize }>, |
| capacity: u64, |
| readonly: bool, |
| } |
| |
| impl<H: Hal, T: Transport> VirtIOBlk<H, T> { |
| /// Create a new VirtIO-Blk driver. |
| pub fn new(mut transport: T) -> Result<Self> { |
| let mut readonly = false; |
| |
| transport.begin_init(|features| { |
| let features = BlkFeature::from_bits_truncate(features); |
| info!("device features: {:?}", features); |
| readonly = features.contains(BlkFeature::RO); |
| // negotiate these flags only |
| let supported_features = BlkFeature::empty(); |
| (features & supported_features).bits() |
| }); |
| |
| // read configuration space |
| let config = transport.config_space::<BlkConfig>()?; |
| info!("config: {:?}", config); |
| // Safe because config is a valid pointer to the device configuration space. |
| let capacity = unsafe { |
| volread!(config, capacity_low) as u64 | (volread!(config, capacity_high) as u64) << 32 |
| }; |
| info!("found a block device of size {}KB", capacity / 2); |
| |
| let queue = VirtQueue::new(&mut transport, QUEUE)?; |
| transport.finish_init(); |
| |
| Ok(VirtIOBlk { |
| transport, |
| queue, |
| capacity, |
| readonly, |
| }) |
| } |
| |
| /// Gets the capacity of the block device, in 512 byte ([`SECTOR_SIZE`]) sectors. |
| pub fn capacity(&self) -> u64 { |
| self.capacity |
| } |
| |
| /// Returns true if the block device is read-only, or false if it allows writes. |
| pub fn readonly(&self) -> bool { |
| self.readonly |
| } |
| |
| /// Acknowledges a pending interrupt, if any. |
| /// |
| /// Returns true if there was an interrupt to acknowledge. |
| pub fn ack_interrupt(&mut self) -> bool { |
| self.transport.ack_interrupt() |
| } |
| |
| /// Reads a block into the given buffer. |
| /// |
| /// Blocks until the read completes or there is an error. |
| pub fn read_block(&mut self, block_id: usize, buf: &mut [u8]) -> Result { |
| assert_eq!(buf.len(), SECTOR_SIZE); |
| let req = BlkReq { |
| type_: ReqType::In, |
| reserved: 0, |
| sector: block_id as u64, |
| }; |
| let mut resp = BlkResp::default(); |
| self.queue.add_notify_wait_pop( |
| &[req.as_bytes()], |
| &mut [buf, resp.as_bytes_mut()], |
| &mut self.transport, |
| )?; |
| resp.status.into() |
| } |
| |
| /// Submits a request to read a block, but returns immediately without waiting for the read to |
| /// complete. |
| /// |
| /// # Arguments |
| /// |
| /// * `block_id` - The identifier of the block to read. |
| /// * `req` - A buffer which the driver can use for the request to send to the device. The |
| /// contents don't matter as `read_block_nb` will initialise it, but like the other buffers it |
| /// needs to be valid (and not otherwise used) until the corresponding `complete_read_block` |
| /// call. |
| /// * `buf` - The buffer in memory into which the block should be read. |
| /// * `resp` - A mutable reference to a variable provided by the caller |
| /// to contain the status of the request. The caller can safely |
| /// read the variable only after the request is complete. |
| /// |
| /// # Usage |
| /// |
| /// It will submit request to the VirtIO block device and return a token identifying |
| /// the position of the first Descriptor in the chain. If there are not enough |
| /// Descriptors to allocate, then it returns [`Error::QueueFull`]. |
| /// |
| /// The caller can then call `peek_used` with the returned token to check whether the device has |
| /// finished handling the request. Once it has, the caller must call `complete_read_block` with |
| /// the same buffers before reading the response. |
| /// |
| /// ``` |
| /// # use virtio_drivers::{Error, Hal}; |
| /// # use virtio_drivers::device::blk::VirtIOBlk; |
| /// # use virtio_drivers::transport::Transport; |
| /// use virtio_drivers::device::blk::{BlkReq, BlkResp, RespStatus}; |
| /// |
| /// # fn example<H: Hal, T: Transport>(blk: &mut VirtIOBlk<H, T>) -> Result<(), Error> { |
| /// let mut request = BlkReq::default(); |
| /// let mut buffer = [0; 512]; |
| /// let mut response = BlkResp::default(); |
| /// let token = unsafe { blk.read_block_nb(42, &mut request, &mut buffer, &mut response) }?; |
| /// |
| /// // Wait for an interrupt to tell us that the request completed... |
| /// assert_eq!(blk.peek_used(), Some(token)); |
| /// |
| /// unsafe { |
| /// blk.complete_read_block(token, &request, &mut buffer, &mut response)?; |
| /// } |
| /// if response.status() == RespStatus::OK { |
| /// println!("Successfully read block."); |
| /// } else { |
| /// println!("Error {:?} reading block.", response.status()); |
| /// } |
| /// # Ok(()) |
| /// # } |
| /// ``` |
| /// |
| /// # Safety |
| /// |
| /// `req`, `buf` and `resp` are still borrowed by the underlying VirtIO block device even after |
| /// this method returns. Thus, it is the caller's responsibility to guarantee that they are not |
| /// accessed before the request is completed in order to avoid data races. |
| pub unsafe fn read_block_nb( |
| &mut self, |
| block_id: usize, |
| req: &mut BlkReq, |
| buf: &mut [u8], |
| resp: &mut BlkResp, |
| ) -> Result<u16> { |
| assert_eq!(buf.len(), SECTOR_SIZE); |
| *req = BlkReq { |
| type_: ReqType::In, |
| reserved: 0, |
| sector: block_id as u64, |
| }; |
| let token = self |
| .queue |
| .add(&[req.as_bytes()], &mut [buf, resp.as_bytes_mut()])?; |
| if self.queue.should_notify() { |
| self.transport.notify(QUEUE); |
| } |
| Ok(token) |
| } |
| |
| /// Completes a read operation which was started by `read_block_nb`. |
| /// |
| /// # Safety |
| /// |
| /// The same buffers must be passed in again as were passed to `read_block_nb` when it returned |
| /// the token. |
| pub unsafe fn complete_read_block( |
| &mut self, |
| token: u16, |
| req: &BlkReq, |
| buf: &mut [u8], |
| resp: &mut BlkResp, |
| ) -> Result<()> { |
| self.queue |
| .pop_used(token, &[req.as_bytes()], &mut [buf, resp.as_bytes_mut()])?; |
| resp.status.into() |
| } |
| |
| /// Writes the contents of the given buffer to a block. |
| /// |
| /// Blocks until the write is complete or there is an error. |
| pub fn write_block(&mut self, block_id: usize, buf: &[u8]) -> Result { |
| assert_eq!(buf.len(), SECTOR_SIZE); |
| let req = BlkReq { |
| type_: ReqType::Out, |
| reserved: 0, |
| sector: block_id as u64, |
| }; |
| let mut resp = BlkResp::default(); |
| self.queue.add_notify_wait_pop( |
| &[req.as_bytes(), buf], |
| &mut [resp.as_bytes_mut()], |
| &mut self.transport, |
| )?; |
| resp.status.into() |
| } |
| |
| /// Submits a request to write a block, but returns immediately without waiting for the write to |
| /// complete. |
| /// |
| /// # Arguments |
| /// |
| /// * `block_id` - The identifier of the block to write. |
| /// * `req` - A buffer which the driver can use for the request to send to the device. The |
| /// contents don't matter as `read_block_nb` will initialise it, but like the other buffers it |
| /// needs to be valid (and not otherwise used) until the corresponding `complete_read_block` |
| /// call. |
| /// * `buf` - The buffer in memory containing the data to write to the block. |
| /// * `resp` - A mutable reference to a variable provided by the caller |
| /// to contain the status of the request. The caller can safely |
| /// read the variable only after the request is complete. |
| /// |
| /// # Usage |
| /// |
| /// See [VirtIOBlk::read_block_nb]. |
| /// |
| /// # Safety |
| /// |
| /// See [VirtIOBlk::read_block_nb]. |
| pub unsafe fn write_block_nb( |
| &mut self, |
| block_id: usize, |
| req: &mut BlkReq, |
| buf: &[u8], |
| resp: &mut BlkResp, |
| ) -> Result<u16> { |
| assert_eq!(buf.len(), SECTOR_SIZE); |
| *req = BlkReq { |
| type_: ReqType::Out, |
| reserved: 0, |
| sector: block_id as u64, |
| }; |
| let token = self |
| .queue |
| .add(&[req.as_bytes(), buf], &mut [resp.as_bytes_mut()])?; |
| if self.queue.should_notify() { |
| self.transport.notify(QUEUE); |
| } |
| Ok(token) |
| } |
| |
| /// Completes a write operation which was started by `write_block_nb`. |
| /// |
| /// # Safety |
| /// |
| /// The same buffers must be passed in again as were passed to `write_block_nb` when it returned |
| /// the token. |
| pub unsafe fn complete_write_block( |
| &mut self, |
| token: u16, |
| req: &BlkReq, |
| buf: &[u8], |
| resp: &mut BlkResp, |
| ) -> Result<()> { |
| self.queue |
| .pop_used(token, &[req.as_bytes(), buf], &mut [resp.as_bytes_mut()])?; |
| resp.status.into() |
| } |
| |
| /// Fetches the token of the next completed request from the used ring and returns it, without |
| /// removing it from the used ring. If there are no pending completed requests returns `None`. |
| pub fn peek_used(&mut self) -> Option<u16> { |
| self.queue.peek_used() |
| } |
| |
| /// Returns the size of the device's VirtQueue. |
| /// |
| /// This can be used to tell the caller how many channels to monitor on. |
| pub fn virt_queue_size(&self) -> u16 { |
| QUEUE_SIZE |
| } |
| } |
| |
| impl<H: Hal, T: Transport> Drop for VirtIOBlk<H, T> { |
| fn drop(&mut self) { |
| // Clear any pointers pointing to DMA regions, so the device doesn't try to access them |
| // after they have been freed. |
| self.transport.queue_unset(QUEUE); |
| } |
| } |
| |
| #[repr(C)] |
| struct BlkConfig { |
| /// Number of 512 Bytes sectors |
| capacity_low: Volatile<u32>, |
| capacity_high: Volatile<u32>, |
| size_max: Volatile<u32>, |
| seg_max: Volatile<u32>, |
| cylinders: Volatile<u16>, |
| heads: Volatile<u8>, |
| sectors: Volatile<u8>, |
| blk_size: Volatile<u32>, |
| physical_block_exp: Volatile<u8>, |
| alignment_offset: Volatile<u8>, |
| min_io_size: Volatile<u16>, |
| opt_io_size: Volatile<u32>, |
| // ... ignored |
| } |
| |
| /// A VirtIO block device request. |
| #[repr(C)] |
| #[derive(AsBytes, Debug)] |
| pub struct BlkReq { |
| type_: ReqType, |
| reserved: u32, |
| sector: u64, |
| } |
| |
| impl Default for BlkReq { |
| fn default() -> Self { |
| Self { |
| type_: ReqType::In, |
| reserved: 0, |
| sector: 0, |
| } |
| } |
| } |
| |
| /// Response of a VirtIOBlk request. |
| #[repr(C)] |
| #[derive(AsBytes, Debug, FromBytes)] |
| pub struct BlkResp { |
| status: RespStatus, |
| } |
| |
| impl BlkResp { |
| /// Return the status of a VirtIOBlk request. |
| pub fn status(&self) -> RespStatus { |
| self.status |
| } |
| } |
| |
| #[repr(u32)] |
| #[derive(AsBytes, Debug)] |
| enum ReqType { |
| In = 0, |
| Out = 1, |
| Flush = 4, |
| Discard = 11, |
| WriteZeroes = 13, |
| } |
| |
| /// Status of a VirtIOBlk request. |
| #[repr(transparent)] |
| #[derive(AsBytes, Copy, Clone, Debug, Eq, FromBytes, PartialEq)] |
| pub struct RespStatus(u8); |
| |
| impl RespStatus { |
| /// Ok. |
| pub const OK: RespStatus = RespStatus(0); |
| /// IoErr. |
| pub const IO_ERR: RespStatus = RespStatus(1); |
| /// Unsupported yet. |
| pub const UNSUPPORTED: RespStatus = RespStatus(2); |
| /// Not ready. |
| pub const NOT_READY: RespStatus = RespStatus(3); |
| } |
| |
| impl From<RespStatus> for Result { |
| fn from(status: RespStatus) -> Self { |
| match status { |
| RespStatus::OK => Ok(()), |
| RespStatus::IO_ERR => Err(Error::IoError), |
| RespStatus::UNSUPPORTED => Err(Error::Unsupported), |
| RespStatus::NOT_READY => Err(Error::NotReady), |
| _ => Err(Error::IoError), |
| } |
| } |
| } |
| |
| impl Default for BlkResp { |
| fn default() -> Self { |
| BlkResp { |
| status: RespStatus::NOT_READY, |
| } |
| } |
| } |
| |
| /// The standard sector size of a VirtIO block device. Data is read and written in multiples of this |
| /// size. |
| pub const SECTOR_SIZE: usize = 512; |
| |
| bitflags! { |
| struct BlkFeature: u64 { |
| /// Device supports request barriers. (legacy) |
| const BARRIER = 1 << 0; |
| /// Maximum size of any single segment is in `size_max`. |
| const SIZE_MAX = 1 << 1; |
| /// Maximum number of segments in a request is in `seg_max`. |
| const SEG_MAX = 1 << 2; |
| /// Disk-style geometry specified in geometry. |
| const GEOMETRY = 1 << 4; |
| /// Device is read-only. |
| const RO = 1 << 5; |
| /// Block size of disk is in `blk_size`. |
| const BLK_SIZE = 1 << 6; |
| /// Device supports scsi packet commands. (legacy) |
| const SCSI = 1 << 7; |
| /// Cache flush command support. |
| const FLUSH = 1 << 9; |
| /// Device exports information on optimal I/O alignment. |
| const TOPOLOGY = 1 << 10; |
| /// Device can toggle its cache between writeback and writethrough modes. |
| const CONFIG_WCE = 1 << 11; |
| /// Device can support discard command, maximum discard sectors size in |
| /// `max_discard_sectors` and maximum discard segment number in |
| /// `max_discard_seg`. |
| const DISCARD = 1 << 13; |
| /// Device can support write zeroes command, maximum write zeroes sectors |
| /// size in `max_write_zeroes_sectors` and maximum write zeroes segment |
| /// number in `max_write_zeroes_seg`. |
| const WRITE_ZEROES = 1 << 14; |
| |
| // device independent |
| const NOTIFY_ON_EMPTY = 1 << 24; // legacy |
| const ANY_LAYOUT = 1 << 27; // legacy |
| const RING_INDIRECT_DESC = 1 << 28; |
| const RING_EVENT_IDX = 1 << 29; |
| const UNUSED = 1 << 30; // legacy |
| const VERSION_1 = 1 << 32; // detect legacy |
| |
| // the following since virtio v1.1 |
| const ACCESS_PLATFORM = 1 << 33; |
| const RING_PACKED = 1 << 34; |
| const IN_ORDER = 1 << 35; |
| const ORDER_PLATFORM = 1 << 36; |
| const SR_IOV = 1 << 37; |
| const NOTIFICATION_DATA = 1 << 38; |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use crate::{ |
| hal::fake::FakeHal, |
| transport::{ |
| fake::{FakeTransport, QueueStatus, State}, |
| DeviceStatus, DeviceType, |
| }, |
| }; |
| use alloc::{sync::Arc, vec}; |
| use core::{mem::size_of, ptr::NonNull}; |
| use std::{sync::Mutex, thread, time::Duration}; |
| |
| #[test] |
| fn config() { |
| let mut config_space = BlkConfig { |
| capacity_low: Volatile::new(0x42), |
| capacity_high: Volatile::new(0x02), |
| size_max: Volatile::new(0), |
| seg_max: Volatile::new(0), |
| cylinders: Volatile::new(0), |
| heads: Volatile::new(0), |
| sectors: Volatile::new(0), |
| blk_size: Volatile::new(0), |
| physical_block_exp: Volatile::new(0), |
| alignment_offset: Volatile::new(0), |
| min_io_size: Volatile::new(0), |
| opt_io_size: Volatile::new(0), |
| }; |
| let state = Arc::new(Mutex::new(State { |
| status: DeviceStatus::empty(), |
| driver_features: 0, |
| guest_page_size: 0, |
| interrupt_pending: false, |
| queues: vec![QueueStatus::default(); 1], |
| })); |
| let transport = FakeTransport { |
| device_type: DeviceType::Console, |
| max_queue_size: QUEUE_SIZE.into(), |
| device_features: BlkFeature::RO.bits(), |
| config_space: NonNull::from(&mut config_space), |
| state: state.clone(), |
| }; |
| let blk = VirtIOBlk::<FakeHal, FakeTransport<BlkConfig>>::new(transport).unwrap(); |
| |
| assert_eq!(blk.capacity(), 0x02_0000_0042); |
| assert_eq!(blk.readonly(), true); |
| } |
| |
| #[test] |
| fn read() { |
| let mut config_space = BlkConfig { |
| capacity_low: Volatile::new(66), |
| capacity_high: Volatile::new(0), |
| size_max: Volatile::new(0), |
| seg_max: Volatile::new(0), |
| cylinders: Volatile::new(0), |
| heads: Volatile::new(0), |
| sectors: Volatile::new(0), |
| blk_size: Volatile::new(0), |
| physical_block_exp: Volatile::new(0), |
| alignment_offset: Volatile::new(0), |
| min_io_size: Volatile::new(0), |
| opt_io_size: Volatile::new(0), |
| }; |
| let state = Arc::new(Mutex::new(State { |
| status: DeviceStatus::empty(), |
| driver_features: 0, |
| guest_page_size: 0, |
| interrupt_pending: false, |
| queues: vec![QueueStatus::default(); 1], |
| })); |
| let transport = FakeTransport { |
| device_type: DeviceType::Console, |
| max_queue_size: QUEUE_SIZE.into(), |
| device_features: 0, |
| config_space: NonNull::from(&mut config_space), |
| state: state.clone(), |
| }; |
| let mut blk = VirtIOBlk::<FakeHal, FakeTransport<BlkConfig>>::new(transport).unwrap(); |
| |
| // Start a thread to simulate the device waiting for a read request. |
| let handle = thread::spawn(move || { |
| println!("Device waiting for a request."); |
| while !state.lock().unwrap().queues[usize::from(QUEUE)].notified { |
| thread::sleep(Duration::from_millis(10)); |
| } |
| println!("Transmit queue was notified."); |
| |
| state |
| .lock() |
| .unwrap() |
| .read_write_queue::<{ QUEUE_SIZE as usize }>(QUEUE, |request| { |
| assert_eq!( |
| request, |
| BlkReq { |
| type_: ReqType::In, |
| reserved: 0, |
| sector: 42 |
| } |
| .as_bytes() |
| ); |
| |
| let mut response = vec![0; SECTOR_SIZE]; |
| response[0..9].copy_from_slice(b"Test data"); |
| response.extend_from_slice( |
| BlkResp { |
| status: RespStatus::OK, |
| } |
| .as_bytes(), |
| ); |
| |
| response |
| }); |
| }); |
| |
| // Read a block from the device. |
| let mut buffer = [0; 512]; |
| blk.read_block(42, &mut buffer).unwrap(); |
| assert_eq!(&buffer[0..9], b"Test data"); |
| |
| handle.join().unwrap(); |
| } |
| |
| #[test] |
| fn write() { |
| let mut config_space = BlkConfig { |
| capacity_low: Volatile::new(66), |
| capacity_high: Volatile::new(0), |
| size_max: Volatile::new(0), |
| seg_max: Volatile::new(0), |
| cylinders: Volatile::new(0), |
| heads: Volatile::new(0), |
| sectors: Volatile::new(0), |
| blk_size: Volatile::new(0), |
| physical_block_exp: Volatile::new(0), |
| alignment_offset: Volatile::new(0), |
| min_io_size: Volatile::new(0), |
| opt_io_size: Volatile::new(0), |
| }; |
| let state = Arc::new(Mutex::new(State { |
| status: DeviceStatus::empty(), |
| driver_features: 0, |
| guest_page_size: 0, |
| interrupt_pending: false, |
| queues: vec![QueueStatus::default(); 1], |
| })); |
| let transport = FakeTransport { |
| device_type: DeviceType::Console, |
| max_queue_size: QUEUE_SIZE.into(), |
| device_features: 0, |
| config_space: NonNull::from(&mut config_space), |
| state: state.clone(), |
| }; |
| let mut blk = VirtIOBlk::<FakeHal, FakeTransport<BlkConfig>>::new(transport).unwrap(); |
| |
| // Start a thread to simulate the device waiting for a write request. |
| let handle = thread::spawn(move || { |
| println!("Device waiting for a request."); |
| while !state.lock().unwrap().queues[usize::from(QUEUE)].notified { |
| thread::sleep(Duration::from_millis(10)); |
| } |
| println!("Transmit queue was notified."); |
| |
| state |
| .lock() |
| .unwrap() |
| .read_write_queue::<{ QUEUE_SIZE as usize }>(QUEUE, |request| { |
| assert_eq!( |
| &request[0..size_of::<BlkReq>()], |
| BlkReq { |
| type_: ReqType::Out, |
| reserved: 0, |
| sector: 42 |
| } |
| .as_bytes() |
| ); |
| let data = &request[size_of::<BlkReq>()..]; |
| assert_eq!(data.len(), SECTOR_SIZE); |
| assert_eq!(&data[0..9], b"Test data"); |
| |
| let mut response = Vec::new(); |
| response.extend_from_slice( |
| BlkResp { |
| status: RespStatus::OK, |
| } |
| .as_bytes(), |
| ); |
| |
| response |
| }); |
| }); |
| |
| // Write a block to the device. |
| let mut buffer = [0; 512]; |
| buffer[0..9].copy_from_slice(b"Test data"); |
| blk.write_block(42, &mut buffer).unwrap(); |
| |
| handle.join().unwrap(); |
| } |
| } |