RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / src / block / virtio_blk.rs
11847 bytes ยท raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
//! Driver for virtio-blk: the paravirtual disk QEMU/KVM offer
//! (`-drive file=disk.img,if=virtio`).
//!
//! Why virtio and not ATA PIO? ATA PIO is simpler (copy 256 words through an
//! I/O port per sector) but it's a 1980s interface that real machines barely
//! have any more. virtio is what VMs actually use, and it teaches the core
//! idea of every modern device: *shared memory queues + DMA*. We use the
//! **legacy** (virtio 0.9.5) interface, which QEMU's "transitional" virtio
//! devices still offer: its registers are a handful of I/O ports (BAR0), and
//! setting it up is ~40 lines. The modern (1.0) interface uses PCI
//! capabilities + MMIO; the queue format is the same.
//!
//! The virtqueue: three arrays in physical memory that driver and device share.
//!   * descriptor table: {physical addr, length, flags, next} -- buffers
//!   * available ring: driver writes "descriptor chain N is ready"
//!   * used ring: device writes "I finished chain N"
//! A block request is a chain of 3 descriptors:
//!   header {type: read/write, sector}  ->  data (512*n bytes)  ->  status byte
//! We put the chain in the available ring, write the queue number to the
//! "notify" port, and wait until the used ring's index moves.
//!
//! Simplifications: one request at a time; we *poll* the used ring instead of
//! using the interrupt (we even ask the device not to interrupt us), and we
//! copy data through one page-sized "bounce buffer", so a request moves at
//! most 8 sectors. Reference: virtio spec 1.2 sections 2.7 (split virtqueues),
//! 4.1.4.8 (legacy PCI interface), 5.2 (block device); wiki.osdev.org/Virtio

use super::{BlockDevice, SECTOR_SIZE, check_range};
use crate::errno::Errno;
use crate::memory;
use crate::pci::PciDevice;
use core::ptr::{read_volatile, write_volatile};
use core::sync::atomic::{Ordering, fence};
use spin::Mutex;
use x86_64::PhysAddr;
use x86_64::instructions::port::Port;

// Legacy virtio PCI registers (offsets into the I/O BAR).
const REG_DEVICE_FEATURES: u16 = 0x00;
const REG_GUEST_FEATURES: u16 = 0x04;
const REG_QUEUE_ADDRESS: u16 = 0x08; // physical page number of the queue
const REG_QUEUE_SIZE: u16 = 0x0c;
const REG_QUEUE_SELECT: u16 = 0x0e;
const REG_QUEUE_NOTIFY: u16 = 0x10;
const REG_DEVICE_STATUS: u16 = 0x12;
const REG_ISR_STATUS: u16 = 0x13;
const REG_CAPACITY: u16 = 0x14; // device config: disk size in sectors (u64)

// Device status bits (we tell the device how far initialization got).
const STATUS_ACKNOWLEDGE: u8 = 1; // "I see you"
const STATUS_DRIVER: u8 = 2; // "I know how to drive you"
const STATUS_DRIVER_OK: u8 = 4; // "ready to go"

// Descriptor flags.
const DESC_NEXT: u16 = 1; // chain continues at `next`
const DESC_WRITE: u16 = 2; // device writes into this buffer (vs. reads it)
const AVAIL_NO_INTERRUPT: u16 = 1; // "don't interrupt me, I'm polling"

const REQ_READ: u32 = 0; // VIRTIO_BLK_T_IN
const REQ_WRITE: u32 = 1; // VIRTIO_BLK_T_OUT

/// Max sectors per request = what fits in our one-page bounce buffer.
const MAX_SECTORS: usize = 4096 / SECTOR_SIZE;

#[repr(C)]
struct Descriptor {
    addr: u64,
    len: u32,
    flags: u16,
    next: u16,
}

#[derive(Debug)]
pub enum VirtioError {
    NoIoBar,
    QueueUnavailable,
    OutOfMemory,
}

/// PCI vendor 0x1af4 (Red Hat/virtio), device 0x1001 = transitional block device.
pub fn is_virtio_blk(dev: &PciDevice) -> bool {
    dev.vendor_id == 0x1af4 && dev.device_id == 0x1001
}

pub struct VirtioBlk {
    io_base: u16,
    sectors: u64,
    queue: Mutex<Queue>,
}

struct Queue {
    size: u16,
    /// Physical address of the queue memory (descriptors, then available
    /// ring, then -- at the next page boundary -- the used ring).
    phys: PhysAddr,
    used_offset: usize,
    /// Our copy of the used ring index: how many requests we've seen finish.
    last_used: u16,
    /// One page for the request header (offset 0) and status byte (offset 16).
    header_page: PhysAddr,
    /// One page bounce buffer for the data.
    data_page: PhysAddr,
}

impl VirtioBlk {
    pub fn new(pci: &PciDevice) -> Result<Self, VirtioError> {
        let bar0 = pci.bar(0);
        if bar0 & 1 == 0 {
            return Err(VirtioError::NoIoBar); // legacy interface lives in I/O space
        }
        pci.enable_io_and_bus_master(); // bus master = allowed to DMA into our RAM
        let io_base = (bar0 & !0x3) as u16;
        let dev = VirtioBlk { io_base, sectors: 0, queue: Mutex::new(Queue::empty()) };

        // 1. Reset, then announce ourselves.
        dev.write8(REG_DEVICE_STATUS, 0);
        dev.write8(REG_DEVICE_STATUS, STATUS_ACKNOWLEDGE);
        dev.write8(REG_DEVICE_STATUS, STATUS_ACKNOWLEDGE | STATUS_DRIVER);
        // 2. Feature negotiation: the device lists what it can do; we accept
        //    nothing optional (plain read/write is always supported).
        let _offered = dev.read32(REG_DEVICE_FEATURES);
        dev.write32(REG_GUEST_FEATURES, 0);
        // 3. Set up queue 0 (block devices have one request queue).
        dev.write16(REG_QUEUE_SELECT, 0);
        let size = dev.read16(REG_QUEUE_SIZE); // the device decides (QEMU: 256)
        if size == 0 {
            return Err(VirtioError::QueueUnavailable);
        }
        // Legacy layout: descriptors (16 bytes each) + available ring
        // (2+2+2*size+2), then the used ring (2+2+8*size+2) page-aligned.
        let n = size as usize;
        let used_offset = align_up(16 * n + 6 + 2 * n, 4096);
        let total = used_offset + align_up(6 + 8 * n, 4096);
        let phys = memory::alloc_contiguous_frames(total / 4096).ok_or(VirtioError::OutOfMemory)?;
        let header_page = memory::alloc_contiguous_frames(1).ok_or(VirtioError::OutOfMemory)?;
        let data_page = memory::alloc_contiguous_frames(1).ok_or(VirtioError::OutOfMemory)?;
        dev.write32(REG_QUEUE_ADDRESS, (phys.as_u64() / 4096) as u32);
        // 4. Read the disk size from the device config area, then go.
        let lo = dev.read32(REG_CAPACITY) as u64;
        let hi = dev.read32(REG_CAPACITY + 4) as u64;
        dev.write8(REG_DEVICE_STATUS, STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_DRIVER_OK);

        let queue = Queue { size, phys, used_offset, last_used: 0, header_page, data_page };
        unsafe { write_volatile(queue.avail_flags(), AVAIL_NO_INTERRUPT) };
        Ok(VirtioBlk { io_base, sectors: hi << 32 | lo, queue: Mutex::new(queue) })
    }

    /// Do one request of up to MAX_SECTORS. For writes, the data must
    /// already be in the bounce buffer; for reads, it's there afterwards.
    fn request(&self, q: &mut Queue, kind: u32, lba: u64, sectors: usize) -> Result<(), Errno> {
        let header = memory::phys_to_virt(q.header_page).as_mut_ptr::<u8>();
        unsafe {
            write_volatile(header as *mut u32, kind); // type
            write_volatile(header.add(4) as *mut u32, 0); // reserved
            write_volatile(header.add(8) as *mut u64, lba); // first sector
            write_volatile(header.add(16), 0xff); // status: device sets 0 = OK
        }
        // The chain: descriptors 0 -> 1 -> 2 (always the same three, since we
        // only ever have one request in flight).
        let data_flags = if kind == REQ_READ { DESC_WRITE } else { 0 };
        q.set_desc(0, q.header_page.as_u64(), 16, DESC_NEXT, 1);
        q.set_desc(1, q.data_page.as_u64(), (sectors * SECTOR_SIZE) as u32, data_flags | DESC_NEXT, 2);
        q.set_desc(2, q.header_page.as_u64() + 16, 1, DESC_WRITE, 0);

        // Publish chain 0 in the available ring, then bump its index.
        unsafe {
            let idx = read_volatile(q.avail_idx());
            write_volatile(q.avail_ring(idx % q.size), 0);
            fence(Ordering::SeqCst); // ring entry visible before the index
            write_volatile(q.avail_idx(), idx.wrapping_add(1));
            fence(Ordering::SeqCst);
        }
        self.write16(REG_QUEUE_NOTIFY, 0); // "queue 0 has work" -> QEMU does the I/O

        // Poll the used ring until the device reports completion.
        while unsafe { read_volatile(q.used_idx()) } == q.last_used {
            core::hint::spin_loop();
        }
        fence(Ordering::SeqCst);
        q.last_used = q.last_used.wrapping_add(1);
        let _ = self.read8(REG_ISR_STATUS); // reading acknowledges (clears) it

        match unsafe { read_volatile(header.add(16)) } {
            0 => Ok(()),
            _ => Err(Errno::EIO),
        }
    }

    fn read8(&self, reg: u16) -> u8 {
        unsafe { Port::<u8>::new(self.io_base + reg).read() }
    }
    fn write8(&self, reg: u16, v: u8) {
        unsafe { Port::<u8>::new(self.io_base + reg).write(v) }
    }
    fn read16(&self, reg: u16) -> u16 {
        unsafe { Port::<u16>::new(self.io_base + reg).read() }
    }
    fn write16(&self, reg: u16, v: u16) {
        unsafe { Port::<u16>::new(self.io_base + reg).write(v) }
    }
    fn read32(&self, reg: u16) -> u32 {
        unsafe { Port::<u32>::new(self.io_base + reg).read() }
    }
    fn write32(&self, reg: u16, v: u32) {
        unsafe { Port::<u32>::new(self.io_base + reg).write(v) }
    }
}

impl BlockDevice for VirtioBlk {
    fn sector_count(&self) -> u64 {
        self.sectors
    }

    fn read_sectors(&self, lba: u64, buf: &mut [u8]) -> Result<(), Errno> {
        check_range(self, lba, buf.len())?;
        // Interrupts off while we hold the queue lock (see thread.rs locking
        // rule): a request takes microseconds in QEMU.
        x86_64::instructions::interrupts::without_interrupts(|| {
            let mut q = self.queue.lock();
            for (i, chunk) in buf.chunks_mut(MAX_SECTORS * SECTOR_SIZE).enumerate() {
                let sectors = chunk.len() / SECTOR_SIZE;
                self.request(&mut q, REQ_READ, lba + (i * MAX_SECTORS) as u64, sectors)?;
                let src = memory::phys_to_virt(q.data_page).as_ptr::<u8>();
                unsafe { core::ptr::copy_nonoverlapping(src, chunk.as_mut_ptr(), chunk.len()) };
            }
            Ok(())
        })
    }

    fn write_sectors(&self, lba: u64, buf: &[u8]) -> Result<(), Errno> {
        check_range(self, lba, buf.len())?;
        x86_64::instructions::interrupts::without_interrupts(|| {
            let mut q = self.queue.lock();
            for (i, chunk) in buf.chunks(MAX_SECTORS * SECTOR_SIZE).enumerate() {
                let dst = memory::phys_to_virt(q.data_page).as_mut_ptr::<u8>();
                unsafe { core::ptr::copy_nonoverlapping(chunk.as_ptr(), dst, chunk.len()) };
                self.request(&mut q, REQ_WRITE, lba + (i * MAX_SECTORS) as u64, chunk.len() / SECTOR_SIZE)?;
            }
            Ok(())
        })
    }
}

impl Queue {
    const fn empty() -> Self {
        Queue {
            size: 0,
            phys: PhysAddr::zero(),
            used_offset: 0,
            last_used: 0,
            header_page: PhysAddr::zero(),
            data_page: PhysAddr::zero(),
        }
    }
    fn base(&self) -> *mut u8 {
        memory::phys_to_virt(self.phys).as_mut_ptr()
    }
    fn set_desc(&self, i: usize, addr: u64, len: u32, flags: u16, next: u16) {
        let desc = unsafe { (self.base() as *mut Descriptor).add(i) };
        unsafe { write_volatile(desc, Descriptor { addr, len, flags, next }) };
    }
    fn avail_base(&self) -> *mut u16 {
        unsafe { self.base().add(16 * self.size as usize) as *mut u16 }
    }
    fn avail_flags(&self) -> *mut u16 {
        self.avail_base()
    }
    fn avail_idx(&self) -> *mut u16 {
        unsafe { self.avail_base().add(1) }
    }
    fn avail_ring(&self, i: u16) -> *mut u16 {
        unsafe { self.avail_base().add(2 + i as usize) }
    }
    fn used_idx(&self) -> *const u16 {
        unsafe { (self.base().add(self.used_offset) as *const u16).add(1) }
    }
}

fn align_up(x: usize, align: usize) -> usize {
    (x + align - 1) & !(align - 1)
}