Interrupt Handling and Device Drivers
Why This Matters
Every keystroke you type and every character printed to the terminal flows through a chain: hardware interrupt → trap handler → device driver → output buffer → display hardware. Understanding this chain tells you how an operating system turns raw hardware signals into the structured I/O abstraction programs use. In xv6 this chain is short enough to read in full, making it ideal for learning the principles that apply to every OS.
From Hardware Interrupt to Driver
When a key is pressed or a byte arrives on the serial port, the hardware fires an interrupt. In xv6 that arrives at trap() in trap.c via the IDT. Two cases in the big switch statement route I/O device interrupts:
case T_IRQ0 + IRQ_KBD:
kbdintr();
lapiceoi();
break;
case T_IRQ0 + IRQ_COM1:
uartintr();
lapiceoi();
break;
T_IRQ0 is the base vector for hardware IRQs. IRQ_KBD (1) and IRQ_COM1 (4) are the standard PC IRQ numbers for the keyboard and first serial port.
After the driver function runs, lapiceoi() sends an End-Of-Interrupt signal to the local APIC, telling the interrupt controller that the interrupt has been serviced and new interrupts may be delivered.
Both kbdintr() and uartintr() ultimately call consoleintr(), passing it a function pointer getc that reads one character from the device:
void consoleintr(int (*getc)(void))
{
while ((c = getc()) >= 0) {
switch (c) {
case C('P'): // Ctrl-P: print process list
procdump();
break;
// ...
default:
consputc(c);
}
}
}
consoleintr reads characters in a loop until the device has no more, handles special control sequences (e.g., Ctrl-P triggers procdump()), and echoes normal characters back to the screen via consputc().
Writing to the Screen: consputc
consputc() is the single function responsible for putting a character on the screen. It fans out to two output paths:
void consputc(int c)
{
if (c == BACKSPACE) {
uartputc('\b'); uartputc(' '); uartputc('\b');
} else {
uartputc(c);
}
cgaputc(c);
}
| Path | Purpose |
|---|---|
uartputc(c) |
Sends the character to the serial port (COM1). Useful when the machine is headless or you're using a terminal emulator. |
cgaputc(c) |
Writes the character to the CGA text-mode display buffer. |
Backspace is special: the UART receives '\b' ' ' '\b' (move back, overwrite with space, move back again) to erase the previous character visually.
CGA: Memory-Mapped Display Hardware
The Color Graphics Adapter (CGA) was introduced by IBM in 1981. xv6 uses its 80×25 text mode, which is still universally supported in x86 emulators and real hardware as a compatibility fallback.
The Frame Buffer
The CGA frame buffer lives at physical address 0xB8000. xv6 maps it into the kernel virtual address space:
// console.c
static ushort *crt = (ushort *)P2V(0xb8000);
Each character cell is 2 bytes:
- Low byte: ASCII character code
- High byte: color attribute (foreground/background)
xv6 uses color 0x07 (gray text on black background):
crt[pos++] = (c & 0xff) | 0x0700; // gray on black
Reading and Writing the Cursor Position
The CGA controller exposes an I/O port (CRTPORT, 0x3D4) for indirect register access. To read the current cursor position, xv6 reads two 8-bit registers (high byte and low byte of the 16-bit position):
outb(CRTPORT, 14); // select register 14 (cursor high)
pos = inb(CRTPORT + 1) << 8; // read high byte
outb(CRTPORT, 15); // select register 15 (cursor low)
pos |= inb(CRTPORT + 1); // read low byte
The position is encoded as col + 80 * row — a flat index into the 2000-cell (80×25) grid. After writing a character, xv6 writes the updated position back to the same registers to move the cursor.
"Everything Is a File": ioctl and Device Drivers
A core Unix design principle is that devices are accessed through the same interface as files. In xv6 (and Linux), you open(), read(), write(), and close() devices just like regular files.
The ioctl() system call handles device-specific operations that don't fit the generic read/write model — things like changing terminal color, setting baud rate, or querying window size.
The ioctl Call Chain
ioctl(fd, request, arg)
└─ sys_ioctl() [syscall entry in sysfile.c]
└─ fileioctl() [dispatches by file type]
└─ consoleioctl() [console-specific logic]
For hw4, prettyprint.c uses:
ioctl(1, 0, 3); // set something on stdout (fd 1)
ioctl(2, 0, (color % 0xe) + 1); // set color on stderr (fd 2)
The consoleioctl() function receives the file descriptor's underlying device pointer and interprets the request and argument to change console behavior (e.g., setting the text color attribute used in cgaputc()).
Why This Architecture?
By routing device control through the file descriptor layer (fileioctl), the kernel keeps device-specific code isolated in driver files (console.c, uart.c). The rest of the kernel and all user programs never need to know which physical device backs a file descriptor — they just call ioctl().
The Full Data Flow: Keypress to Screen
[Keyboard hardware]
│ IRQ 1
▼
trap() in trap.c
│ T_IRQ0 + IRQ_KBD
▼
kbdintr()
│
▼
consoleintr(kbdgetc)
│ reads from keyboard controller
▼
consputc(c)
├──► uartputc(c) → serial port (COM1)
└──► cgaputc(c) → write to crt[pos] at 0xB8000
The same consoleintr function serves both the keyboard and UART — only the getc function pointer differs.
Key Takeaways
- Interrupt routing: Device IRQs arrive at
trap(), which dispatches to the appropriate driver function (kbdintr,uartintr) and then callslapiceoi()to acknowledge the interrupt. - consoleintr is a shared interrupt handler that reads characters from whichever device fired and handles control sequences or echoes characters.
- CGA text mode uses a memory-mapped frame buffer at
0xB8000; each 2-byte cell holds an ASCII code and a color attribute. The cursor position is managed through I/O port registers. - consputc writes to both the UART and the CGA display in parallel, so output appears on-screen and is also available over the serial line.
- ioctl extends the "everything is a file" model to device-specific control:
sys_ioctl → fileioctl → consoleioctlroutes the call to the right driver without exposing device details to the rest of the kernel.