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.
Console Input Mechanism
The console driver processes human input via UART serial-port hardware, interacting with an emulated 16550 chip.
UART hardware is exposed as memory-mapped control registers starting at physical address UART0 (0x10000000).
LSR (Line Status Register): Contains flag bits indicating if unread characters are waiting.
RHR (Receive Holding Register): Provides the waiting character; reading it triggers the hardware to dequeue the byte from its internal FIFO.
THR (Transmit Holding Register): Accepts software bytes for hardware transmission.
Initialization (consoleinit) configures the UART to raise distinct interrupts upon receiving a byte and upon completing a byte transmission.
Input execution flow bridges the hardware and the reading process:
A process invokes read, entering consoleread, which sleeps waiting for an accumulated line in cons.buf.
Hardware receives a character and raises an interrupt, entering the trap handler.
devintr queries the PLIC (Platform-Level Interrupt Controller) to identify the device and routes execution to uartintr.
uartintr extracts the character from the hardware and passes it to consoleintr.
consoleintr buffers characters in cons.buf and processes special sequences (e.g., backspace).
Upon detecting a newline, consoleintr wakes the sleeping consoleread thread, which then copies the buffered line to user space.
While console input halts a process until external data arrives, console output demonstrates how buffering can unblock processes before slow hardware finishes its work.
Console Output Mechanism
A write system call targeted at the console routes to the uartputc function.
uartputc appends outgoing characters to a driver-maintained buffer (uart_tx_buf) and invokes uartstart to initiate hardware transmission.
The writing process returns immediately without waiting for transmission, yielding to the sleep state only if uart_tx_buf is entirely full.
When the UART hardware finishes transmitting a byte, it raises a transmit-complete interrupt:
The interrupt invokes uartintr, which routes back to uartstart.
uartstart verifies the hardware’s ready state and feeds the next buffered byte into the THR.
This buffering architecture achieves I/O concurrency, decoupling high-speed process execution from slow hardware transmission constraints.
Decoupling processes from asynchronous hardware interrupts necessitates robust synchronization to maintain data integrity across shared buffers.
Concurrency and Driver Synchronization
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 handlers operate outside the context of the interrupted process.
Handlers cannot rely on process-specific state. For instance, invoking copyout is unsafe because the active page table belongs to the arbitrarily interrupted process.
Bottom-half handler logic is strictly minimized to moving data and signaling wakeups, deferring complex memory operations to the awakened top-half thread.
While device drivers utilize standard supervisor-mode traps for hardware management, the system’s core timekeeping demands specialized, higher-privilege interrupt routing.
Timer Interrupts and Machine Mode
Periodic timer interrupts drive the system clock and enforce preemptive thread scheduling via yield.
RISC-V architecture mandates that timer interrupts trap into machine mode, executing with full privileges and without virtual memory paging.
Timer initialization occurs in start.c before standard kernel execution:
Programs the Core-Local Interruptor (CLINT) hardware to trigger at a defined interval.
Establishes a dedicated scratch memory area to preserve register state independent of process trap frames.
Configures the mtvec register to point to timervec and enables machine-mode interrupts.
Timer interrupts execute without disturbing supervisor-mode kernel code:
timervec saves critical registers to the machine-mode scratch area.
Programs the CLINT for the subsequent timer interval.
Triggers a RISC-V software interrupt and executes an immediate return.
The hardware delivers the software interrupt to supervisor mode via the standard trap mechanism (devintr), safely invoking the kernel scheduler without violating machine-mode isolation.
The delegation of timer events via software interrupts exemplifies one specific hardware interaction model; varied hardware interfaces require distinct optimization strategies.
Real-World Driver Optimization Techniques
Programmed I/O: The driver explicitly reads and writes hardware control registers byte-by-byte (as seen in the UART driver). This is structurally simple but incurs massive CPU overhead at high data rates.
Direct Memory Access (DMA): Hardware reads and writes directly to physical RAM. The driver populates memory buffers and issues a single control command to trigger bulk data transfer. DMA is standard for modern disk and network controllers.
Interrupt Mitigation vs. Polling:
Interrupts carry high CPU context-switch overhead, making them detrimental for continuous, high-speed data streams.
High-throughput drivers batch events, raising a single interrupt for an entire queue of completed requests.
Under heavy load, drivers disable interrupts entirely and switch to polling (periodically reading device status registers). Dynamic switching between polling and interrupts optimizes CPU utilization based on real-time load.
Zero-copy architectures: Eliminate the CPU overhead of moving data between the kernel buffer and user space by directly mapping hardware DMA targets into user-space memory.
Out-of-band control: Operations that violate standard byte-stream read/write semantics (e.g., configuring baud rates) are managed via the ioctl system call interface.
Real-time execution: Standard kernel architectures cannot guarantee hard real-time bounded response limits due to non-deterministic scheduling and extended critical sections where interrupts are globally disabled.