|
AquilaOS documentation
How it works, and its code
|
An interrupt makes the CPU stop what it is running and jump to a handler, then return to it. Devices use interrupts to say "something happened" (a key was pressed, a packet arrived, a disk request finished), the CPU uses them to report faults (a page that isn't mapped, a division by zero), the timer uses one to give the scheduler a chance to switch threads, and programs use one to call the kernel.
How an interrupt reaches the kernel is the machine's business: every CPU does it its own way, and every machine has its own interrupt controllers. What the kernel does with it is not. This document starts with the kernel's side, the same on every machine, then describes how x86 and the PC, and aarch64 and QEMU's virt machine, bring interrupts to it (see The kernel and the machine for the split).
To the kernel, an interrupt is one of four things, each with its own entry into the kernel:
| Event | Comes from | The kernel's entry |
|---|---|---|
| a system call | the running program, on purpose | its handler in syscall_table[] (see System calls) |
| a fault | the running program, by accident | a page fault: mm_page_fault(); anything else: a signal |
| a device interrupt | a device | the handlers its driver installed |
| the tick | the timer | sched_timer_tick(), sched_cpu_tick() |
The architecture catches each one, saves what was running, works out which it is, and calls the kernel. The kernel never sees vectors, error codes or controllers: it gets "a write to this address from user mode faulted", or "IRQ 11 came".
The central decision: the kernel runs with interrupts disabled. When an interrupt or a system call enters the kernel, nothing else interrupts it until it returns to user mode or goes to sleep. The only place interrupts are allowed in the kernel is the idle loop, which waits for the next one.
This is what keeps the kernel simple. A system call never finds a data structure half-changed by an interrupt handler, so the kernel needs no locks against its own interrupts, and code is never preempted in the middle. The price is latency: an interrupt that comes while the kernel runs waits until the kernel is done. AquilaOS's system calls are short or sleep, so it waits little. Big kernels (Linux, the BSDs) made the opposite choice long ago, and pay for it with locks everywhere.
With several CPUs, the same promise is kept by the kernel lock: one CPU at a time runs in the kernel. An interrupt from user mode or from the idle loop takes the lock on its way in (kernel_enter()) and lets it go on its way out. Device interrupts all go to the boot CPU. See SMP and the kernel lock.
Devices raise numbered IRQs. A driver installs a handler for its IRQ, irq_install(irq, fn, arg) (dev/irq.h, each architecture's): fn(arg) runs when it comes, arg saying which device (two ATA channels, two serial ports, one handler). Several handlers can share an IRQ (PCI devices share lines), so every handler checks its device and does nothing if the interrupt wasn't its. The IRQ's number is the platform's; a driver gets it from the platform's table of devices, its bus, or an MSI it was given.
A handler does as little as it can: it takes the data off the device (a received packet, a key), acknowledges it, and wakes the threads waiting for it (waitq_wakeup(), which makes them ready with sched_thread_wake()). The work that takes time (TCP's processing, a filesystem's) happens in those threads, with interrupts off as anywhere else in the kernel, but no longer in the handler.
An interrupt is where threads switch without asking to. After the handlers ran:
On the way back to user mode, the signals that came meanwhile are delivered (signal_take(), see Signals).
The scheduler's tick is an interrupt every 2 ms, on every CPU (arch_sched_init() sets it up). On each one the tick charges the running process its time (sched_cpu_tick()), and the architecture asks for a switch, which happens once the interrupt is acknowledged: a time slice is one tick. The boot CPU's also moves the clock on (the architecture's), runs the timeouts (callouts), wakes the threads whose sleep or wait is over, and sends SIGALRM to the processes whose timer expired (sched_timer_tick()).
With several CPUs, one sometimes has to make another do something now: flush its TLB after page tables changed, or enter the kernel because a thread became ready for it or one of its threads was killed. The kernel asks with tlb_shootdown() and arch_cpu_kick(); the architecture sends an inter-processor interrupt (IPI).
A page fault goes to the memory manager, mm_page_fault(), which maps the page if the address is part of the process's memory (demand paging, copy on write; see Memory). If it isn't:
Any other fault from user mode is a signal to the process (a division by zero is SIGFPE, an invalid instruction SIGILL); from the kernel, a panic.
On x86 every interrupt has a number, its vector, from 0 to 255. The first 32 are the CPU's own exceptions (14 is a page fault, 13 a general protection fault, and so on); the rest are free for the system to use. The Interrupt Descriptor Table (IDT) has an entry, a gate, for each vector: the address of its handler, the code segment to run it in, and the privilege level allowed to raise it with an int instruction.
AquilaOS uses interrupt gates everywhere: the CPU clears the interrupt flag (IF) when it enters through one, which is how the kernel runs with interrupts off. The system call gate (vector 0x80) is the only one user mode may raise itself.
AquilaOS's vectors:
| Vector | What |
|---|---|
| 0 – 31 | CPU exceptions |
| 32 – 79 | device interrupts, IRQ 0 to 47 (vector = 32 + IRQ) |
| 0x80 | system calls (int $0x80) |
| 0xF0 | the local APIC's timer (the tick) |
| 0xF1 | IPI: flush the TLB |
| 0xF2 | IPI: wake up and enter the kernel (arch_cpu_kick()) |
| 0xFF | the local APIC's spurious interrupt |
The IDT is one table, shared by every CPU; it is filled in by x86_isr_setup() (exceptions, the system call) and x86_irq_gates_setup() (the rest), and loaded on each CPU by x86_idt_setup().
When an interrupt comes, the CPU pushes the interrupted context's instruction pointer, code segment and flags; if it came from user mode it first switches to the kernel stack named in the task state segment (TSS) and also pushes the user stack pointer and stack segment. Some exceptions push an error code after those (a page fault's says whether it was a read or a write, from user or kernel mode). Then it jumps to the gate's handler. iret undoes it.
Every vector's gate points to a small stub in cpu/cpu.S. The stubs make every interrupt look the same to C:
The C handlers are __x86_isr() for exceptions and system calls (cpu/isr.c) and __x86_irq_handler() for device interrupts and IPIs (platform/misc/irq.c).
__x86_isr() turns the CPU's exceptions into the kernel's events:
__x86_irq_handler() handles every vector from 32 on:
A thread switch happens inside the handler, on the interrupted thread's kernel stack: the thread's registers are saved there, and the stack is left as it is until the thread runs again and returns through the rest of the handler. Because the controller was told it's done before the switch, other interrupts keep coming meanwhile.
The number of times each IRQ came is kept, and /proc/interrupts shows them with the controller and each IRQ's route (with several CPUs, a last line, tlb:, counts each CPU's TLB flushes for another):
Devices don't talk to the CPU directly: an interrupt controller takes their interrupt lines and turns them into vectors. PCs have had two kinds, and AquilaOS supports both behind one interface, struct x86_irq_chip:
An IRQ is unmasked when its first handler is installed, masked when its handlers are removed, and acknowledged after its handlers ran. Which controller is used is chosen at boot (x86_pc_irq_init(), platform/pc/init.c).
The original PC's controller, two 8259s chained together, has 16 lines: the ISA IRQs, 0 to 15 (0 the PIT, 1 the keyboard, 4 the first serial port, 12 the PS/2 mouse, 14 and 15 the ATA channels). The second 8259 is connected to the first one's line 2.
At boot the 8259s send their interrupts to vectors 8 to 15 and 0x70 to 0x77, which collide with the CPU's exceptions. AquilaOS remaps them to 32–47 (platform/misc/pic.c), so that IRQ n is vector 32 + n. An EOI is a command to the 8259 that raised it (both, for the second one's lines).
The 8259 is used when there is no APIC, or when the kernel is booted with apic=off. With the APIC, the 8259s are remapped anyway (a stray interrupt from one lands on a harmless vector) and then masked.
Every x86 CPU since the Pentium has a local APIC, and chipsets have one or more I/O APICs (platform/misc/apic.c).
Where the APICs are, and how the ISA IRQs map to pins, come from ACPI's MADT table (signature APIC). By default ISA IRQ n is GSI n, but the MADT has interrupt source overrides for the exceptions: on most machines the PIT's IRQ 0 is wired to pin 2, and some lines are level-triggered. x86_apic_setup() reads the table, enables the local APIC, masks every pin, and installs the APIC as the system's x86_irq_chip. From then on IRQ n is the pin the MADT says it is (apic_route()), sent to the boot CPU with vector 32 + n. A PCI device's IRQ is its configuration space's interrupt line, an ISA IRQ number, which QEMU's chipsets wire to the matching pins (level-triggered, as the MADT's overrides say). A machine whose PCI lines are only on the I/O APIC's pins past 15 would need ACPI's _PRT table, which AquilaOS doesn't read; a device with MSI (below) doesn't need it.
A PCI device that supports message signalled interrupts doesn't need a line at all: it raises an interrupt by writing a value (the data) to an address (the message address), which the local APIC receives. The address names the CPU, the data the vector.
MSI is PCI's, so the kernel's side is generic: pci_msi_enable() (dev/pci/pci.c) asks the architecture for an IRQ and its message (arch_msi_alloc()), writes them to the device's MSI capability, and turns the device's line off. On the PC, the IRQs for messages are allocated above the I/O APICs' pins (x86_irq_alloc()), each with a controller of its own, msi_chip, whose EOI is the local APIC's. The e1000 driver uses MSI when the device has it (QEMU's e1000e) and its line otherwise (e1000). With the 8259 there are no messages: arch_msi_alloc() fails and the driver falls back to the line.
The tick comes from (platform_timer_setup(), in platform/pc/init.c, called by sys/sched.c):
The clock, arch_rtime_ns(), is chosen before the tick starts (x86_clock_init(), sys/clock.c): the TSC if the CPU says it's invariant (calibrated against the HPET, or the PIT), else the HPET's counter, else the ticks. clock=tsc, clock=hpet or clock=pit chooses one.
The local APIC sends the IPIs: 0xF1 to flush another CPU's TLB, 0xF2 for arch_cpu_kick() (mm/tlb.c, cpu/smp.c). The TLB flush is handled without the kernel lock: the CPU that sent it holds the lock while it waits for the others. A CPU spinning for the lock with interrupts off does the flush from its spin loop instead.
A local APIC that never gets its EOI blocks every vector of the same or lower priority, the CPU's own tick and IPIs included. So a device interrupt is always acknowledged, even when the thread it interrupted was killed meanwhile and frees itself on the way out.
On aarch64 the exceptions have no numbers: VBAR_EL1 points to a table of 16 entries (cpu/vectors.S), one for each kind (synchronous, IRQ, FIQ, SError) and each place it came from (the kernel, user mode, and two that AquilaOS doesn't use). IRQs are masked on entry (PSTATE.I), and stay masked in the kernel. Each entry saves the registers on the kernel stack, a struct aarch64_regs, and calls C (cpu/trap.c):
Both take the kernel lock on the way in (kernel_enter()). The other entries (an FIQ, an SError, an exception from aarch32) panic (aarch64_bad()).
QEMU's virt machine has ARM's GICv2 (platform/virt/gic.c). Its distributor takes the devices' interrupts (SPIs, from 32) and each CPU's own (PPIs, 16 to 31), and sends them to the CPUs' CPU interfaces. An IRQ is the GIC's interrupt ID; every one goes to the boot CPU, masked until its first handler is installed. irq_handle() (platform/virt/irq.c) reads GICC_IAR to learn which interrupt came, runs its handlers, and writes it to GICC_EOIR, before any thread switch. PCI devices interrupt on the host bridge's four lines (INTA# to INTD#); there is no MSI.
The tick is the virtual timer of ARM's generic timer, each CPU's own, on PPI 27 (platform/virt/timer.c). Its counter, CNTVCT_EL0, runs at CNTFRQ_EL0 from boot, so it is the clock too (arch_rtime_ns()): nothing needs calibrating. Each tick sets the next one, every 2 ms.
| File | Layer | What |
|---|---|---|
| kernel/sys/sched.c | kernel | the tick, switching, waking |
| kernel/mm/fault.c | kernel | mm_page_fault() |
| kernel/sys/signal.c | kernel | signals from faults |
| kernel/dev/pci/pci.c | kernel | MSI on PCI devices |
| kernel/arch/i386/cpu/idt.c | x86 | the IDT and its gates |
| kernel/arch/i386/cpu/cpu.S | x86 | the entry stubs, the context switch |
| kernel/arch/i386/cpu/isr.c | x86 | exceptions and the system call |
| kernel/arch/i386/cpu/uaccess.S | x86 | copies from and to user memory |
| kernel/arch/i386/sys/sched.c | x86 | the tick's handler |
| kernel/arch/i386/mm/tlb.c | x86 | the TLB flush IPI |
| kernel/arch/i386/platform/misc/irq.c | PC | IRQs: handlers, dispatch, /proc/interrupts |
| kernel/arch/i386/platform/misc/pic.c | PC | the 8259 PIC |
| kernel/arch/i386/platform/misc/apic.c | PC | the local and I/O APICs, MSI, IPIs |
| kernel/arch/i386/platform/misc/pit.c | PC | the PIT (8254) |
| kernel/arch/i386/platform/pc/init.c | PC | which controller and timer are used |
| kernel/arch/i386/sys/clock.c | x86 | the clock: the TSC, the HPET or the ticks |
| kernel/arch/i386/platform/misc/hpet.c | PC | the HPET |
| kernel/arch/aarch64/cpu/vectors.S | aarch64 | the exception vectors |
| kernel/arch/aarch64/cpu/trap.c | aarch64 | exceptions, the system call, interrupts |
| kernel/arch/aarch64/cpu/uaccess.S | aarch64 | copies from and to user memory |
| kernel/arch/aarch64/sys/sched.c | aarch64 | the tick's handler |
| kernel/arch/aarch64/platform/virt/gic.c | virt | the GIC |
| kernel/arch/aarch64/platform/virt/irq.c | virt | IRQs: handlers, dispatch, /proc/interrupts |
| kernel/arch/aarch64/platform/virt/timer.c | virt | the generic timer: the tick, the clock |