2 · The Linux platform Draft
Interrupts and deferred work
On this page
2.5 Sharing the devices: interrupts
The second promise is that the devices are everybody’s. A network card receives packets for a hundred processes, a disk serves a thousand files, and no process may touch either directly. The kernel’s drivers do, and the question this section answers is how a driver finds out that its device has something to say, because the answer is the second of the three ways the kernel takes a core from a thread.
Why a kernel interrupts
A device with something to say, a packet in the NIC’s ring, a block read finished, the local timer, raises an interrupt. The core stops the running thread between two instructions, saves enough state to resume it, switches to the kernel and runs the handler that driver registered; when the handler returns, the thread resumes, unaware. The alternative, every driver checking its device all the time, would burn the CPU of a mostly idle machine on devices that mostly have nothing to say, so a general-purpose kernel is interrupt-driven. The design is right for a machine with a hundred devices and nothing urgent. A program with one device that matters and a core to spare can do the opposite and check the device itself.
Each interrupt line is steered to one core or a set of them, its affinity, written in /proc/irq/<n>/smp_affinity_list; a daemon, irqbalance, spreads the lines across cores at boot and as load changes, so that no core takes all of them. On a stock box your thread’s core is as likely a target as any other.
Top halves and bottom halves
The handler runs with interrupts off on that core, so it must be short: acknowledge the device, note what arrived, get out. That is the top half. The real work is deferred to a bottom half, and for the network the real work is the whole receive path, walking each packet through Ethernet, IP and TCP to the right socket’s queue. It runs as a softirq, one of ten fixed kinds of deferred kernel work (NET_RX, NET_TX, TIMER, RCU, SCHED among them) that the kernel runs on the same core as soon as the top half returns, with interrupts enabled, still in kernel mode, still through your caches. /proc/softirqs counts them per core.
A busy NIC raises more softirq work than the kernel is willing to run in one go. When a softirq keeps re-arming past a small budget, the kernel hands the backlog to ksoftirqd/<n>, the per-core thread from the previous section whose only job is to run softirqs, and from that moment the network stack is an ordinary runnable thread on that core’s run queue, competing with yours under the fair class. The modern driver model, NAPI, has the driver switch its interrupt off under load and let the NET_RX softirq poll the ring instead, up to 64 packets a pass: fewer interrupts, not none.
Cores interrupt each other as well. The scheduler’s reschedule request is one. A function-call interrupt (CAL), used when one core needs every other core to run a short piece of code now, is another. The TLB shootdown (TLB), is the one your own process is most likely to cause. /proc/interrupts has a row for each, and the local timer row, LOC, is the tick: the clock is a device like any other, and the tick is its interrupt.
What it costs
A device interrupt is 1–10 µs of the top half, the softirq and the return, and under load the softirq runs for hundreds of microseconds before it yields. Each one also leaves the kernel’s receive path in your L1 and the predictor trained on its branches. A core that takes the NIC’s interrupts is a core that stops, for a few microseconds, every time a packet arrives for anyone on the box; at 100,000 packets a second that is a tenth of the core, in pieces too small to see in a CPU graph and large enough to fill a histogram’s tail. Which cores take which interrupts is decided by the steering, not by the program, so on a stock box a thread’s tail includes every device whose line happened to be steered to its core.
Seeing it
/proc/interrupts is the whole story, one row per source and one column per core. Take the column for your thread’s core, wait ten seconds, take it again: every row that moved is a device, a clock or another core that stopped the thread. /proc/softirqs says what kind of deferred work followed. On a stock box the NIC rows move on whichever cores irqbalance chose, and LOC moves on every busy core.