Add Ext2 fs and basic bio layer

This commit is contained in:
LI Qing
2023-09-18 11:47:17 +08:00
committed by Tate, Hongliang Tian
parent 1616f2d32c
commit 9473889c6b
51 changed files with 5346 additions and 427 deletions

View File

@ -8,10 +8,13 @@ edition = "2021"
[dependencies]
bitflags = "1.3"
spin = "0.9.4"
pod = { git = "https://github.com/asterinas/pod", rev = "d7dba56" }
aster-frame = { path = "../../../framework/aster-frame" }
aster-util = { path = "../../libs/aster-util" }
int-to-c-enum = { path = "../../libs/int-to-c-enum" }
component = { path = "../../libs/comp-sys/component" }
log = "0.4"
static_assertions = "1.1.0"
[features]

View File

@ -0,0 +1,475 @@
use crate::prelude::*;
use super::{id::Sid, BlockDevice};
use aster_frame::{
sync::WaitQueue,
vm::{VmFrame, VmReader, VmSegment, VmWriter},
};
use int_to_c_enum::TryFromInt;
/// The unit for block I/O.
///
/// Each `Bio` packs the following information:
/// (1) The type of the I/O,
/// (2) The target sectors on the device for doing I/O,
/// (3) The memory locations (`BioSegment`) from/to which data are read/written,
/// (4) The optional callback function that will be invoked when the I/O is completed.
pub struct Bio(Arc<BioInner>);
impl Bio {
/// Constructs a new `Bio`.
///
/// The `type_` describes the type of the I/O.
/// The `start_sid` is the starting sector id on the device.
/// The `segments` describes the memory segments.
/// The `complete_fn` is the optional callback function.
pub fn new(
type_: BioType,
start_sid: Sid,
segments: Vec<BioSegment>,
complete_fn: Option<fn(&SubmittedBio)>,
) -> Self {
let nsectors = segments
.iter()
.map(|segment| segment.nsectors().to_raw())
.sum();
let inner = Arc::new(BioInner {
type_,
sid_range: start_sid..start_sid + nsectors,
segments,
complete_fn,
status: AtomicU32::new(BioStatus::Init as u32),
wait_queue: WaitQueue::new(),
});
Self(inner)
}
/// Returns the type.
pub fn type_(&self) -> BioType {
self.0.type_()
}
/// Returns the range of target sectors on the device.
pub fn sid_range(&self) -> &Range<Sid> {
self.0.sid_range()
}
/// Returns the slice to the memory segments.
pub fn segments(&self) -> &[BioSegment] {
self.0.segments()
}
/// Returns the status.
pub fn status(&self) -> BioStatus {
self.0.status()
}
/// Submits self to the `block_device` asynchronously.
///
/// Returns a `BioWaiter` to the caller to wait for its completion.
///
/// # Panic
///
/// The caller must not submit a `Bio` more than once. Otherwise, a panic shall be triggered.
pub fn submit(&self, block_device: &dyn BlockDevice) -> Result<BioWaiter, BioEnqueueError> {
// Change the status from "Init" to "Submit".
let result = self.0.status.compare_exchange(
BioStatus::Init as u32,
BioStatus::Submit as u32,
Ordering::Release,
Ordering::Relaxed,
);
assert!(result.is_ok());
if let Err(e) = block_device
.request_queue()
.enqueue(SubmittedBio(self.0.clone()))
{
// Fail to submit, revert the status.
let result = self.0.status.compare_exchange(
BioStatus::Submit as u32,
BioStatus::Init as u32,
Ordering::Release,
Ordering::Relaxed,
);
assert!(result.is_ok());
return Err(e);
}
Ok(BioWaiter {
bios: vec![self.0.clone()],
})
}
/// Submits self to the `block_device` and waits for the result synchronously.
///
/// Returns the result status of the `Bio`.
///
/// # Panic
///
/// The caller must not submit a `Bio` more than once. Otherwise, a panic shall be triggered.
pub fn submit_sync(
&self,
block_device: &dyn BlockDevice,
) -> Result<BioStatus, BioEnqueueError> {
let waiter = self.submit(block_device)?;
match waiter.wait() {
Some(status) => {
assert!(status == BioStatus::Complete);
Ok(status)
}
None => {
let status = self.status();
assert!(status != BioStatus::Complete);
Ok(status)
}
}
}
}
/// The error type returned when enqueueing the `Bio`.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum BioEnqueueError {
/// The request queue is full
IsFull,
/// Refuse to enqueue the bio
Refused,
}
impl From<BioEnqueueError> for aster_frame::Error {
fn from(_error: BioEnqueueError) -> Self {
aster_frame::Error::NotEnoughResources
}
}
/// A waiter for `Bio` submissions.
///
/// This structure holds a list of `Bio` requests and provides functionality to
/// wait for their completion and retrieve their statuses.
#[must_use]
pub struct BioWaiter {
bios: Vec<Arc<BioInner>>,
}
impl BioWaiter {
/// Constructs a new `BioWaiter` instance with no `Bio` requests.
pub fn new() -> Self {
Self { bios: Vec::new() }
}
/// Returns the number of `Bio` requests associated with `self`.
pub fn nreqs(&self) -> usize {
self.bios.len()
}
/// Gets the `index`-th `Bio` request associated with `self`.
///
/// # Panic
///
/// If the `index` is out of bounds, this method will panic.
pub fn req(&self, index: usize) -> Bio {
Bio(self.bios[index].clone())
}
/// Returns the status of the `index`-th `Bio` request associated with `self`.
///
/// # Panic
///
/// If the `index` is out of bounds, this method will panic.
pub fn status(&self, index: usize) -> BioStatus {
self.bios[index].status()
}
/// Merges the `Bio` requests from another `BioWaiter` into this one.
///
/// The another `BioWaiter`'s `Bio` requests are appended to the end of
/// the `Bio` list of `self`, effectively concatenating the two lists.
pub fn concat(&mut self, mut other: Self) {
self.bios.append(&mut other.bios);
}
/// Waits for the completion of all `Bio` requests.
///
/// This method iterates through each `Bio` in the list, waiting for their
/// completion.
///
/// The return value is an option indicating whether all the requests in the list
/// have successfully completed.
/// On success this value is guaranteed to be equal to `Some(BioStatus::Complete)`.
pub fn wait(&self) -> Option<BioStatus> {
let mut ret = Some(BioStatus::Complete);
for bio in self.bios.iter() {
let status = bio.wait_queue.wait_until(|| {
let status = bio.status();
if status != BioStatus::Submit {
Some(status)
} else {
None
}
});
if status != BioStatus::Complete && ret.is_some() {
ret = None;
}
}
ret
}
}
impl Default for BioWaiter {
fn default() -> Self {
Self::new()
}
}
/// A submitted `Bio` object.
///
/// The request queue of block device only accepts a `SubmittedBio` into the queue.
pub struct SubmittedBio(Arc<BioInner>);
impl SubmittedBio {
/// Returns the type.
pub fn type_(&self) -> BioType {
self.0.type_()
}
/// Returns the range of target sectors on the device.
pub fn sid_range(&self) -> &Range<Sid> {
self.0.sid_range()
}
/// Returns the slice to the memory segments.
pub fn segments(&self) -> &[BioSegment] {
self.0.segments()
}
/// Returns the status.
pub fn status(&self) -> BioStatus {
self.0.status()
}
/// Completes the `Bio` with the `status` and invokes the callback function.
///
/// When the driver finishes the request for this `Bio`, it will call this method.
pub fn complete(&self, status: BioStatus) {
assert!(status != BioStatus::Init && status != BioStatus::Submit);
// Set the status.
let result = self.0.status.compare_exchange(
BioStatus::Submit as u32,
status as u32,
Ordering::Release,
Ordering::Relaxed,
);
assert!(result.is_ok());
self.0.wait_queue.wake_all();
if let Some(complete_fn) = self.0.complete_fn {
complete_fn(self);
}
}
}
/// The common inner part of `Bio`.
struct BioInner {
/// The type of the I/O
type_: BioType,
/// The range of the sector id on device
sid_range: Range<Sid>,
/// The memory segments in this `Bio`
segments: Vec<BioSegment>,
/// The I/O completion method
complete_fn: Option<fn(&SubmittedBio)>,
/// The I/O status
status: AtomicU32,
/// The wait queue for I/O completion
wait_queue: WaitQueue,
}
impl BioInner {
pub fn type_(&self) -> BioType {
self.type_
}
pub fn sid_range(&self) -> &Range<Sid> {
&self.sid_range
}
pub fn segments(&self) -> &[BioSegment] {
&self.segments
}
pub fn status(&self) -> BioStatus {
BioStatus::try_from(self.status.load(Ordering::Relaxed)).unwrap()
}
}
/// The type of `Bio`.
#[derive(Clone, Copy, Debug, PartialEq, TryFromInt)]
#[repr(u8)]
pub enum BioType {
/// Read sectors from the device.
Read = 0,
/// Write sectors into the device.
Write = 1,
/// Flush the volatile write cache.
Flush = 2,
/// Discard sectors.
Discard = 3,
}
/// The status of `Bio`.
#[derive(Clone, Copy, PartialEq, Eq, Debug, TryFromInt)]
#[repr(u32)]
pub enum BioStatus {
/// The initial status for a newly created `Bio`.
Init = 0,
/// After a `Bio` is submitted, its status will be changed to "Submit".
Submit = 1,
/// The I/O operation has been successfully completed.
Complete = 2,
/// The I/O operation is not supported.
NotSupported = 3,
/// Insufficient space is available to perform the I/O operation.
NoSpace = 4,
/// An error occurred while doing I/O.
IoError = 5,
}
/// `BioSegment` is a smallest memory unit in block I/O.
///
/// It is a contiguous memory region that contains multiple sectors.
#[derive(Debug, Clone)]
pub struct BioSegment {
/// The contiguous pages on which this segment resides.
pages: Pages,
/// The offset (in bytes) relative to the first page.
offset: AlignedUsize<SECTOR_SIZE>,
// The length (in bytes), may cross pages.
len: AlignedUsize<SECTOR_SIZE>,
}
const SECTOR_SIZE: u16 = super::SECTOR_SIZE as u16;
#[derive(Debug, Clone)]
enum Pages {
Frame(VmFrame),
Segment(VmSegment),
}
impl<'a> BioSegment {
/// Constructs a new `BioSegment` from `VmSegment`.
pub fn from_segment(segment: VmSegment, offset: usize, len: usize) -> Self {
assert!(offset + len <= segment.nbytes());
Self {
pages: Pages::Segment(segment),
offset: AlignedUsize::<SECTOR_SIZE>::new(offset).unwrap(),
len: AlignedUsize::<SECTOR_SIZE>::new(len).unwrap(),
}
}
/// Constructs a new `BioSegment` from `VmFrame`.
pub fn from_frame(frame: VmFrame, offset: usize, len: usize) -> Self {
assert!(offset + len <= super::BLOCK_SIZE);
Self {
pages: Pages::Frame(frame),
offset: AlignedUsize::<SECTOR_SIZE>::new(offset).unwrap(),
len: AlignedUsize::<SECTOR_SIZE>::new(len).unwrap(),
}
}
/// Returns the number of sectors.
pub fn nsectors(&self) -> Sid {
Sid::from_offset(self.len.value())
}
/// Returns the number of bytes.
pub fn nbytes(&self) -> usize {
self.len.value()
}
/// Returns a reader to read data from it.
pub fn reader(&'a self) -> VmReader<'a> {
let reader = match &self.pages {
Pages::Segment(segment) => segment.reader(),
Pages::Frame(frame) => frame.reader(),
};
reader.skip(self.offset.value()).limit(self.len.value())
}
/// Returns a writer to write data into it.
pub fn writer(&'a self) -> VmWriter<'a> {
let writer = match &self.pages {
Pages::Segment(segment) => segment.writer(),
Pages::Frame(frame) => frame.writer(),
};
writer.skip(self.offset.value()).limit(self.len.value())
}
}
/// An aligned unsigned integer number.
///
/// An instance of `AlignedUsize<const N: u16>` is guaranteed to have a value that is a multiple
/// of `N`, a predetermined const value. It is preferable to express an unsigned integer value
/// in type `AlignedUsize<_>` instead of `usize` if the value must satisfy an alignment requirement.
/// This helps readability and prevents bugs.
///
/// # Examples
///
/// ```rust
/// const SECTOR_SIZE: u16 = 512;
///
/// let sector_num = 1234; // The 1234-th sector
/// let sector_offset: AlignedUsize<SECTOR_SIZE> = {
/// let sector_offset = sector_num * (SECTOR_SIZE as usize);
/// AlignedUsize::<SECTOR_SIZE>::new(sector_offset).unwrap()
/// };
/// assert!(sector_offset.value() % sector_offset.align() == 0);
/// ```
///
/// # Limitation
///
/// Currently, the alignment const value must be expressed in `u16`;
/// it is not possible to use a larger or smaller type.
/// This limitation is inherited from that of Rust's const generics:
/// your code can be generic over the _value_ of a const, but not the _type_ of the const.
/// We choose `u16` because it is reasonably large to represent any alignment value
/// used in practice.
#[derive(Debug, Clone)]
pub struct AlignedUsize<const N: u16>(usize);
impl<const N: u16> AlignedUsize<N> {
/// Constructs a new instance of aligned integer if the given value is aligned.
pub fn new(val: usize) -> Option<Self> {
if val % (N as usize) == 0 {
Some(Self(val))
} else {
None
}
}
/// Returns the value.
pub fn value(&self) -> usize {
self.0
}
/// Returns the corresponding ID.
///
/// The so-called "ID" of an aligned integer is defined to be `self.value() / self.align()`.
/// This value is named ID because one common use case is using `Aligned` to express
/// the byte offset of a sector, block, or page. In this case, the `id` method returns
/// the ID of the corresponding sector, block, or page.
pub fn id(&self) -> usize {
self.value() / self.align()
}
/// Returns the alignment.
pub fn align(&self) -> usize {
N as usize
}
}

View File

@ -0,0 +1,100 @@
use core::{
iter::Step,
ops::{Add, Sub},
};
use pod::Pod;
use static_assertions::const_assert;
/// The block index used in the filesystem.
pub type Bid = BlockId<BLOCK_SIZE>;
/// The sector index used in the device.
pub type Sid = BlockId<SECTOR_SIZE>;
impl From<Bid> for Sid {
fn from(bid: Bid) -> Self {
Self::new(bid.to_raw() * (BLOCK_SIZE / SECTOR_SIZE) as u64)
}
}
const BLOCK_SIZE: u16 = super::BLOCK_SIZE as u16;
const SECTOR_SIZE: u16 = super::SECTOR_SIZE as u16;
const_assert!(BLOCK_SIZE / SECTOR_SIZE >= 1);
/// An index of a block.
///
/// The `BlockId<const N: u16>` is a generic type that is parameterized by a constant `N`, which
/// represents the size of each block in bytes. The `BlockId<_>` provides a type-safe way of handling
/// block indices.
/// An Instance of `BlockId<_>` is guaranteed to represent valid block index, derived from byte offset
/// and the specified block size `N`.
///
/// # Examples
///
/// ```rust
/// const BLOCK_SIZE: u16 = 512;
///
/// let bytes_offset = 2048;
/// let block_id = BlockId<BLOCK_SIZE>::from_offset(bytes_offset);
/// assert!(block_id == (bytes_offset / BLOCK_SIZE));
/// ```
///
/// # Limitation
///
/// Currently, the block size is expressed in `u16`. We choose `u16` because
/// it is reasonably large to represent the common block size used in practice.
#[repr(C)]
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Pod)]
pub struct BlockId<const N: u16>(u64);
impl<const N: u16> BlockId<N> {
/// Constructs an id from a raw id.
pub const fn new(raw_id: u64) -> Self {
Self(raw_id)
}
/// Constructs an id from a byte offset.
pub const fn from_offset(offset: usize) -> Self {
Self((offset / (N as usize)) as _)
}
/// Converts to a byte offset.
pub fn to_offset(self) -> usize {
(self.0 as usize) * (N as usize)
}
/// Converts to raw id.
pub fn to_raw(self) -> u64 {
self.0
}
}
impl<const N: u16> Add<u64> for BlockId<N> {
type Output = Self;
fn add(self, other: u64) -> Self::Output {
Self(self.0 + other)
}
}
impl<const N: u16> Sub<u64> for BlockId<N> {
type Output = Self;
fn sub(self, other: u64) -> Self::Output {
Self(self.0 - other)
}
}
/// Implements the `Step` trait to iterate over `Range<Id>`.
impl<const N: u16> Step for BlockId<N> {
fn steps_between(start: &Self, end: &Self) -> Option<usize> {
u64::steps_between(&start.0, &end.0)
}
fn forward_checked(start: Self, count: usize) -> Option<Self> {
u64::forward_checked(start.0, count).map(Self::new)
}
fn backward_checked(start: Self, count: usize) -> Option<Self> {
u64::backward_checked(start.0, count).map(Self::new)
}
}

View File

@ -0,0 +1,237 @@
use crate::prelude::*;
use super::{
bio::{Bio, BioEnqueueError, BioSegment, BioStatus, BioType, BioWaiter, SubmittedBio},
id::{Bid, Sid},
BlockDevice, BLOCK_SIZE, SECTOR_SIZE,
};
use aster_frame::vm::{VmAllocOptions, VmFrame, VmIo, VmSegment};
/// Implements several commonly used APIs for the block device to conveniently
/// read and write block(s).
impl dyn BlockDevice {
/// Synchronously reads contiguous blocks starting from the `bid`.
pub fn read_blocks_sync(
&self,
bid: Bid,
segment: &VmSegment,
) -> Result<BioStatus, BioEnqueueError> {
let bio = create_bio_from_segment(BioType::Read, bid, segment);
let status = bio.submit_sync(self)?;
Ok(status)
}
/// Asynchronously reads contiguous blocks starting from the `bid`.
pub fn read_blocks(&self, bid: Bid, segment: &VmSegment) -> Result<BioWaiter, BioEnqueueError> {
let bio = create_bio_from_segment(BioType::Read, bid, segment);
bio.submit(self)
}
/// Synchronously reads one block indicated by the `bid`.
pub fn read_block_sync(&self, bid: Bid, frame: &VmFrame) -> Result<BioStatus, BioEnqueueError> {
let bio = create_bio_from_frame(BioType::Read, bid, frame);
let status = bio.submit_sync(self)?;
Ok(status)
}
/// Asynchronously reads one block indicated by the `bid`.
pub fn read_block(&self, bid: Bid, frame: &VmFrame) -> Result<BioWaiter, BioEnqueueError> {
let bio = create_bio_from_frame(BioType::Read, bid, frame);
bio.submit(self)
}
/// Synchronously writes contiguous blocks starting from the `bid`.
pub fn write_blocks_sync(
&self,
bid: Bid,
segment: &VmSegment,
) -> Result<BioStatus, BioEnqueueError> {
let bio = create_bio_from_segment(BioType::Write, bid, segment);
let status = bio.submit_sync(self)?;
Ok(status)
}
/// Asynchronously writes contiguous blocks starting from the `bid`.
pub fn write_blocks(
&self,
bid: Bid,
segment: &VmSegment,
) -> Result<BioWaiter, BioEnqueueError> {
let bio = create_bio_from_segment(BioType::Write, bid, segment);
bio.submit(self)
}
/// Synchronously writes one block indicated by the `bid`.
pub fn write_block_sync(
&self,
bid: Bid,
frame: &VmFrame,
) -> Result<BioStatus, BioEnqueueError> {
let bio = create_bio_from_frame(BioType::Write, bid, frame);
let status = bio.submit_sync(self)?;
Ok(status)
}
/// Asynchronously writes one block indicated by the `bid`.
pub fn write_block(&self, bid: Bid, frame: &VmFrame) -> Result<BioWaiter, BioEnqueueError> {
let bio = create_bio_from_frame(BioType::Write, bid, frame);
bio.submit(self)
}
}
impl VmIo for dyn BlockDevice {
/// Reads consecutive bytes of several sectors in size.
fn read_bytes(&self, offset: usize, buf: &mut [u8]) -> aster_frame::Result<()> {
if offset % SECTOR_SIZE != 0 || buf.len() % SECTOR_SIZE != 0 {
return Err(aster_frame::Error::InvalidArgs);
}
if buf.is_empty() {
return Ok(());
}
let (bio, bio_segment) = {
let num_blocks = {
let first = Bid::from_offset(offset).to_raw();
let last = Bid::from_offset(offset + buf.len() - 1).to_raw();
last - first + 1
};
let segment = VmAllocOptions::new(num_blocks as usize)
.uninit(true)
.is_contiguous(true)
.alloc_contiguous()?;
let bio_segment = BioSegment::from_segment(segment, offset % BLOCK_SIZE, buf.len());
(
Bio::new(
BioType::Read,
Sid::from_offset(offset),
vec![bio_segment.clone()],
None,
),
bio_segment,
)
};
let status = bio.submit_sync(self)?;
match status {
BioStatus::Complete => {
let _ = bio_segment.reader().read(&mut buf.into());
Ok(())
}
_ => Err(aster_frame::Error::IoError),
}
}
/// Writes consecutive bytes of several sectors in size.
fn write_bytes(&self, offset: usize, buf: &[u8]) -> aster_frame::Result<()> {
if offset % SECTOR_SIZE != 0 || buf.len() % SECTOR_SIZE != 0 {
return Err(aster_frame::Error::InvalidArgs);
}
if buf.is_empty() {
return Ok(());
}
let bio = {
let num_blocks = {
let first = Bid::from_offset(offset).to_raw();
let last = Bid::from_offset(offset + buf.len() - 1).to_raw();
last - first + 1
};
let segment = VmAllocOptions::new(num_blocks as usize)
.uninit(true)
.is_contiguous(true)
.alloc_contiguous()?;
segment.write_bytes(offset % BLOCK_SIZE, buf)?;
let len = segment
.writer()
.skip(offset % BLOCK_SIZE)
.write(&mut buf.into());
let bio_segment = BioSegment::from_segment(segment, offset % BLOCK_SIZE, len);
Bio::new(
BioType::Write,
Sid::from_offset(offset),
vec![bio_segment],
None,
)
};
let status = bio.submit_sync(self)?;
match status {
BioStatus::Complete => Ok(()),
_ => Err(aster_frame::Error::IoError),
}
}
}
impl dyn BlockDevice {
/// Asynchronously writes consecutive bytes of several sectors in size.
pub fn write_bytes_async(&self, offset: usize, buf: &[u8]) -> aster_frame::Result<BioWaiter> {
if offset % SECTOR_SIZE != 0 || buf.len() % SECTOR_SIZE != 0 {
return Err(aster_frame::Error::InvalidArgs);
}
if buf.is_empty() {
return Ok(BioWaiter::new());
}
let bio = {
let num_blocks = {
let first = Bid::from_offset(offset).to_raw();
let last = Bid::from_offset(offset + buf.len() - 1).to_raw();
last - first + 1
};
let segment = VmAllocOptions::new(num_blocks as usize)
.uninit(true)
.is_contiguous(true)
.alloc_contiguous()?;
segment.write_bytes(offset % BLOCK_SIZE, buf)?;
let len = segment
.writer()
.skip(offset % BLOCK_SIZE)
.write(&mut buf.into());
let bio_segment = BioSegment::from_segment(segment, offset % BLOCK_SIZE, len);
Bio::new(
BioType::Write,
Sid::from_offset(offset),
vec![bio_segment],
Some(general_complete_fn),
)
};
let complete = bio.submit(self)?;
Ok(complete)
}
}
// TODO: Maybe we should have a builder for `Bio`.
fn create_bio_from_segment(type_: BioType, bid: Bid, segment: &VmSegment) -> Bio {
let bio_segment = BioSegment::from_segment(segment.clone(), 0, segment.nbytes());
Bio::new(
type_,
Sid::from(bid),
vec![bio_segment],
Some(general_complete_fn),
)
}
// TODO: Maybe we should have a builder for `Bio`.
fn create_bio_from_frame(type_: BioType, bid: Bid, frame: &VmFrame) -> Bio {
let bio_segment = BioSegment::from_frame(frame.clone(), 0, BLOCK_SIZE);
Bio::new(
type_,
Sid::from(bid),
vec![bio_segment],
Some(general_complete_fn),
)
}
fn general_complete_fn(bio: &SubmittedBio) {
match bio.status() {
BioStatus::Complete => (),
err_status => log::error!(
"faild to do {:?} on the device with error status: {:?}",
bio.type_(),
err_status
),
}
}

View File

@ -1,32 +1,67 @@
//! The block devices of Asterinas.
//
//This crate provides a number of base components for block devices, including
//! an abstraction of block devices, as well as the registration and lookup of block devices.
//!
//! Block devices use a queue-based model for asynchronous I/O operations. It is necessary
//! for a block device to maintain a queue to handle I/O requests. The users (e.g., fs)
//! submit I/O requests to this queue and wait for their completion. Drivers implementing
//! block devices can create their own queues as needed, with the possibility to reorder
//! and merge requests within the queue.
//!
//! This crate also offers the `Bio` related data structures and APIs to accomplish
//! safe and convenient block I/O operations, for exmaple:
//!
//! ```no_run
//! // Creates a bio request.
//! let bio = Bio::new(BioType::Write, sid, segments, None);
//! // Submits to the block device.
//! let bio_waiter = bio.submit(block_device)?;
//! // Waits for the the completion.
//! let Some(status) = bio_waiter.wait() else {
//! return Err(IoError);
//! };
//! assert!(status == BioStatus::Complete);
//! ```
//!
#![no_std]
#![forbid(unsafe_code)]
#![feature(fn_traits)]
#![feature(step_trait)]
#![feature(trait_upcasting)]
#![allow(dead_code)]
extern crate alloc;
use core::any::Any;
use core::fmt::Debug;
pub mod bio;
pub mod id;
mod impl_block_device;
mod prelude;
pub mod request_queue;
use self::{prelude::*, request_queue::BioRequestQueue};
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use aster_frame::sync::SpinLock;
use aster_frame::vm::VmReader;
use aster_frame::vm::VmWriter;
use component::init_component;
use component::ComponentInitError;
use spin::Once;
pub const BLK_SIZE: usize = 512;
pub const BLOCK_SIZE: usize = aster_frame::config::PAGE_SIZE;
pub const SECTOR_SIZE: usize = 512;
pub trait BlockDevice: Send + Sync + Any + Debug {
fn read_block(&self, block_id: usize, buf: &[VmWriter]);
fn write_block(&self, block_id: usize, buf: &[VmReader]);
/// Returns this block device's request queue, to which block I/O requests may be submitted.
fn request_queue(&self) -> &dyn BioRequestQueue;
fn handle_irq(&self);
}
impl dyn BlockDevice {
pub fn downcast_ref<T: BlockDevice>(&self) -> Option<&T> {
(self as &dyn Any).downcast_ref::<T>()
}
}
pub fn register_device(name: String, device: Arc<dyn BlockDevice>) {
COMPONENT
.get()

View File

@ -0,0 +1,9 @@
pub(crate) use alloc::collections::{BTreeMap, VecDeque};
pub(crate) use alloc::string::String;
pub(crate) use alloc::sync::Arc;
pub(crate) use alloc::vec;
pub(crate) use alloc::vec::Vec;
pub(crate) use core::any::Any;
pub(crate) use core::fmt::Debug;
pub(crate) use core::ops::Range;
pub(crate) use core::sync::atomic::{AtomicU32, Ordering};

View File

@ -0,0 +1,93 @@
use crate::prelude::*;
use super::{
bio::{BioEnqueueError, BioType, SubmittedBio},
id::Sid,
};
/// Represents the software staging queue for the `BioRequest` objects.
pub trait BioRequestQueue {
/// Enqueues a `SubmittedBio` to this queue.
///
/// This `SubmittedBio` will be merged into an existing `BioRequest`, or a new
/// `BioRequest` will be created from the `SubmittedBio` before being placed
/// into the queue.
///
/// This method will wake up the waiter if a new `BioRequest` is enqueued.
fn enqueue(&self, bio: SubmittedBio) -> Result<(), BioEnqueueError>;
/// Dequeues a `BioRequest` from this queue.
///
/// This method will wait until one request can be retrieved.
fn dequeue(&self) -> BioRequest;
}
/// The block I/O request.
pub struct BioRequest {
/// The type of the I/O
type_: BioType,
/// The range of target sectors on the device
sid_range: Range<Sid>,
/// The submitted bios
bios: VecDeque<SubmittedBio>,
}
impl BioRequest {
/// Returns the type of the I/O.
pub fn type_(&self) -> BioType {
self.type_
}
/// Returns the range of sector id on device.
pub fn sid_range(&self) -> &Range<Sid> {
&self.sid_range
}
/// Returns an iterator to the `SubmittedBio`s.
pub fn bios(&self) -> impl Iterator<Item = &SubmittedBio> {
self.bios.iter()
}
/// Returns `true` if can merge the `SubmittedBio`, `false` otherwise.
pub fn can_merge(&self, rq_bio: &SubmittedBio) -> bool {
if rq_bio.type_() != self.type_ {
return false;
}
rq_bio.sid_range().start == self.sid_range.end
|| rq_bio.sid_range().end == self.sid_range.start
}
/// Merges the `SubmittedBio` into this request.
///
/// The merged `SubmittedBio` can only be placed at the front or back.
///
/// # Panic
///
/// If the `SubmittedBio` can not be merged, this method will panic.
pub fn merge_bio(&mut self, rq_bio: SubmittedBio) {
assert!(self.can_merge(&rq_bio));
if rq_bio.sid_range().start == self.sid_range.end {
self.sid_range.end = rq_bio.sid_range().end;
self.bios.push_back(rq_bio);
} else {
self.sid_range.start = rq_bio.sid_range().start;
self.bios.push_front(rq_bio);
}
}
}
impl From<SubmittedBio> for BioRequest {
fn from(bio: SubmittedBio) -> Self {
Self {
type_: bio.type_(),
sid_range: bio.sid_range().clone(),
bios: {
let mut bios = VecDeque::with_capacity(1);
bios.push_front(bio);
bios
},
}
}
}

View File

@ -1,28 +1,132 @@
use core::{hint::spin_loop, mem::size_of};
use core::{
fmt::Debug,
hint::spin_loop,
mem::size_of,
sync::atomic::{AtomicUsize, Ordering},
};
use alloc::{boxed::Box, string::ToString, sync::Arc};
use alloc::{boxed::Box, string::ToString, sync::Arc, vec::Vec};
use alloc::{boxed::Box, collections::VecDeque, string::ToString, sync::Arc, vec::Vec};
use aster_block::{
bio::{BioEnqueueError, BioStatus, BioType, SubmittedBio},
id::Sid,
request_queue::{BioRequest, BioRequestQueue},
};
use aster_frame::{
io_mem::IoMem,
sync::SpinLock,
sync::{Mutex, WaitQueue},
trap::TrapFrame,
vm::{VmAllocOptions, VmFrame, VmIo, VmReader, VmWriter},
};
use aster_util::safe_ptr::SafePtr;
use log::info;
use pod::Pod;
use crate::{
device::block::{BlkReq, BlkResp, ReqType, RespStatus},
device::block::{ReqType, RespStatus},
device::VirtioDeviceError,
queue::VirtQueue,
transport::VirtioTransport,
};
use super::{BlkFeatures, VirtioBlkConfig};
use super::{BlockFeatures, VirtioBlockConfig};
#[derive(Debug)]
pub struct BlockDevice {
config: SafePtr<VirtioBlkConfig, IoMem>,
device: DeviceInner,
/// The software staging queue.
queue: BioSingleQueue,
}
impl BlockDevice {
/// Creates a new VirtIO-Block driver and registers it.
pub(crate) fn init(transport: Box<dyn VirtioTransport>) -> Result<(), VirtioDeviceError> {
let block_device = {
let device = DeviceInner::init(transport)?;
Self {
device,
queue: BioSingleQueue::new(),
}
};
aster_block::register_device(super::DEVICE_NAME.to_string(), Arc::new(block_device));
Ok(())
}
/// Dequeues a `BioRequest` from the software staging queue and
/// processes the request.
///
/// TODO: Current read and write operations are still synchronous
/// it needs to be modified to use the queue-based asynchronous programming pattern.
pub fn handle_requests(&self) {
let request = self.queue.dequeue();
match request.type_() {
BioType::Read => self.do_read(&request),
BioType::Write => self.do_write(&request),
BioType::Flush | BioType::Discard => todo!(),
}
}
fn do_read(&self, request: &BioRequest) {
let start_sid = request.sid_range().start;
let writers = {
let mut writers = Vec::new();
for bio in request.bios() {
for segment in bio.segments() {
writers.push(segment.writer());
}
}
writers
};
self.device.read(start_sid, writers.as_slice());
for bio in request.bios() {
bio.complete(BioStatus::Complete);
}
}
fn do_write(&self, request: &BioRequest) {
let start_sid = request.sid_range().start;
let readers = {
let mut readers = Vec::new();
for bio in request.bios() {
for segment in bio.segments() {
readers.push(segment.reader());
}
}
readers
};
self.device.write(start_sid, readers.as_slice());
for bio in request.bios() {
bio.complete(BioStatus::Complete);
}
}
/// Negotiate features for the device specified bits 0~23
pub(crate) fn negotiate_features(features: u64) -> u64 {
let feature = BlockFeatures::from_bits(features).unwrap();
let support_features = BlockFeatures::from_bits(features).unwrap();
(feature & support_features).bits
}
}
impl aster_block::BlockDevice for BlockDevice {
fn request_queue(&self) -> &dyn BioRequestQueue {
&self.queue
}
fn handle_irq(&self) {
info!("Virtio block device handle irq");
}
}
#[derive(Debug)]
struct DeviceInner {
config: SafePtr<VirtioBlockConfig, IoMem>,
queue: SpinLock<VirtQueue>,
transport: Box<dyn VirtioTransport>,
/// Block requests, we use VmFrame to store the requests so that
@ -34,109 +138,10 @@ pub struct BlockDevice {
id_allocator: SpinLock<Vec<u8>>,
}
impl BlockDevice {
/// read data from block device, this function is blocking
/// FIEME: replace slice with a more secure data structure to use dma mapping.
pub fn read(&self, block_id: usize, buf: &[VmWriter]) {
// FIXME: Handling cases without id.
let id = self.id_allocator.lock().pop().unwrap() as usize;
let req = BlkReq {
type_: ReqType::In as _,
reserved: 0,
sector: block_id as u64,
};
let resp = BlkResp::default();
self.block_requests
.write_val(id * size_of::<BlkReq>(), &req)
.unwrap();
self.block_responses
.write_val(id * size_of::<BlkResp>(), &resp)
.unwrap();
let req = self
.block_requests
.reader()
.skip(id * size_of::<BlkReq>())
.limit(size_of::<BlkReq>());
let resp = self
.block_responses
.writer()
.skip(id * size_of::<BlkResp>())
.limit(size_of::<BlkResp>());
let mut outputs: Vec<&VmWriter<'_>> = buf.iter().collect();
outputs.push(&resp);
let mut queue = self.queue.lock_irq_disabled();
let token = queue
.add_vm(&[&req], outputs.as_slice())
.expect("add queue failed");
queue.notify();
while !queue.can_pop() {
spin_loop();
}
queue.pop_used_with_token(token).expect("pop used failed");
let resp: BlkResp = self
.block_responses
.read_val(id * size_of::<BlkResp>())
.unwrap();
self.id_allocator.lock().push(id as u8);
match RespStatus::try_from(resp.status).unwrap() {
RespStatus::Ok => {}
_ => panic!("io error in block device"),
};
}
/// write data to block device, this function is blocking
/// FIEME: replace slice with a more secure data structure to use dma mapping.
pub fn write(&self, block_id: usize, buf: &[VmReader]) {
// FIXME: Handling cases without id.
let id = self.id_allocator.lock().pop().unwrap() as usize;
let req = BlkReq {
type_: ReqType::Out as _,
reserved: 0,
sector: block_id as u64,
};
let resp = BlkResp::default();
self.block_requests
.write_val(id * size_of::<BlkReq>(), &req)
.unwrap();
self.block_responses
.write_val(id * size_of::<BlkResp>(), &resp)
.unwrap();
let req = self
.block_requests
.reader()
.skip(id * size_of::<BlkReq>())
.limit(size_of::<BlkReq>());
let resp = self
.block_responses
.writer()
.skip(id * size_of::<BlkResp>())
.limit(size_of::<BlkResp>());
let mut queue = self.queue.lock_irq_disabled();
let mut inputs: Vec<&VmReader<'_>> = buf.iter().collect();
inputs.insert(0, &req);
let token = queue
.add_vm(inputs.as_slice(), &[&resp])
.expect("add queue failed");
queue.notify();
while !queue.can_pop() {
spin_loop();
}
queue.pop_used_with_token(token).expect("pop used failed");
let resp: BlkResp = self
.block_responses
.read_val(id * size_of::<BlkResp>())
.unwrap();
self.id_allocator.lock().push(id as u8);
match RespStatus::try_from(resp.status).unwrap() {
RespStatus::Ok => {}
_ => panic!("io error in block device:{:?}", resp.status),
};
}
/// Create a new VirtIO-Block driver.
pub(crate) fn init(mut transport: Box<dyn VirtioTransport>) -> Result<(), VirtioDeviceError> {
let config = VirtioBlkConfig::new(transport.as_mut());
impl DeviceInner {
/// Creates and inits the device.
pub fn init(mut transport: Box<dyn VirtioTransport>) -> Result<Self, VirtioDeviceError> {
let config = VirtioBlockConfig::new(transport.as_mut());
let num_queues = transport.num_queues();
if num_queues != 1 {
return Err(VirtioDeviceError::QueuesAmountDoNotMatch(num_queues, 1));
@ -170,30 +175,225 @@ impl BlockDevice {
info!("Virtio block device config space change");
}
device.transport.finish_init();
Ok(device)
}
aster_block::register_device(super::DEVICE_NAME.to_string(), Arc::new(device));
/// Reads data from the block device, this function is blocking.
/// FIEME: replace slice with a more secure data structure to use dma mapping.
pub fn read(&self, sector_id: Sid, buf: &[VmWriter]) {
// FIXME: Handling cases without id.
let id = self.id_allocator.lock().pop().unwrap() as usize;
let req = BlockReq {
type_: ReqType::In as _,
reserved: 0,
sector: sector_id.to_raw(),
};
let resp = BlockResp::default();
self.block_requests
.write_val(id * size_of::<BlockReq>(), &req)
.unwrap();
self.block_responses
.write_val(id * size_of::<BlockResp>(), &resp)
.unwrap();
let req_reader = self
.block_requests
.reader()
.skip(id * size_of::<BlockReq>())
.limit(size_of::<BlockReq>());
let resp_writer = self
.block_responses
.writer()
.skip(id * size_of::<BlockResp>())
.limit(size_of::<BlockResp>());
let mut outputs: Vec<&VmWriter<'_>> = buf.iter().collect();
outputs.push(&resp_writer);
let mut queue = self.queue.lock_irq_disabled();
let token = queue
.add_vm(&[&req_reader], outputs.as_slice())
.expect("add queue failed");
queue.notify();
while !queue.can_pop() {
spin_loop();
}
queue.pop_used_with_token(token).expect("pop used failed");
let resp: BlockResp = self
.block_responses
.read_val(id * size_of::<BlockResp>())
.unwrap();
self.id_allocator.lock().push(id as u8);
match RespStatus::try_from(resp.status).unwrap() {
RespStatus::Ok => {}
_ => panic!("io error in block device"),
};
}
/// Writes data to the block device, this function is blocking.
/// FIEME: replace slice with a more secure data structure to use dma mapping.
pub fn write(&self, sector_id: Sid, buf: &[VmReader]) {
// FIXME: Handling cases without id.
let id = self.id_allocator.lock().pop().unwrap() as usize;
let req = BlockReq {
type_: ReqType::Out as _,
reserved: 0,
sector: sector_id.to_raw(),
};
let resp = BlockResp::default();
self.block_requests
.write_val(id * size_of::<BlockReq>(), &req)
.unwrap();
self.block_responses
.write_val(id * size_of::<BlockResp>(), &resp)
.unwrap();
let req_reader = self
.block_requests
.reader()
.skip(id * size_of::<BlockReq>())
.limit(size_of::<BlockReq>());
let resp_writer = self
.block_responses
.writer()
.skip(id * size_of::<BlockResp>())
.limit(size_of::<BlockResp>());
let mut queue = self.queue.lock_irq_disabled();
let mut inputs: Vec<&VmReader<'_>> = buf.iter().collect();
inputs.insert(0, &req_reader);
let token = queue
.add_vm(inputs.as_slice(), &[&resp_writer])
.expect("add queue failed");
queue.notify();
while !queue.can_pop() {
spin_loop();
}
queue.pop_used_with_token(token).expect("pop used failed");
let resp: BlockResp = self
.block_responses
.read_val(id * size_of::<BlockResp>())
.unwrap();
self.id_allocator.lock().push(id as u8);
match RespStatus::try_from(resp.status).unwrap() {
RespStatus::Ok => {}
_ => panic!("io error in block device:{:?}", resp.status),
};
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod)]
struct BlockReq {
pub type_: u32,
pub reserved: u32,
pub sector: u64,
}
/// Response of a VirtIOBlock request.
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod)]
struct BlockResp {
pub status: u8,
}
impl Default for BlockResp {
fn default() -> Self {
Self {
status: RespStatus::_NotReady as _,
}
}
}
/// A simple block I/O request queue with a single queue.
///
/// It is a FIFO producer-consumer queue, where the producer (e.g., filesystem)
/// submits requests to the queue, and the consumer (e.g., block device driver)
/// continuously consumes and processes these requests from the queue.
pub struct BioSingleQueue {
queue: Mutex<VecDeque<BioRequest>>,
num_requests: AtomicUsize,
wait_queue: WaitQueue,
}
impl BioSingleQueue {
/// Creates an empty queue.
pub fn new() -> Self {
Self {
queue: Mutex::new(VecDeque::new()),
num_requests: AtomicUsize::new(0),
wait_queue: WaitQueue::new(),
}
}
/// Returns the number of requests currently in this queue.
pub fn num_requests(&self) -> usize {
self.num_requests.load(Ordering::Relaxed)
}
fn dec_num_requests(&self) {
self.num_requests.fetch_sub(1, Ordering::Relaxed);
}
fn inc_num_requests(&self) {
self.num_requests.fetch_add(1, Ordering::Relaxed);
}
}
impl Default for BioSingleQueue {
fn default() -> Self {
Self::new()
}
}
impl Debug for BioSingleQueue {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_struct("BioSingleQueue")
.field("num_requests", &self.num_requests())
.finish()
}
}
impl BioRequestQueue for BioSingleQueue {
/// Enqueues a `SubmittedBio` to this queue.
///
/// When enqueueing the `SubmittedBio`, try to insert it into the last request if the
/// type is same and the sector range is contiguous.
/// Otherwise, creates and inserts a new request for the `SubmittedBio`.
fn enqueue(&self, bio: SubmittedBio) -> Result<(), BioEnqueueError> {
let mut queue = self.queue.lock();
if let Some(request) = queue.front_mut() {
if request.can_merge(&bio) {
request.merge_bio(bio);
return Ok(());
}
}
let new_request = BioRequest::from(bio);
queue.push_front(new_request);
drop(queue);
self.inc_num_requests();
self.wait_queue.wake_all();
Ok(())
}
/// Negotiate features for the device specified bits 0~23
pub(crate) fn negotiate_features(features: u64) -> u64 {
let feature = BlkFeatures::from_bits(features).unwrap();
let support_features = BlkFeatures::from_bits(features).unwrap();
(feature & support_features).bits
}
}
impl aster_block::BlockDevice for BlockDevice {
fn read_block(&self, block_id: usize, buf: &[VmWriter]) {
self.read(block_id, buf);
}
fn write_block(&self, block_id: usize, buf: &[VmReader]) {
self.write(block_id, buf);
}
fn handle_irq(&self) {
info!("Virtio block device handle irq");
/// Dequeues a `BioRequest` from this queue.
fn dequeue(&self) -> BioRequest {
let mut num_requests = self.num_requests();
loop {
if num_requests > 0 {
if let Some(request) = self.queue.lock().pop_back() {
self.dec_num_requests();
return request;
}
}
num_requests = self.wait_queue.wait_until(|| {
let num_requests = self.num_requests();
if num_requests > 0 {
Some(num_requests)
} else {
None
}
});
}
}
}

View File

@ -8,12 +8,11 @@ use pod::Pod;
use crate::transport::VirtioTransport;
pub const BLK_SIZE: usize = 512;
pub static DEVICE_NAME: &str = "Virtio-Block";
bitflags! {
/// features for virtio block device
pub(crate) struct BlkFeatures : u64{
pub(crate) struct BlockFeatures : u64 {
const BARRIER = 1 << 0;
const SIZE_MAX = 1 << 1;
const SEG_MAX = 1 << 2;
@ -29,29 +28,6 @@ bitflags! {
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod)]
pub struct BlkReq {
pub type_: u32,
pub reserved: u32,
pub sector: u64,
}
/// Response of a VirtIOBlk request.
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod)]
pub struct BlkResp {
pub status: u8,
}
impl Default for BlkResp {
fn default() -> Self {
BlkResp {
status: RespStatus::_NotReady as _,
}
}
}
#[repr(u32)]
#[derive(Debug, Copy, Clone, TryFromInt)]
pub enum ReqType {
@ -77,12 +53,12 @@ pub enum RespStatus {
#[derive(Debug, Copy, Clone, Pod)]
#[repr(C)]
pub struct VirtioBlkConfig {
pub struct VirtioBlockConfig {
capacity: u64,
size_max: u64,
geometry: VirtioBlkGeometry,
geometry: VirtioBlockGeometry,
blk_size: u32,
topology: VirtioBlkTopology,
topology: VirtioBlockTopology,
writeback: u8,
unused0: [u8; 3],
max_discard_sectors: u32,
@ -96,7 +72,7 @@ pub struct VirtioBlkConfig {
#[derive(Debug, Copy, Clone, Pod)]
#[repr(C)]
pub struct VirtioBlkGeometry {
pub struct VirtioBlockGeometry {
cylinders: u16,
heads: u8,
sectors: u8,
@ -104,14 +80,14 @@ pub struct VirtioBlkGeometry {
#[derive(Debug, Copy, Clone, Pod)]
#[repr(C)]
pub struct VirtioBlkTopology {
pub struct VirtioBlockTopology {
physical_block_exp: u8,
alignment_offset: u8,
min_io_size: u16,
opt_io_size: u32,
}
impl VirtioBlkConfig {
impl VirtioBlockConfig {
pub(self) fn new(transport: &dyn VirtioTransport) -> SafePtr<Self, IoMem> {
let memory = transport.device_config_memory();
SafePtr::new(memory, 0)