Drivers manage specific hardware devices by configuring hardware, initiating operations, handling resulting interrupts, and interacting with waiting processes. Device interrupts are a class of traps routed through the kernel’s trap handling logic (e.g., devintr). Driver execution is structured into two concurrent contexts:
- Top half: within a process’s kernel thread via system calls (e.g.,
read,write), asks the hardware to initiate operations, and yields the CPU to wait for completion. - Bottom half: asynchronously at interrupt time, identifies completed operations, wakes waiting processes, and issues the next pending hardware command.
UART
The following diagram summarizes the full driver stack from user process to hardware:
--- config: layout: dagre --- flowchart LR subgraph P["caller side"] direction TB USER_IO["user process<br><br>read(fd)<br>write(fd)"] KERNEL_OUT["kernel output<br><br>printf<br>console echo"] end subgraph C["console boundary"] direction TB DEVSW["devsw[CONSOLE]<br><br>read = consoleread<br>write = consolewrite"] CONSOLE["console.c<br><br>line buffering<br>consoleintr(c)<br>consoleread / consolewrite"] end subgraph U["UART driver interface"] direction TB UARTINIT["uartinit()<br><br>configure 16550a UART<br>enable RX/TX interrupts"] UARTOUT["UART output path<br><br>uartwrite<br>uartputc_sync"] UARTINTR["uartintr()<br><br>handle RX-ready<br>handle TX-ready"] end subgraph S["UART driver state"] direction TB TXSTATE["TX state<br><br>tx_lock<br>tx_busy<br>tx_chan"] end subgraph H["UART hardware source"] direction TB UART_REGS["16550a UART registers<br><br>RHR / THR<br>IER / ISR<br>LSR"] UART_IRQ["UART0_IRQ<br><br>external interrupt"] end subgraph I["interrupt delivery"] direction TB PLIC_PATH["PLIC + devintr()<br><br>claim UART IRQ<br>call uartintr<br>complete IRQ"] end USER_IO -- console fd read/write --> DEVSW DEVSW -- dispatches to --> CONSOLE CONSOLE -- consolewrite sends bytes --> UARTOUT KERNEL_OUT -- direct/synchronous output --> UARTOUT UARTINIT -- configures --> UART_REGS UARTOUT -- writes THR / checks LSR --> UART_REGS UARTOUT -- uses --> TXSTATE UART_REGS -- input ready or TX ready --> UART_IRQ UART_IRQ -- external interrupt --> PLIC_PATH PLIC_PATH -- calls --> UARTINTR UARTINTR -- reads/writes UART registers --> UART_REGS UARTINTR -- updates TX completion --> TXSTATE UARTINTR -- passes input chars upward --> CONSOLE USER_IO:::process KERNEL_OUT:::process DEVSW:::iface CONSOLE:::iface UARTINIT:::iface UARTOUT:::iface UARTINTR:::iface TXSTATE:::source UART_REGS:::source UART_IRQ:::source PLIC_PATH:::backend classDef process fill:#F3EFE2,stroke:#111,stroke-width:2px,color:#111 classDef source fill:#E9F1FF,stroke:#111,stroke-width:2px,color:#111 classDef iface fill:#EDE7D4,stroke:#111,stroke-width:2px,color:#111 classDef backend fill:#F8F8F8,stroke:#111,stroke-width:2px,color:#111
Virtio Disk
The virtio disk driver handles block I/O through a shared memory virtqueue. Unlike the UART (character device with PIO), the virtio disk uses DMA: the driver builds descriptors in memory, notifies the device, and the device transfers data directly.
he following diagram shows the driver architecture:
--- config: layout: dagre --- flowchart LR subgraph C["caller side"] direction TB FS["filesystem / log layer<br><br>readi / writei<br>log_write / install_trans"] BCACHE["buffer cache<br><br>bread<br>bwrite"] BUF["struct buf<br><br>dev<br>blockno<br>valid<br>disk<br>data[BSIZE]"] end subgraph VDI["virtio disk interface"] direction TB VINIT["virtio_disk_init()<br><br>verify device<br>negotiate features<br>initialize queue 0"] VRW["virtio_disk_rw(b, write)<br><br>build disk request<br>submit to virtqueue<br>sleep until done"] VINTR["virtio_disk_intr()<br><br>ack interrupt<br>process used ring<br>wake completed buffers"] end subgraph VDS["virtio disk state"] direction TB DISK["struct disk<br><br>vdisk_lock<br>free[]<br>used_idx<br>info[]<br>ops[]"] VQ["virtqueue<br><br>desc[]<br>avail ring<br>used ring"] end subgraph HW["virtio disk hardware source"] direction TB MMIO["virtio MMIO registers<br><br>STATUS<br>QUEUE_NOTIFY<br>INTERRUPT_STATUS<br>INTERRUPT_ACK"] DEVICE["qemu virtio-blk device<br><br>reads / writes fs.img blocks"] IRQ["VIRTIO0_IRQ<br><br>external interrupt"] end subgraph I["interrupt delivery"] direction TB PLIC_PATH["PLIC + devintr()<br><br>claim virtio IRQ<br>call virtio_disk_intr<br>complete IRQ"] end FS -- needs disk block --> BCACHE BCACHE -- uses locked buffer --> BUF BCACHE -- cache miss or writeback --> VRW VINIT -- sets up --> DISK VINIT -- allocates and registers --> VQ VINIT -- configures --> MMIO VRW -- uses buffer data --> BUF VRW -- allocates 3 descriptors<br>header + data + status --> VQ VRW -- "records in-flight request" --> DISK VRW -- notifies queue 0 --> MMIO MMIO -- device sees available request --> DEVICE DEVICE -- DMA reads/writes buffer block --> VQ DEVICE -- completion interrupt --> IRQ IRQ -- external interrupt --> PLIC_PATH PLIC_PATH -- calls --> VINTR VINTR -- acknowledges interrupt --> MMIO VINTR -- reads completed entries --> VQ VINTR -- "clears b->disk and wakeup(b)" --> BUF FS:::process BCACHE:::iface BUF:::file VINIT:::iface VRW:::iface VINTR:::iface DISK:::source VQ:::source MMIO:::source DEVICE:::source IRQ:::source PLIC_PATH:::backend classDef process fill:#F3EFE2,stroke:#111,stroke-width:2px,color:#111 classDef file fill:#FFFFFF,stroke:#111,stroke-width:3px,color:#111 classDef source fill:#E9F1FF,stroke:#111,stroke-width:2px,color:#111 classDef iface fill:#EDE7D4,stroke:#111,stroke-width:2px,color:#111 classDef backend fill:#F8F8F8,stroke:#111,stroke-width:2px,color:#111
The driver maintains global state in struct disk. Key fields:
| Field | Meaning |
|---|---|
desc | Descriptor table used to describe request buffers. |
avail | Ring where the driver publishes descriptor chains for the device. |
used | Ring where the device publishes completed descriptor chains. |
free[NUM] | Tracks which descriptors are free. |
used_idx | Tracks how far the driver has processed the used ring. |
info[NUM] | Maps an in-flight request back to its struct buf and status byte. |
ops[NUM] | Request headers, one per descriptor slot. |
vdisk_lock | Protects virtio disk driver state. |
The virtqueue has three main parts.
| Part | Owner | Purpose |
|---|---|---|
| Descriptor table | Driver writes, device reads | Describes memory buffers for a request. |
| Available ring | Driver writes, device reads | Tells the device which descriptor chain to process. |
| Used ring | Device writes, driver reads | Tells the driver which request finished. |
The full read/write flow proceeds as follows:
sequenceDiagram autonumber participant Proc as "process / filesystem code" participant BCache as "buffer cache" participant VDRW as "virtio_disk_rw(b, write)" participant Desc as "virtqueue descriptors" participant Avail as "avail ring" participant MMIO as "virtio MMIO regs" participant Disk as "virtio disk device" participant Used as "used ring" participant Trap as "kerneltrap/usertrap" participant Devintr as "devintr" participant PLIC as "PLIC" participant VIntr as "virtio_disk_intr" participant Sched as "scheduler" Proc->>BCache: bread(blockno) or bwrite(b) BCache->>VDRW: virtio_disk_rw(b, write) VDRW->>VDRW: acquire disk.vdisk_lock alt no 3 free descriptors VDRW->>Sched: sleep(&disk.free[0], &disk.vdisk_lock) Note over VDRW: wait until another disk request frees descriptors Sched-->>VDRW: later scheduled after wakeup end VDRW->>Desc: alloc3_desc(idx) VDRW->>Desc: desc[0] = virtio_blk_req header alt write request VDRW->>Desc: req.type = VIRTIO_BLK_T_OUT VDRW->>Desc: desc[1] points to b->data, device reads data else read request VDRW->>Desc: req.type = VIRTIO_BLK_T_IN VDRW->>Desc: desc[1] points to b->data, device writes data end VDRW->>Desc: desc[2] points to status byte, device writes status VDRW->>VDRW: b->disk = 1 VDRW->>Avail: put desc[0] head index into avail->ring VDRW->>Avail: memory barrier, avail->idx++ VDRW->>MMIO: write VIRTIO_MMIO_QUEUE_NOTIFY = 0 loop while b->disk == 1 VDRW->>Sched: sleep(b, &disk.vdisk_lock) Note over VDRW: caller sleeps until this buffer's disk I/O completes end Disk->>Avail: read available descriptor chain Disk->>Desc: read request header alt write request Disk->>Desc: DMA-read b->data from memory Disk->>Disk: write block to disk image else read request Disk->>Disk: read block from disk image Disk->>Desc: DMA-write data into b->data end Disk->>Desc: write status byte Disk->>Used: put completed desc head into used ring Disk->>Trap: raise virtio interrupt Trap->>Devintr: devintr() Devintr->>PLIC: plic_claim() PLIC-->>Devintr: VIRTIO0_IRQ Devintr->>VIntr: virtio_disk_intr() VIntr->>VIntr: acquire disk.vdisk_lock VIntr->>MMIO: read INTERRUPT_STATUS VIntr->>MMIO: write INTERRUPT_ACK loop while disk.used_idx != used->idx VIntr->>Used: read used->ring[disk.used_idx % NUM] VIntr->>Desc: get disk.info[id].b VIntr->>VIntr: check status == 0 VIntr->>VDRW: b->disk = 0 VIntr->>Sched: wakeup(b) VIntr->>VIntr: disk.used_idx++ end VIntr->>VIntr: release disk.vdisk_lock VIntr-->>Devintr: return Devintr->>PLIC: plic_complete(VIRTIO0_IRQ) Devintr-->>Trap: return from interrupt Sched-->>VDRW: later schedule sleeping caller VDRW->>Desc: free_chain(desc[0]) VDRW->>Sched: wakeup(&disk.free[0]) VDRW->>VDRW: release disk.vdisk_lock VDRW-->>BCache: return alt bread path BCache->>BCache: b->valid = 1 BCache-->>Proc: return locked buffer with data else bwrite path BCache-->>Proc: write complete end