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:

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

Practice

  1. In xv6's trap.c, what does lapiceoi() do after a keyboard or UART interrupt is handled?
  2. Both kbdintr() and uartintr() call consoleintr(), but with different arguments. What differs between the two calls?
  3. The CGA frame buffer in xv6 is accessed via static ushort *crt = (ushort*)P2V(0xb8000). Why is ushort (2 bytes) used rather than char (1 byte) for each element?
  4. When a user presses Backspace, consputc sends '\b', ' ', '\b' to the UART instead of a single '\b'. Why?
  5. Explain why consputc() writes to both uartputc() and cgaputc() for every character, rather than writing to just one of them.
  6. What is the formula xv6 uses to compute the linear index pos into the CGA frame buffer from a row and column?
  7. Describe the call chain that executes when a user program calls ioctl(fd, request, arg) in xv6, naming each function and its role.
  8. In xv6, the CGA cursor position is read and written via I/O port 0x3D4 (CRTPORT) using an indirect register scheme. What are the two register numbers used for the cursor?
  9. Pressing Ctrl-P in an xv6 console triggers procdump(). Trace the exact path from the hardware interrupt to the procdump() call, naming every function along the way.
  10. Why does Unix (and xv6) treat devices as files? What concrete benefit does this give to user programs and to the kernel?