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:

FieldMeaning
descDescriptor table used to describe request buffers.
availRing where the driver publishes descriptor chains for the device.
usedRing where the device publishes completed descriptor chains.
free[NUM]Tracks which descriptors are free.
used_idxTracks 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_lockProtects virtio disk driver state.

The virtqueue has three main parts.

PartOwnerPurpose
Descriptor tableDriver writes, device readsDescribes memory buffers for a request.
Available ringDriver writes, device readsTells the device which descriptor chain to process.
Used ringDevice writes, driver readsTells 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