Input and output: polling, interrupts and DMA

Keyboards, screens, disks and network cards are far slower than the processor and run on their own time. Each device has a controller with a few registers: a data register, a status register ("ready?", "error?") and a command register.

The processor reaches those registers in one of two ways: special I/O instructions (IN and OUT on x86), or memory-mapped I/O, where the device registers have ordinary memory addresses and normal loads and stores reach them (used by ARM and RISC-V).

Three ways to wait for a device

Polling. The processor reads the status register again and again until the device is ready. Simple, but every check is wasted work if the device is slow.

Interrupts. The processor gets on with other work. When the device is ready it raises an interrupt signal, and the processor:

  1. finishes the current instruction;
  2. saves the PC and its state (flags, some registers);
  3. looks up the interrupt vector table to find the handler for this device, and runs it;
  4. restores the state and returns to where it was.

DMA (direct memory access). For big transfers (a disk block, a network packet), even one interrupt per byte is too many. A DMA controller copies the whole block between the device and memory by itself and interrupts the processor once, at the end.

device_ready_at = [30, 75, 120]        # when a slow device has data, in microseconds
check_cost, handler_cost = 1, 3        # microseconds per status check, per interrupt

wasted = 0
t = 0
for ready in device_ready_at:          # polling: keep checking until ready
    while t < ready:
        t += check_cost
        wasted += check_cost
print("polling: CPU time spent checking:", wasted, "microseconds")
print("interrupts: CPU time spent:", handler_cost * len(device_ready_at), "microseconds")
Open in compiler
def timer():
    return "switch to the next program"

def keyboard():
    return "read the key code"

vectors = {0: timer, 1: keyboard}      # interrupt number -> handler

pc = 1200
for number in [1, 0]:
    saved = pc                         # save the state
    print("interrupt", number, "at PC", pc, "->", vectors[number]())
    pc = saved                         # restore it and carry on
Open in compiler

Interrupts have priorities: a timer or power failure outranks a keyboard. The timer interrupt is what lets an operating system switch between programs (see the Operating Systems course).

MethodThe CPU's work per transferBest for
Pollingconstant checkingvery fast devices, or very simple systems
Interruptsone handler call per eventkeyboards, mice, small transfers
DMAone interrupt per blockdisks, network cards, graphics

Common mistakes

  • Thinking an interrupt stops the current instruction in the middle: the processor finishes it first.
  • Forgetting to save the state before running the handler: the interrupted program would then continue with wrong values.
  • Thinking DMA means no interrupt at all: there is still one, when the transfer ends.

Quick check

1. What does the processor do while polling?
2. Where does the processor find the address of an interrupt handler?
3. Which method suits copying a 4 KB disk block?
4. What must happen before an interrupt handler runs?
My notes

Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.