Skip to content

Commit dd30b80

Browse files
committed
block/virtio_blk: Add new driver
1 parent c48e4ed commit dd30b80

6 files changed

Lines changed: 679 additions & 0 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "virtio_blk"
3+
version = "0.1.0"
4+
edition = "2024"
5+
build = "../../build.rs"
6+
7+
[lib]
8+
crate-type = ["dylib"]
9+
10+
[dependencies]
11+
zinnia = { workspace = true }
12+
virtio = { path = "../../common/virtio" }
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
use crate::{error::BlkError, queue::RequestQueue, spec};
2+
use virtio::VirtioDevice;
3+
use zinnia::{
4+
alloc::sync::Arc,
5+
arch,
6+
device::{
7+
Device,
8+
block::{BioRequest, BlockDevice, BlockLimits, BlockOp},
9+
},
10+
log,
11+
memory::Register,
12+
posix::errno::{EResult, Errno},
13+
vfs::file::{FileOps, OpenFlags},
14+
warn,
15+
};
16+
17+
const VIRTIO_BLK_MAJOR: u32 = 254;
18+
const MAX_TRANSFER_BYTES: usize = 1024 * 1024;
19+
20+
pub struct Geometry {
21+
lba_size: usize,
22+
lba_count: u64,
23+
sectors_per_lba: u64,
24+
max_lbas: usize,
25+
max_segments: usize,
26+
read_only: bool,
27+
}
28+
29+
impl Geometry {
30+
pub fn read(virtio: &VirtioDevice, features: u32, queue_size: u16) -> Result<Self, BlkError> {
31+
let read = |reg: Register<u32>| {
32+
virtio
33+
.read_config32(reg)
34+
.map_err(|_| BlkError::UnsupportedLayout)
35+
};
36+
37+
let capacity = (read(spec::config::CAPACITY_HI)? as u64) << 32
38+
| read(spec::config::CAPACITY_LO)? as u64;
39+
40+
let lba_size = if features & spec::VIRTIO_BLK_F_BLK_SIZE != 0 {
41+
let blk_size = read(spec::config::BLK_SIZE)? as usize;
42+
if blk_size.is_power_of_two()
43+
&& blk_size >= spec::SECTOR_SIZE
44+
&& blk_size <= arch::virt::get_page_size()
45+
{
46+
blk_size
47+
} else {
48+
warn!("Ignoring unusable block size {blk_size}");
49+
spec::SECTOR_SIZE
50+
}
51+
} else {
52+
spec::SECTOR_SIZE
53+
};
54+
55+
let sectors_per_lba = (lba_size / spec::SECTOR_SIZE) as u64;
56+
let lba_count = capacity / sectors_per_lba;
57+
if lba_count == 0 {
58+
return Err(BlkError::UnsupportedLayout);
59+
}
60+
61+
let hardware_segments = (queue_size as usize).saturating_sub(2).max(1);
62+
let max_segments = if features & spec::VIRTIO_BLK_F_SEG_MAX != 0 {
63+
(read(spec::config::SEG_MAX)? as usize).clamp(1, hardware_segments)
64+
} else {
65+
hardware_segments
66+
};
67+
68+
let max_bytes = if features & spec::VIRTIO_BLK_F_SIZE_MAX != 0 {
69+
match read(spec::config::SIZE_MAX)? as usize {
70+
0 => MAX_TRANSFER_BYTES,
71+
size_max => size_max.min(MAX_TRANSFER_BYTES),
72+
}
73+
} else {
74+
MAX_TRANSFER_BYTES
75+
};
76+
77+
Ok(Self {
78+
lba_size,
79+
lba_count,
80+
sectors_per_lba,
81+
max_lbas: (max_bytes / lba_size).max(1),
82+
max_segments,
83+
read_only: features & spec::VIRTIO_BLK_F_RO != 0,
84+
})
85+
}
86+
}
87+
88+
pub struct VirtioBlkDevice {
89+
queue: Arc<RequestQueue>,
90+
geometry: Geometry,
91+
minor: u32,
92+
}
93+
94+
impl VirtioBlkDevice {
95+
pub fn new(queue: Arc<RequestQueue>, geometry: Geometry, minor: u32) -> Self {
96+
log!(
97+
"New block device: {} byte blocks, {} MBs total, {}",
98+
geometry.lba_size,
99+
(geometry.lba_count * geometry.lba_size as u64) / 1024 / 1024,
100+
if geometry.read_only {
101+
"read-only"
102+
} else {
103+
"read-write"
104+
}
105+
);
106+
Self {
107+
queue,
108+
geometry,
109+
minor,
110+
}
111+
}
112+
}
113+
114+
impl BlockDevice for VirtioBlkDevice {
115+
fn get_lba_size(&self) -> usize {
116+
self.geometry.lba_size
117+
}
118+
119+
fn lba_count(&self) -> u64 {
120+
self.geometry.lba_count
121+
}
122+
123+
fn limits(&self) -> BlockLimits {
124+
BlockLimits {
125+
max_lbas: self.geometry.max_lbas,
126+
max_segments: self.geometry.max_segments,
127+
}
128+
}
129+
130+
fn submit_bio(&self, bio: &Arc<BioRequest>) -> EResult<()> {
131+
if self.geometry.read_only && bio.op() == BlockOp::Write {
132+
bio.complete(Err(Errno::EROFS));
133+
return Ok(());
134+
}
135+
136+
let Some(end_lba) = bio.lba().checked_add(bio.num_lbas() as u64) else {
137+
bio.complete(Err(Errno::EOVERFLOW));
138+
return Ok(());
139+
};
140+
if end_lba > self.geometry.lba_count {
141+
bio.complete(match bio.op() {
142+
BlockOp::Read => Ok(0),
143+
BlockOp::Write => Err(Errno::ENOSPC),
144+
});
145+
return Ok(());
146+
}
147+
148+
let Some(sector) = bio.lba().checked_mul(self.geometry.sectors_per_lba) else {
149+
bio.complete(Err(Errno::EOVERFLOW));
150+
return Ok(());
151+
};
152+
153+
let kind = match bio.op() {
154+
BlockOp::Read => spec::req_type::IN,
155+
BlockOp::Write => spec::req_type::OUT,
156+
};
157+
158+
if let Err(error) = self.queue.submit(bio, kind, sector) {
159+
bio.complete(Err(error.into()));
160+
return Ok(());
161+
}
162+
163+
self.queue.wait_if_polling(bio);
164+
Ok(())
165+
}
166+
}
167+
168+
impl Device for VirtioBlkDevice {
169+
fn open(self: Arc<Self>, _flags: OpenFlags) -> EResult<Arc<dyn FileOps>> {
170+
Ok(self.clone())
171+
}
172+
173+
fn major(&self) -> u32 {
174+
VIRTIO_BLK_MAJOR
175+
}
176+
177+
fn minor(&self) -> u32 {
178+
self.minor
179+
}
180+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use core::fmt::Display;
2+
use zinnia::posix::errno::Errno;
3+
4+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5+
pub enum BlkError {
6+
AllocationFailed,
7+
EncodingFailed,
8+
QueueFull,
9+
NotifyFailed,
10+
Timeout,
11+
UnsupportedLayout,
12+
}
13+
14+
impl From<BlkError> for Errno {
15+
fn from(value: BlkError) -> Self {
16+
match value {
17+
BlkError::AllocationFailed => Errno::ENOMEM,
18+
BlkError::QueueFull => Errno::EBUSY,
19+
BlkError::Timeout => Errno::ETIMEDOUT,
20+
BlkError::UnsupportedLayout => Errno::ENOTSUP,
21+
_ => Errno::EIO,
22+
}
23+
}
24+
}
25+
26+
impl Display for BlkError {
27+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
28+
match self {
29+
BlkError::AllocationFailed => f.write_str("Failed to allocate DMA memory"),
30+
BlkError::EncodingFailed => f.write_str("A request header did not fit into its slot"),
31+
BlkError::QueueFull => f.write_str("The request does not fit into the virtqueue"),
32+
BlkError::NotifyFailed => f.write_str("Failed to notify the device"),
33+
BlkError::Timeout => f.write_str("Timed out waiting for the device"),
34+
BlkError::UnsupportedLayout => f.write_str("The device reported an unusable geometry"),
35+
}
36+
}
37+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#![no_std]
2+
3+
use crate::{
4+
device::{Geometry, VirtioBlkDevice},
5+
queue::RequestQueue,
6+
};
7+
use core::sync::atomic::{AtomicUsize, Ordering};
8+
use virtio::VirtioDevice;
9+
use zinnia::{
10+
alloc::{boxed::Box, format, sync::Arc},
11+
device::pci::{DeviceView, Driver, PciVariant},
12+
error,
13+
irq::{IrqHandler, Status},
14+
log,
15+
posix::errno::{EResult, Errno},
16+
};
17+
18+
mod device;
19+
mod error;
20+
mod queue;
21+
mod spec;
22+
23+
const MSIX_VECTOR: u16 = 0;
24+
const MIN_QUEUE_SIZE: u16 = 4;
25+
26+
static BLK_COUNTER: AtomicUsize = AtomicUsize::new(0);
27+
28+
struct VirtioBlkIrqHandler {
29+
queue: Arc<RequestQueue>,
30+
}
31+
32+
impl IrqHandler for VirtioBlkIrqHandler {
33+
fn raise(&mut self) -> Status {
34+
self.queue.drain();
35+
Status::Handled
36+
}
37+
}
38+
39+
fn enable_bus_mastering(view: &mut DeviceView<'static>) {
40+
let command = view.access().read32(view.address(), 0x04) as u16;
41+
let updated = command | (1 << 1) | (1 << 2);
42+
view.access().write32(view.address(), 0x04, updated as u32);
43+
}
44+
45+
fn bind_interrupts(virtio: &mut VirtioDevice) -> EResult<bool> {
46+
if virtio.set_queue_msix_vector(spec::REQUEST_QUEUE, MSIX_VECTOR)? != MSIX_VECTOR {
47+
return Ok(false);
48+
}
49+
virtio.set_config_msix_vector(0xFFFF)?;
50+
Ok(true)
51+
}
52+
53+
fn probe(_: &PciVariant, mut view: DeviceView<'static>) -> EResult<()> {
54+
log!("Probing VirtIO block device on {}", view.address());
55+
56+
enable_bus_mastering(&mut view);
57+
let irq_line = view.setup_irq().ok();
58+
59+
let mut virtio = VirtioDevice::new_pci(view)?;
60+
61+
let device_lo = virtio.get_device_features(0)?;
62+
let device_hi = virtio.get_device_features(1)?;
63+
if device_hi & spec::VIRTIO_F_VERSION_1_LO == 0 {
64+
error!("VirtIO block device does not implement the 1.0 interface");
65+
return Err(Errno::ENOTSUP);
66+
}
67+
68+
let driver_lo = device_lo & spec::SUPPORTED_FEATURES;
69+
virtio.set_driver_features(0, driver_lo)?;
70+
virtio.set_driver_features(1, spec::VIRTIO_F_VERSION_1_LO)?;
71+
virtio.finalize_features()?;
72+
log!("Negotiated features: {driver_lo:#010x}");
73+
74+
if virtio.num_queues()? == 0 {
75+
error!("VirtIO block device exposes no request queue");
76+
return Err(Errno::ENODEV);
77+
}
78+
79+
let queue = virtio.setup_queue(spec::REQUEST_QUEUE)?;
80+
let queue_size = queue.queue_size();
81+
if queue_size < MIN_QUEUE_SIZE {
82+
error!("Request queue is too small ({queue_size} descriptors)");
83+
return Err(Errno::ENODEV);
84+
}
85+
86+
let geometry = Geometry::read(&virtio, driver_lo, queue_size)?;
87+
88+
let irq_line = match irq_line {
89+
Some(line) => bind_interrupts(&mut virtio)?.then_some(line),
90+
None => None,
91+
};
92+
if irq_line.is_none() {
93+
log!("Falling back to polled completions");
94+
}
95+
96+
virtio.set_driver_ok()?;
97+
98+
let requests = Arc::new(RequestQueue::new(virtio, queue, irq_line.is_none())?);
99+
100+
if let Some(line) = irq_line.as_ref() {
101+
line.attach(Box::new(VirtioBlkIrqHandler {
102+
queue: requests.clone(),
103+
}));
104+
line.unmask();
105+
}
106+
107+
let index = BLK_COUNTER.fetch_add(1, Ordering::SeqCst);
108+
let blk = Arc::new(VirtioBlkDevice::new(requests, geometry, index as u32));
109+
110+
zinnia::device::block::register_block_device(&format!("virtblk{index}"), blk)
111+
}
112+
113+
const BASE_VARIANT: PciVariant = PciVariant::new().vendor(0x1AF4);
114+
115+
static DRIVER: Driver = Driver {
116+
name: "virtio_blk",
117+
probe,
118+
variants: &[BASE_VARIANT.device(0x1001), BASE_VARIANT.device(0x1042)],
119+
};
120+
121+
zinnia::module!("VirtIO block driver", "Marvin Friedrich", main);
122+
123+
pub fn main(_cmdline: &str) {
124+
match DRIVER.register() {
125+
Ok(_) => (),
126+
Err(e) => error!("Unable to load VirtIO block driver: {:?}", e),
127+
}
128+
}

0 commit comments

Comments
 (0)