virtio_disk_init sets up the virtio block device — the emulated hard disk that holds the filesystem. It’s the most complex init function in the boot sequence because it has to go through a multi-step handshake protocol defined by the virtio specification.
Verifying the device exists
First it checks four magic registers at the virtio MMIO address (0x10001000). The magic value must be 0x74726976 (which is “virt” in ASCII), version must be 2, device ID must be 2 (block device), and vendor must be 0x554d4551 (“QEMU”). If any of these are wrong, the device isn’t what we expect — panic.
The virtio handshake
The driver and device go through a status negotiation, one bit at a time:
First, reset the device by writing 0 to the status register. Clean slate.
Then set ACKNOWLEDGE — “I see you, I know you’re a virtio device.”
Then set DRIVER — “I know how to drive you.”
Then feature negotiation. The device advertises what it can do, the driver strips out features it doesn’t want. xv6 turns off read-only mode, SCSI commands, write cache, multi-queue, and some ring optimizations. It wants the simplest possible block device — just read and write sectors. Write back the accepted features.
Then set FEATURES_OK and re-read status to confirm the device accepted the feature set. If FEATURES_OK isn’t set after writing it, the device rejected the negotiation — panic.
Setting up the virtqueue
Virtio uses a shared memory structure called a virtqueue for communication between the driver and device. It has three parts:
The descriptor table (desc) — an array of descriptors, each pointing to a buffer in memory. The driver chains descriptors together to describe a single I/O operation.
The available ring (avail) — where the driver tells the device “here are new requests to process.” The driver writes descriptor chain heads here.
The used ring (used) — where the device tells the driver “I finished these requests.” The device writes completed descriptor indices here.
The function allocates three pages with kalloc (one for each part), zeros them, tells the device their physical addresses by writing to the MMIO registers (split into low and high 32-bit halves because the registers are 32-bit but addresses are 64-bit), and sets the queue size.
Then it marks the queue as ready and sets DRIVER_OK — “I’m fully initialized, let’s go.”
The disk struct
All the driver’s bookkeeping lives in a static disk struct:
free[NUM] tracks which descriptors are available. Initialized to all 1 (all free).
used_idx tracks how far the driver has read in the used ring. Starts at 0.
info[NUM] maps each in-flight descriptor chain to the buffer it belongs to and a status byte. When the completion interrupt arrives, the driver uses this to find which struct buf to wake up.
ops[NUM] holds the command headers — each disk operation needs a small header saying “read or write, which sector.” These are pre-allocated, one per descriptor.
How it gets used later
When bread needs to read a block from disk, it calls virtio_disk_rw, which allocates three descriptors (command header, data buffer, status byte), chains them together, puts the chain head in the available ring, and pokes the device by writing to VIRTIO_MMIO_QUEUE_NOTIFY. Then the calling process sleeps.
The device (QEMU) processes the request, does the actual disk I/O, puts the completed descriptor index in the used ring, and fires an interrupt. The interrupt handler (virtio_disk_intr) walks the used ring, finds which buffer finished, sets b->disk = 0, and calls wakeup(b). The sleeping process wakes up and finds its data ready.
The whole thing is essentially a two-way mailbox. The driver drops requests in the available ring, the device drops completions in the used ring, and interrupts plus sleep/wakeup synchronize the two sides.