The hart priority threshold is also set here.
PLIC_SPRIORITY(hart)selects this hart’s supervisor-mode threshold register- xv6 writes threshold
0 - A pending interrupt is delivered only if its priority is greater than the threshold
- UART and virtio were given priority
1inplicinit - Since
1 > 0, this hart can receive those interrupts
External device interrupts then pass through devintr.
- The CPU enters the supervisor trap path
devintrreadsscause- Supervisor external interrupt means the interrupt came through the PLIC
plic_claimasks the PLIC which device interruptedplic_completetells the PLIC that the device interrupt has been handled
UART interrupt path:
- PLIC reports
UART0_IRQ devintrcallsuartintruartintrreads pending bytes from the UART- Each input byte is passed to
consoleintr consoleintrupdates the console input buffer and wakesconsolereadwhen input is ready
Virtio disk interrupt path:
- PLIC reports
VIRTIO0_IRQ devintrcallsvirtio_disk_intrvirtio_disk_intrmarks completed disk requests as done- Sleeping processes waiting for disk I/O are woken
After this, the current hart can receive external interrupts from the UART and virtio disk through the PLIC.
Interrupts and Device Drivers
Driver Architecture
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: Executes 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: Executes asynchronously at interrupt time. Identifies completed operations, wakes waiting processes, and issues the next pending hardware command.
Separating device management into process-driven top halves and asynchronous bottom halves provides the architectural foundation for handling unpredictable external events, such as console input.
Initialization
xv6 configures the console/UART once during boot so later console input can be handled by interrupts instead of polling.
- Purpose: xv6 uses the UART as the console device for keyboard input and terminal output.
- QEMU setup: In QEMU, the UART is simulated. The keyboard/display are connected to xv6 through QEMU’s emulated UART.
- UART hardware model: xv6 talks to a 16550 UART chip, emulated by QEMU.
- Memory-mapped I/O: UART registers are exposed at physical addresses, not normal RAM.
- UART base address:
UART0 = 0x10000000 - Control registers: UART registers are byte-wide and accessed using offsets from
UART0. - Receive side:
- UART stores received input bytes in an internal FIFO.
LSRregister tells whether input is ready.RHRregister is used to read received bytes.- Reading from
RHRremoves that byte from the UART FIFO.
- Transmit side:
- Software writes output bytes to
THR. - UART sends those bytes to the terminal/display.
- Software writes output bytes to
- Initialization entry point:
main()callsconsoleinit().
- What
consoleinit()does:- Initializes the console lock.
- Connects console read/write functions to xv6’s device switch table.
- Calls
uartinit()to initialize UART hardware.
- What
uartinit()configures:- Enables UART receive interrupts.
- Enables UART transmit-complete interrupts.
- Sets up UART so xv6 is notified when input arrives or output is ready for more bytes.
Input:

Output:

Concurrency
Driver data structures are vulnerable to three distinct concurrency vectors that require lock protection:
- Simultaneous execution of top-half routines by multiple processes on different CPUs.
- Hardware interrupting a CPU while it is mid-execution in a top-half routine.
- Hardware delivering an interrupt on a secondary CPU concurrently with top-half execution on a primary CPU.
Interrupt delivery creates a separate constraint: the process waiting for a device may not be the process running when the interrupt arrives, and there may be no current user process at all. Thus interrupt handlers cannot assume the interrupted process is the one waiting for the device or safely use its page table (e.g.,
copyoutwith the current process). Bottom halves therefore do minimal work: copy data into a kernel buffer, update device state, and wake the top half to finish the rest.
Timer Interrupts
- Periodic timer interrupts drive the system clock and enforce preemptive thread scheduling via
yield. - xv6 uses them to maintain time and to switch among compute-bound processes; the
yieldcalls inusertrapandkerneltrapperform that switching. - Modern xv6 uses the RISC-V
sstcextension, so supervisor mode can receive timer interrupts directly. - Early machine-mode setup prepares timer delivery before the kernel proper starts:
- delegates interrupts and exceptions to supervisor mode,
- enables supervisor external and timer interrupts,
- enables
sstcthroughmenvcfg, - allows supervisor mode to read the
timecounter throughmcounteren, and - schedules the first timer interrupt with
stimecmp.
- The hardware
timecounter keeps increasing. - When
timereachesstimecmp, the hardware raises a supervisor timer interrupt. - The normal supervisor trap path handles the interrupt; the old machine-mode forwarding path through CLINT is not used here.
- The next timer event is scheduled by writing a later value to
stimecmp.
Real-World
Xv6 allows device and timer interrupts while running kernel code as well as user code. Timer interrupts may call yield even from kernel context, which helps time-slice compute-bound kernel threads. The cost is extra complexity: kernel code must tolerate being suspended and later resumed, possibly on a different CPU. A simpler kernel could permit interrupts only while running user code, but that would reduce fairness and responsiveness for long-running kernel work.
On real systems, drivers often account for more code than the core kernel because there are many devices, many features, and often poorly documented protocols.
- Programmed I/O: The driver moves data by explicitly reading and writing device registers. This is simple, but too slow for high-rate devices.
- DMA: The device transfers data directly to and from RAM. The driver prepares buffers in memory and kicks off the transfer with a control-register write.
- Interrupt mitigation and polling:
- High-speed devices often batch work and raise one interrupt for many completions.
- Under heavy load, drivers may disable interrupts and poll device state instead.
- Some systems switch dynamically between polling and interrupts based on load.
For very fast devices, copying data first into a kernel buffer and then into user space can be too expensive. Real systems may use zero-copy techniques, often built around DMA, to move data more directly between devices and user buffers. When applications need device-specific controls that do not fit read and write, Unix systems expose them through ioctl.
Xv6 is also unsuitable for hard or soft real-time work. Its scheduler is too simple, and some kernel paths keep interrupts disabled for too long to guarantee bounded response times.
PLIC Block Diagram
--- config: layout: dagre --- flowchart LR 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 subgraph DEV["external interrupt sources"] direction TB UART_HW["UART hardware<br/><br/>UART0_IRQ = 10"]:::source VIRTIO_HW["virtio disk hardware<br/><br/>VIRTIO0_IRQ = 1"]:::source end subgraph SETUP["PLIC setup interface"] direction TB PLICINIT["plicinit()<br/><br/>set device priorities"]:::iface PLICINITHART["plicinithart()<br/><br/>enable device IRQs for this hart<br/>set threshold = 0"]:::iface end subgraph PLIC_BOX["PLIC hardware state"] direction TB PLIC_STATE["PLIC<br/><br/>tracks pending external IRQs<br/>checks priority / enable / threshold<br/>routes IRQ to a hart<br/>supports claim + complete"]:::source end subgraph TRAP["trap-side interface"] direction TB CPU["RISC-V hart<br/><br/>supervisor external interrupt<br/>scause = external interrupt"]:::process DEVINTR["devintr()<br/><br/>classify interrupt<br/>external device / timer / unknown"]:::iface CLAIM["plic_claim()<br/><br/>read claimed IRQ number"]:::iface COMPLETE["plic_complete(irq)<br/><br/>mark IRQ as handled"]:::iface end subgraph BACKEND["driver backend after IRQ dispatch"] direction TB UARTINTR["uartintr()<br/><br/>console interrupt handler"]:::backend VIRTIOINTR["virtio_disk_intr()<br/><br/>disk interrupt handler"]:::backend UNKNOWN["unexpected irq<br/><br/>print warning"]:::backend end subgraph NONPLIC["not PLIC path"] direction TB TIMER["supervisor timer interrupt<br/><br/>clockintr()<br/>does not use PLIC"]:::backend end PLICINIT -->|"configures priorities"| PLIC_STATE PLICINITHART -->|"configures per-hart enables"| PLIC_STATE UART_HW -->|"raises IRQ 10"| PLIC_STATE VIRTIO_HW -->|"raises IRQ 1"| PLIC_STATE PLIC_STATE -->|"delivers external interrupt"| CPU CPU -->|"trap enters kernel"| DEVINTR DEVINTR -->|"external interrupt branch"| CLAIM CLAIM -->|"reads claim register"| PLIC_STATE CLAIM -->|"irq == UART0_IRQ"| UARTINTR CLAIM -->|"irq == VIRTIO0_IRQ"| VIRTIOINTR CLAIM -->|"other nonzero irq"| UNKNOWN UARTINTR -->|"handled"| COMPLETE VIRTIOINTR -->|"handled"| COMPLETE UNKNOWN -->|"handled / reported"| COMPLETE COMPLETE -->|"writes completion"| PLIC_STATE DEVINTR -.->|"timer branch bypasses PLIC"| TIMER