COMP 3150Operating Systems

01 Introduction

Lesson 11.1 presented the I/O subsystem's hardware and its three data-transfer mechanisms. Programmed I/O needs no special mechanism beyond the processor's own load and store instructions, and polling needs only a status register. But interrupt-driven I/O and DMA both depend on a mechanism that lets a device signal the processor when it has completed a command — the interrupt. Without interrupts, interrupt-driven I/O would be polling, and DMA would require the processor to poll the DMA controller's status register. The interrupt is what makes the asynchronous I/O model possible, and it is the subject of this lesson.

An interrupt is an event that causes the processor to suspend its current instruction stream, save its state, and transfer control to a designated handler. The event may come from a hardware device — a disk controller that has finished a read, a network adapter that has received a packet, a timer that has reached its deadline — or it may come from the processor itself, in which case it is usually called an exception or a trap: a division by zero, a page fault, a system call, a breakpoint. The two kinds of event share a mechanism — the processor's response is the same — but they differ in their origin and in their handling: a hardware interrupt is asynchronous (it can arrive at any time, between any two instructions), while an exception is synchronous (it is caused by the instruction that is currently executing).

The interrupt mechanism's design is one of the operating system's most consequential decisions. A naive design — a single handler for all interrupts, a long handler that does all the work — would make the system's response time unpredictable and its throughput poor, because a long handler blocks every other interrupt and every other device. The modern design splits the handling into two halves: a short top half (the hardware interrupt handler) that acknowledges the device and schedules the work, and a longer bottom half (the deferred work) that performs the work at a safe time. The split is the kernel's answer to the tension between responsiveness and throughput, and it is the lesson's central idea.

This lesson presents the interrupt mechanism. It defines the interrupt and distinguishes it from the exception. It presents the hardware that delivers the interrupt — the interrupt request lines, the interrupt controller (the PIC, the APIC, and the GIC), and the message-signaled interrupts — and the interrupt vector table (the IDT) that the processor uses to find the handler. It presents the interrupt's priority and masking, the handler's structure, and the interrupt latency that the mechanism imposes. It presents the top-half and bottom-half split in detail, with the Linux kernel's softirqs, tasklets, and workqueues as the concrete mechanisms. It presents threaded interrupts and interrupt coalescing, and it closes with the interrupt's role in the modern kernel's concurrency model.

Prerequisites

This lesson builds on Lesson 11.1 and on the process-management unit. The I/O subsystem's layers, the device controller and its registers, and the three data-transfer mechanisms from Lesson 11.1 are assumed. The process concept, the process control block, and the context switch from Unit 3 (Lessons 3.1–3.5) are the basis for the discussion of the interrupt's effect on the current process, and the kernel-mode and user-mode distinction from Lesson 1.4 is the basis for the discussion of the handler's privilege. No new operating-system concepts are introduced; the lesson is about the interrupt mechanism and the kernel's handling of it. Familiarity with the Linux command line from Lesson 2.2 is helpful, since the interactive activity uses standard Unix tools.

02 Learning objectives

By the end of this lesson you will be able to

  • Define the interrupt and distinguish it from exceptions, traps, faults, and aborts.
  • Describe the hardware path of a hardware interrupt: the interrupt request line, the interrupt controller, and the interrupt vector.
  • Explain the interrupt descriptor table and how the processor finds the handler for a given interrupt.
  • Describe interrupt priority, masking, and nesting, and explain why the processor needs to control them.
  • Explain the top-half and bottom-half split and describe Linux's softirqs, tasklets, and workqueues.
  • Describe threaded interrupts, message-signaled interrupts, and interrupt coalescing, and explain the trade-offs each introduces.

This lesson contributes to U11.2 and U11.3, and to course learning outcomes CLO1, CLO2, CLO5, and CLO6.

03 Required reading

  • Operating System Concepts — Chapter 1, Section 1.3.2, Interrupts, and Chapter 12, Section 12.2, I/O Hardware. The standard treatment of the interrupt mechanism, the interrupt cycle, and the hardware's role in delivering interrupts.
  • OSTEP — Chapter 36, I/O Devices, and the dialogue Interlude: Into the Land of the Interrupts. OSTEP's treatment of the interrupt's use in the canonical device protocol and its discussion of the interrupt's overhead are the clearest available accounts, and they are the basis for the worked example in this lesson.
  • Tanenbaum & Bos — Modern Operating Systems, Chapter 5, Section 5.1.4, Interrupts Revisited. A complementary treatment that develops the interrupt mechanism from the hardware's perspective.
  • Love, R. — Linux Kernel Development, 3rd ed., Chapter 7, Interrupts and Interrupt Handlers, and Chapter 8, Bottom Halves and Deferring Work. The Linux kernel's interrupt handling: the top half, the softirqs and tasklets, the workqueues, and the threaded interrupt handlers.
  • Bovet, D. P., & Cesati, M. — Understanding the Linux Kernel, 3rd ed. O'Reilly, 2005. Chapter 4, Interrupts and Exceptions. The x86 interrupt mechanism, the IDT, the do_IRQ handler, and the softirq infrastructure.
  • Corbet, J., Rubini, A., & Kroah-Hartman, G. — Linux Device Drivers, 3rd ed. O'Reilly, 2005. Chapter 10, Interrupt Handling. The driver's view of the interrupt: request_irq, the handler's rules, and the top-half and bottom-half split.
  • Patterson, D. A., & Hennessy, J. L. — Computer Organization and Design. The treatment of exceptions, interrupts, and the processor's response from the computer architecture perspective.
  • Intel Corporation. — Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 3, Chapter 6, Interrupt and Exception Handling. The authoritative reference for the x86 interrupt mechanism.
  • Linux man pages: man 5 proc (for /proc/interrupts and /proc/softirqs), man 1 watch, man 8 irqbalance, man 1 dmesg, man 2 sigaction (for the user-space signal mechanism, which is related but distinct), man 7 signal, man 1 trace-cmd, man 1 perf.

Reading strategy

Read the OSTEP interlude first. It develops the interrupt from the canonical device protocol and shows step by step why the interrupt is necessary and why its handling is split. Then read the Silberschatz section for the formal mechanism — the interrupt cycle, the interrupt vector, the priority and masking — and the Tanenbaum section for the hardware's perspective. The Linux kernel chapters (Love and Bovet & Cesati) are the practical view: they show how the kernel's interrupt handling is organized, why the top half and bottom half are separate, and how the deferred work is scheduled. The device-driver book (Corbet et al.) is the driver author's view: it shows the rules that a driver's interrupt handler must follow and the pitfalls of getting them wrong. Finally, the Intel manual is the authoritative reference for the x86 mechanism, and it is worth consulting for the IDT's layout and the exact semantics of the interrupt's entry and exit. Pay particular attention to the distinction between the hardware interrupt (the signal from the device's controller) and the software interrupt (the kernel's internal event mechanism, which includes the system call and the softirq). The two share a mechanism but serve different purposes, and the confusion between them is a common source of misunderstanding.

04 Core content

4.1 What an interrupt is — and what it is not

An interrupt is an event that causes the processor to suspend its current instruction stream, save its state, and transfer control to a designated handler. The event may come from a hardware device (a hardware interrupt) or from the processor itself (a software interrupt, exception, or trap). The processor's response to both kinds is the same: it saves the current instruction pointer and the flags, loads a new instruction pointer and flags from a table, and begins executing the handler. The difference is in the event's origin and in the handler's semantics.

The terminology is worth making precise, because the terms are often used loosely:

Hardware interrupt (asynchronous)
An event generated by a device's controller — a disk that has finished a read, a network adapter that has received a packet, a timer that has reached its deadline. It is asynchronous: it can arrive at any time, between any two instructions, and it is unrelated to the instruction that is currently executing. The processor's response is to suspend the current instruction stream and enter the device's handler.
Exception (synchronous)
An event generated by the processor in response to the instruction that is currently executing. It is synchronous: it is caused by the instruction, and it occurs at a well-defined point in the instruction stream. The x86 architecture divides exceptions into three kinds: faults (the instruction can be restarted after the handler corrects the cause — a page fault is the classic example), traps (the instruction completes and the handler runs after it — a breakpoint or a system call), and aborts (the instruction cannot be restarted and the program may have to be terminated — a hardware error or a double fault).
System call (a kind of trap)
The mechanism by which a user-mode program requests a service from the kernel. On x86, the program executes a special instruction (classically int 0x80; on modern 64-bit systems, syscall), which causes the processor to switch to kernel mode and enter the kernel's system-call handler. The system call is a synchronous event: it is caused by the instruction, and the kernel's handler knows which service is requested from the register state.
Software interrupt (softirq, Linux)
A kernel-internal event that the kernel raises to defer work from the hardware interrupt handler to a safer time. The softirq is not a hardware signal; it is a software flag and a function pointer that the kernel checks and invokes at designated points. The name is unfortunate — the softirq is unrelated to the x86 INT instruction — but it is the established Linux terminology.

The distinctions matter because the mechanisms' requirements differ. A hardware interrupt handler must be short (it blocks other interrupts while it runs) and must not sleep (it runs in a context where sleeping is not safe). An exception handler has more freedom: it is caused by the current instruction, so the kernel knows what the instruction was trying to do and can take corrective action (for example, a page fault handler can load the missing page and restart the instruction). A system call handler has even more freedom: it runs on behalf of a specific process, with a defined context, and it can sleep and be preempted. The kernel's design reflects the differences, and a handler that ignores them is a source of subtle bugs.

4.2 The hardware path of a hardware interrupt

A hardware interrupt's journey from the device to the processor's handler passes through several stages.

Stage 1: the device asserts its interrupt request line. When the device's controller completes a command (or needs attention), it asserts a signal on its interrupt request (IRQ) line. On legacy systems, each device has its own IRQ line, and the line's number identifies the device. On modern systems, the lines are virtual: the device sends a message (a message-signaled interrupt) rather than asserting a wire.

Stage 2: the interrupt controller receives the request. The interrupt controller is the hardware that collects the devices' requests, prioritizes them, and delivers them to the processor. The classical x86 interrupt controller is the 8259A PIC (Programmable Interrupt Controller), which has been replaced on modern systems by the APIC (Advanced Programmable Interrupt Controller). The APIC has two parts: the local APIC, which is built into each processor core and receives the interrupts directed at that core, and the I/O APIC, which collects the devices' requests and routes them to the appropriate local APIC. On ARM systems, the equivalent is the GIC (Generic Interrupt Controller).

Stage 3: the interrupt controller signals the processor. The local APIC asserts the processor's interrupt pin (or sends an interrupt message). The processor checks whether interrupts are enabled; if they are disabled (because the processor is in a critical section, or because the interrupt's priority is lower than the processor's current priority), the request is held pending until they are enabled. If they are enabled, the processor acknowledges the interrupt and asks the local APIC for the interrupt's vector — a small integer that identifies the interrupt.

Stage 4: the processor transfers control to the handler. The processor uses the vector as an index into the interrupt descriptor table (IDT), a table in memory whose entries describe the handler for each vector. The entry contains the handler's address, the code segment to use, and the privilege level at which the handler runs. The processor saves the current instruction pointer, the code segment, the flags, and (if a privilege change occurs) the stack pointer and the stack segment, then loads the new values from the IDT entry and begins executing the handler.

Stage 5: the handler runs. The handler identifies the device (if the vector is shared among several devices), acknowledges the interrupt to the interrupt controller, reads the device's status register, performs the urgent work (or schedules it for the bottom half), and returns.

Stage 6: the processor returns. The handler's return instruction (iret on x86) restores the saved instruction pointer, code segment, flags, and (if a privilege change occurred) the stack pointer and stack segment, and resumes the interrupted instruction stream.

The hardware path of a hardware interrupt A diagram showing the six stages of a hardware interrupt. A device asserts its interrupt request line to the I/O APIC, which routes the request to the processor's local APIC. The local APIC signals the processor, which acknowledges the interrupt and reads the vector. The processor uses the vector as an index into the interrupt descriptor table (IDT) to find the handler, saves the current context, and transfers control to the handler. The handler runs, acknowledges the interrupt, does the urgent work, and returns via an iret instruction that restores the saved context. THE HARDWARE PATH OF A HARDWARE INTERRUPT Device controller completes a command and asserts its IRQ line 1 I/O APIC collects and prioritizes the devices' requests 2 Local APIC built into each core; signals the processor 3 Processor acknowledges and reads the vector Interrupt Descriptor Table (IDT) A table in memory; one entry per vector. Each entry gives the handler's address, the code segment, and the privilege level. Vector 32: IRQ 0 (timer) Vector 33: IRQ 1 (keyboard) 4 Handler (top half) Acknowledge the interrupt Read the device's status register Do the urgent work, schedule the rest Context save and restore On entry, the processor saves: • the instruction pointer (RIP) • the code segment (CS) • the flags register (RFLAGS) • the stack pointer and segment (if a privilege change occurs) On exit, the iret instruction restores: • the saved values, in reverse order • the interrupted instruction stream resumes exactly where it stopped 5, 6 Deferred work (bottom half): softirqs, tasklets, workqueues The top half schedules the deferred work; the kernel runs it at a safe time, with interrupts enabled, so the device's long work does not block other interrupts. This split is the kernel's answer to the tension between responsiveness and throughput.
Figure 11.2 — The hardware path of a hardware interrupt. The device's controller asserts its IRQ line; the I/O APIC collects the request and routes it to the local APIC of the target core; the local APIC signals the processor, which acknowledges the interrupt and reads the vector; the processor uses the vector as an index into the interrupt descriptor table (IDT) to find the handler; the processor saves the current context and transfers control to the handler. The handler runs, does the urgent work, and schedules the rest for the bottom half. On return, the iret instruction restores the saved context and the interrupted instruction stream resumes.

4.3 The interrupt descriptor table

The interrupt descriptor table (IDT) is the table that maps an interrupt vector to its handler. It is the processor's dispatch table: when an interrupt occurs, the processor reads the vector, indexes the IDT, and loads the handler's address and the associated privilege level from the entry.

On x86, the IDT has 256 entries (vectors 0 through 255), each 16 bytes. The entries are of three kinds:

  • Task gates. A legacy mechanism for task switching, not used by modern operating systems.
  • Interrupt gates. The processor clears the interrupt-enable flag (IF) when it enters the handler through an interrupt gate, so the handler runs with interrupts disabled on the same processor. The clearing is automatic: the handler does not need to disable the interrupts itself. Most hardware interrupt handlers use interrupt gates.
  • Trap gates. The processor does not clear the interrupt-enable flag when it enters the handler through a trap gate, so the handler runs with interrupts enabled. The trap gate is used for exceptions and system calls, where the handler may legitimately be interrupted by a hardware interrupt.

The IDT's entries are set up by the kernel during its initialization. The vectors 0 through 31 are reserved for the processor's exceptions (divide error, page fault, double fault, and so on); the vectors 32 through 255 are available for the operating system, and the kernel uses them for the hardware interrupts (mapped through the interrupt controller) and for the system-call mechanism. The table's address is stored in the processor's IDTR register, which the kernel loads with the lidt instruction.

The IDT's design has two important consequences. First, the dispatch is a direct lookup: the processor does not search for the handler, it indexes the table, so the dispatch is O(1) and does not depend on the number of handlers. Second, the dispatch is a privileged operation: the IDT's entries specify the privilege level at which the handler runs, and the processor enforces the level when it transfers control. A user-mode program cannot install its own interrupt handler, because it cannot modify the IDT; it must request the kernel's services through the system-call mechanism, which the kernel has installed in the IDT's entries.

4.4 Interrupt priority, masking, and nesting

A system with many devices generates many interrupts, and the processor cannot handle them all simultaneously. The interrupt mechanism therefore provides three controls: priority, masking, and nesting.

Priority determines which interrupt is delivered when several are pending. The interrupt controller assigns each interrupt a priority, and when several requests are pending, the controller delivers the highest-priority one first. The processor's current priority (its interrupt priority level) is part of its state; an interrupt whose priority is lower than the processor's current level is held pending until the level drops. The mechanism is what allows a high-priority interrupt (a timer tick, a real-time event) to preempt a low-priority one (a slow device's completion).

Masking is the ability to disable interrupts selectively. The processor has a global interrupt-enable flag (IF on x86): when IF is clear, the processor ignores all maskable interrupts. The interrupt controller also has per-line masks: the kernel can disable a specific device's interrupt line without disabling the others. Masking is what makes the critical sections possible: a kernel code path that manipulates a data structure shared with an interrupt handler disables the relevant interrupts while it holds the structure, so the handler cannot observe the structure in an inconsistent state.

Nesting is the ability of a higher-priority interrupt to preempt a lower-priority handler. On x86, an interrupt gate clears IF when the handler is entered, so the handler runs with interrupts disabled and cannot be preempted by another maskable interrupt. A trap gate does not clear IF, so the handler can be preempted. The kernel chooses the gate type per vector: the hardware interrupt handlers use interrupt gates (they must not be preempted, to keep the handling short and to protect the data structures they share), and the system-call handler uses a trap gate (it may legitimately be interrupted, and it can be preempted by a hardware interrupt).

The three controls' interaction is the mechanism's subtle part. A kernel code path that disables interrupts for too long increases the system's interrupt latency — the time between the device's request and the handler's response — and the latency is what the device's driver must tolerate. A path that disables interrupts for a few instructions is acceptable; a path that disables them for milliseconds is a bug, because it can cause the device's buffer to overflow or the network's packets to be dropped. The kernel's design therefore keeps the interrupt-disabled regions short, and it uses finer-grained synchronization (spinlocks, per-CPU data) where possible.

4.5 The interrupt handler and the top-half / bottom-half split

The interrupt handler is the code that runs in response to an interrupt. Its design is constrained by three requirements:

  • It must be short. While the handler runs with interrupts disabled, no other interrupt can be delivered to the same processor, and the devices whose interrupts are blocked must wait. A long handler increases the system's latency and can cause devices to lose data.
  • It must not sleep. The handler runs in interrupt context, which is not associated with any process. Sleeping (blocking) in interrupt context would deadlock, because there is no process to wake and no scheduler state to save. The handler must use only non-blocking operations.
  • It must not access user memory. The handler runs on behalf of a device, not a process, so it cannot assume that the current process's address space is valid. The handler must use only kernel memory (or must use special mechanisms, such as the copy_to_user functions called from a context where the access is safe).

The three requirements create a tension: a device's work is often long, but the handler must be short. The kernel's resolution is the top-half / bottom-half split:

  • The top half is the hardware interrupt handler. It runs with interrupts disabled, and it does only the urgent work: acknowledging the interrupt to the controller, reading the device's status register, copying the data that must not be lost, and scheduling the rest of the work for the bottom half. The top half is short — typically a few dozen to a few hundred instructions — so the interrupt-disabled region is short and the system's latency is low.
  • The bottom half is the deferred work. It runs later, with interrupts enabled, at a time the kernel chooses. The bottom half can be long, can access data structures that the top half could not, and (in the workqueue case) can sleep. The bottom half's flexibility is what allows the device's driver to do substantial work without blocking the interrupts.

The split is the kernel's central interrupt-handling design, and it appears in every modern operating system. The names differ — Linux uses softirqs, tasklets, and workqueues; BSD uses ithreads and taskqueues; Windows uses DPC (deferred procedure calls) — but the idea is the same. The next section presents Linux's mechanisms in detail.

4.6 Linux's deferred-work mechanisms

Linux provides three mechanisms for deferred work, each with its own context and its own rules:

Softirqs
The lowest-level deferred-work mechanism. A softirq is a statically defined function that runs in softirq context: it is not associated with any process, it runs with interrupts enabled, and it can run concurrently on several processors (the same softirq can execute simultaneously on different CPUs, so the code must be reentrant or use per-CPU data). Softirqs are used for the highest-throughput deferred work — network processing, block I/O completion, timers — and they are defined at compile time. There are a small number of them (typically ten or so), and each has a fixed priority.
Tasklets
A wrapper around softirqs that provides a simpler interface. A tasklet is a dynamically registered function that runs in softirq context, but the kernel guarantees that a given tasklet runs on only one processor at a time — the same tasklet cannot execute concurrently on two CPUs. The guarantee makes tasklets easier to write than softirqs (the code does not need to be reentrant), but it limits their throughput. Tasklets are used for medium-throughput deferred work, particularly for device drivers.
Workqueues
A mechanism that defers work to a kernel thread. A workqueue is a queue of work items, each represented by a function and its arguments, and the kernel's worker threads execute the items one at a time. Because the work runs in process context (the worker thread's context), the work item can sleep — it can block, wait for I/O, take mutexes, and access user memory through the normal mechanisms. Workqueues are used for the lowest-throughput deferred work and for work that must sleep (for example, work that reads from a file or waits for a device).

The three mechanisms' contexts differ, and the differences determine which one a driver should use:

Linux's deferred-work mechanisms
Mechanism Context Can sleep? Concurrent on multiple CPUs? Typical use
Softirq Softirq (interrupt-like) No Yes (same softirq on different CPUs) Network, block I/O completion, timers
Tasklet Softirq (interrupt-like) No No (same tasklet serialized) Device drivers' medium-throughput work
Workqueue Process (kernel thread) Yes Yes (different worker threads) Work that must sleep, low-throughput work

The mechanisms' scheduling differs too. Softirqs are run at designated points: when the top half returns, when the kernel's ksoftirqd thread runs (if the softirqs are being deferred because the system is overloaded), and at a few other points where the kernel checks whether any softirqs are pending. Tasklets are run by the softirq infrastructure (two of the softirqs are reserved for tasklets). Workqueue items are run by the kernel's worker threads, which the scheduler schedules like any other thread.

4.7 Threaded interrupts

The top-half / bottom-half split is the traditional approach, but it has a cost: the top half still runs with interrupts disabled, and the kernel must carefully choose what to put in each half. The threaded interrupt is a modern alternative: the interrupt's handler is split into a minimal hardirq handler (the top half, which runs with interrupts disabled and does only what must be done urgently) and a threaded handler (which runs as a kernel thread, with a normal process context, and can sleep).

The threaded interrupt's advantage is its simplicity and its scheduling. The threaded handler runs as a thread, so it can be scheduled with a priority, it can be preempted, and it can sleep. The kernel's real-time scheduler can give the handler a high priority, so a real-time device's interrupt is handled promptly; the handler's work is accounted for like any other thread's, so the administrator can see its CPU usage and its latency. The disadvantage is the additional context switch: the hardirq handler must wake the thread, and the thread must be scheduled, which adds latency compared with a bottom-half that runs immediately on the interrupt's return.

Linux supports threaded interrupts through the request_threaded_irq function, and many drivers use them — particularly the drivers for devices that are used in real-time and low-latency systems. The PREEMPT_RT patch set (which has been merged into the mainline kernel in stages) converts most interrupt handlers to threaded handlers, so the kernel's interrupt handling is scheduled like any other work and the system's worst-case latency is bounded.

4.8 Message-signaled interrupts

The classical interrupt is a wire: the device asserts a signal on an interrupt request line, and the interrupt controller receives it. The mechanism has a limitation: the number of interrupt lines is finite (the 8259A PIC had eight, with cascading for more; the I/O APIC has 24), and a device that needs several interrupts (a multi-queue network adapter, an NVMe SSD with many queues) cannot be given a line per queue.

The modern alternative is the message-signaled interrupt (MSI). Instead of asserting a wire, the device writes a specific value to a specific memory address, and the interrupt controller interprets the write as an interrupt. The address and the value are programmed into the device during its initialization, and the device uses them to signal each of its interrupts. The mechanism has several advantages:

  • No wires. The device does not need an interrupt line, so the system's interrupt lines are not a limiting resource. A device can have hundreds of interrupts (one per queue) without consuming hundreds of lines.
  • No sharing. Each interrupt has its own address and value, so the handler does not need to poll the devices to identify the source. The mechanism's dispatch is direct, and the handler's overhead is lower.
  • Better for virtualization. A virtual machine's devices can use MSI to signal their interrupts, and the hypervisor can route the interrupts to the appropriate virtual processor without emulating the physical interrupt controller's wires.

PCI Express devices use MSI or MSI-X (an extended version that supports more interrupts and per-interrupt masking). The NVMe protocol's queue-based interface relies on MSI-X: each queue has its own interrupt, and the operating system can direct each queue's interrupts to a different processor core, so the storage device's parallelism is matched by the processors' parallelism. The mechanism is one of the reasons NVMe SSDs achieve their high throughput and low latency.

4.9 Interrupt coalescing

A high-throughput device generates many interrupts. A gigabit network adapter that receives a stream of minimum-size packets generates hundreds of thousands of interrupts per second; an NVMe SSD that completes many small reads generates even more. Each interrupt costs the processor a few hundred to a few thousand cycles, so the aggregate cost can consume a significant fraction of the processor's time — the condition known as interrupt livelock, in which the processor spends all its time entering and leaving handlers and has no time for the work the handlers schedule.

Interrupt coalescing is the mechanism's response. The device (or its driver) delays the interrupt and delivers one interrupt for several events:

  • Count-based coalescing. The device delivers an interrupt after it has accumulated N events (for example, after receiving N packets). The count is a tunable parameter: a larger count reduces the interrupt rate but increases the latency of the individual events.
  • Time-based coalescing. The device delivers an interrupt after a timer expires (for example, every 100 microseconds), regardless of how many events have accumulated. The timer bounds the latency: an event waits at most the timer's interval.
  • Hybrid coalescing. The device delivers an interrupt when either the count or the timer's deadline is reached, whichever comes first. The hybrid is the common choice, because it bounds the latency while still reducing the interrupt rate under high load.

Coalescing's trade-off is the classical latency / throughput trade-off. A larger coalescing window reduces the interrupt rate and the processor's overhead, but it increases the latency of the individual events. For a throughput-oriented workload (a bulk file transfer, a backup), the latency is irrelevant, and the coalescing can be aggressive; for a latency-sensitive workload (a database's transaction, an interactive application), the latency matters, and the coalescing must be conservative. The device's driver exposes the coalescing parameters through its interface — the network adapter's ethtool -C, the NVMe drive's queue parameters — and the administrator can tune them for the workload.

Interrupt coalescing and NAPI

The Linux network stack's NAPI (New API) is a related mechanism. Under high load, the network adapter's driver disables the device's interrupts and switches to polling: the kernel polls the device for packets at a bounded rate, processes a batch, and then re-enables the interrupts. The mechanism combines the best of both worlds: interrupts for the low-load case (where the latency matters and the interrupt rate is low), and polling for the high-load case (where the throughput matters and the interrupt rate would otherwise be overwhelming). NAPI is the reason the Linux network stack can sustain millions of packets per second without interrupt livelock.

4.10 Interrupt latency

Interrupt latency is the time between the device's assertion of its interrupt request and the completion of the handler's urgent work. It is the system's responsiveness measure, and it is the sum of several components:

  • The hardware's latency. The time for the interrupt controller to receive the request, prioritize it, and signal the processor; the time for the processor to acknowledge the interrupt and read the vector. The hardware's latency is typically a few hundred nanoseconds to a few microseconds, depending on the bus and the controller.
  • The processor's masking. If the processor is running with interrupts disabled (in a critical section, or in a higher-priority handler), the interrupt is held pending until the processor enables them. The masking's duration is the kernel's responsibility: a kernel that disables interrupts for long periods has high latency.
  • The handler's scheduling. The processor must finish the current instruction, save its state, and enter the handler. The time is small (tens to hundreds of cycles) but not negligible.
  • The handler's work. The handler must acknowledge the interrupt, read the device's status, and do the urgent work. The handler's duration is the kernel's responsibility: a long handler has high latency for the other interrupts that are blocked while it runs.

The latency's components are the reason the kernel's design keeps the interrupt-disabled regions short and the handlers brief. A real-time system — a system that must respond to an event within a bounded time — requires the latency to be bounded, not merely small: the worst case, not the average, is what matters. The PREEMPT_RT patch set and the threaded interrupts are the mechanisms that make the worst case bounded, and they are the reason Linux is used in real-time and industrial-control systems.

4.11 Common misconceptions

"Interrupts and exceptions are the same thing."

The two share a mechanism — the processor's response is the same — but they differ in their origin and their handling. A hardware interrupt is asynchronous: it can arrive at any time, and it is unrelated to the current instruction. An exception is synchronous: it is caused by the current instruction, and the handler knows what the instruction was doing and can take corrective action.

"The interrupt handler runs the whole device operation."

The handler runs only the urgent part of the operation: acknowledging the interrupt, reading the status, and scheduling the rest. The rest of the work runs in the bottom half (softirq, tasklet, workqueue), which runs later with interrupts enabled. The split is what keeps the handler short and the system's latency low.

"Interrupt handlers can sleep."

A hardware interrupt handler runs in interrupt context, which is not associated with any process. Sleeping (blocking) in that context would deadlock, because there is no process to wake and no scheduler state to save. The handler must use only non-blocking operations. Work that must sleep belongs in a workqueue, where it runs in a kernel thread's process context.

"Disabling interrupts is harmless."

Disabling interrupts increases the system's latency: the devices whose interrupts are blocked must wait, and their buffers may overflow or their packets may be dropped. The kernel keeps its interrupt-disabled regions short — typically a few instructions to a few dozen — and a long-disabled region is a bug. The correct approach is to use finer-grained synchronization (spinlocks, per-CPU data) wherever possible.

"Interrupts are always better than polling."

Interrupts save the processor's time when the expected wait is long, but they have overhead. For a very high-rate device — a 100 Gb/s network adapter, an NVMe SSD under heavy load — the interrupt overhead can exceed the transfer's cost, and the correct response is to switch to polling (as NAPI does). The choice depends on the load: interrupts for low load, polling for high load.

"An interrupt can be delivered to any processor."

On a multi-processor system, each interrupt has an affinity: the set of processors to which it can be delivered. The kernel chooses the affinity, and it tries to balance the load and to keep the interrupt's handling on the same processor as the process that will consume the data. The irqbalance daemon adjusts the affinities dynamically, and the administrator can set them through /proc/irq/<N>/smp_affinity.

"Message-signaled interrupts are just a wire by another name."

MSI replaces the wire with a memory write, and the change has real consequences: the number of interrupts is no longer limited by the number of lines, each interrupt has its own address and value (so no sharing and no polling to identify the source), and the mechanism is naturally compatible with virtualization. The NVMe protocol's queue-based interface relies on MSI-X, and its performance depends on the mechanism.

"Interrupt latency is the handler's duration."

The latency is the time from the device's request to the handler's completion, and it includes the hardware's delivery time, the processor's masking time, the handler's scheduling time, and the handler's own duration. The handler's duration is often the smallest component; the masking time and the hardware's delivery time are usually the largest. Reducing the latency requires reducing all of them, not just the handler.

05 Worked example

The following example traces a disk interrupt from the device's assertion to the completion of the deferred work, computes the interrupt's cost, and shows how the top-half / bottom-half split affects the system's latency.

Tracing a disk read's interrupt

Consider a disk read that the driver submitted with DMA (Lesson 11.1). The disk's controller has read the data into memory and asserts its interrupt. The trace follows the interrupt through the hardware, the top half, and the bottom half.

  1. The disk's controller asserts its interrupt. The controller has completed the DMA transfer and sets its status register to "command complete". It asserts its IRQ line (or, on a modern system, sends an MSI-X message).
  2. The I/O APIC receives the request. The I/O APIC identifies the source (the disk's interrupt line or the MSI-X address), determines its priority, and routes the request to the local APIC of the processor that the interrupt's affinity designates.
  3. The local APIC signals the processor. The local APIC asserts the processor's interrupt pin. The processor checks its interrupt-enable flag: if interrupts are enabled, it acknowledges the interrupt and asks the local APIC for the vector. If interrupts are disabled (the processor is in a critical section), the request is held pending.
  4. The processor enters the top half. The processor uses the vector as an index into the IDT, loads the handler's address, saves the current context (the instruction pointer, the code segment, the flags), and jumps to the handler. The handler runs with interrupts disabled (the IDT entry is an interrupt gate).
  5. The top half does the urgent work. The handler:
    • Reads the disk controller's status register to confirm the transfer's success.
    • Acknowledges the interrupt to the controller (which deasserts its IRQ line or sends the next message).
    • Marks the DMA buffer's descriptor as complete.
    • Schedules the block layer's completion work (a softirq) to process the completed request.
    • Returns with the iret instruction.
    The top half's duration is typically a few hundred to a few thousand cycles — a few microseconds at most.
  6. The kernel runs the bottom half. After the top half returns (or at the next designated point), the kernel checks whether any softirqs are pending. The block layer's completion softirq runs: it processes the completed request, updates the page cache, wakes the process that was waiting for the read (or completes the asynchronous I/O operation), and frees the DMA buffer. The bottom half runs with interrupts enabled, so other interrupts can be delivered while it runs.
  7. The waiting process resumes. The process that issued the read (or the process that the asynchronous I/O operation belongs to) is woken and scheduled. It returns from the read system call with the data in its buffer.

The trace shows the split in action: the top half does the urgent work (acknowledging the device, reading its status, scheduling the completion), and the bottom half does the substantial work (processing the request, updating the cache, waking the process) at a safe time. The split keeps the interrupt-disabled region short, so the system's latency is low.

The interrupt's cost

The interrupt's cost has four components:

Component Time Notes
Hardware delivery ~1 µs Controller, APIC, and processor's acknowledgement
Context save and entry ~0.2 µs Saving the RIP, CS, and RFLAGS, and loading the handler
Top half ~2 µs Status read, acknowledgement, scheduling
Bottom half ~5 µs Request completion, page cache update, process wakeup
Total ~8 µs Dominated by the software's work

The table is illustrative — the actual numbers depend on the processor, the bus, and the kernel's configuration — but the pattern is what matters: the hardware's delivery is a small fraction of the cost, and the software's work dominates. The observation is the reason the kernel's design focuses on the software's work: the top half must be short (it blocks other interrupts), and the bottom half must be efficient (it runs on every I/O completion).

Interrupt rate and the processor's overhead

Suppose the disk performs 100,000 I/O operations per second (a realistic rate for a fast SSD). Each operation generates one interrupt, and each interrupt costs about 8 µs of the processor's time. The processor's overhead is:

100,000 interrupts/s × 8 µs/interrupt = 0.8 s/s = 80%

The processor spends 80% of its time handling the interrupts — a condition close to interrupt livelock. The system has little time for the application's work, and its throughput is limited by the interrupt overhead rather than by the device's bandwidth.

The response is interrupt coalescing (section 4.9). If the drive delivers one interrupt per 32 operations, the interrupt rate falls to 3,125 per second, and the processor's overhead falls to 2.5%:

100,000 ops/s ÷ 32 ops/interrupt = 3,125 interrupts/s
3,125 interrupts/s × 8 µs/interrupt = 0.025 s/s = 2.5%

The cost is the latency: an operation's completion is delayed by up to 32 operations' worth of time. For a throughput-oriented workload (a bulk transfer), the latency is irrelevant and the coalescing is free; for a latency-sensitive workload (a database's transaction), the latency matters, and the coalescing must be conservative. The driver exposes the coalescing parameters, and the administrator tunes them for the workload.

The top-half / bottom-half split and latency

Suppose the disk's driver put the entire completion work in the top half — the request processing, the page cache update, and the process wakeup — instead of splitting it. The top half would run for about 7 µs with interrupts disabled, and the system's interrupt latency would be 7 µs for every other interrupt that arrives during that window.

With the split, the top half runs for about 2 µs and the bottom half for about 5 µs, but the bottom half runs with interrupts enabled. The system's interrupt latency falls to 2 µs (plus the hardware's delivery), and the other interrupts can be delivered while the bottom half runs. The split's benefit is the latency reduction: the interrupts that are blocked by the handler are blocked for 2 µs instead of 7 µs.

The benefit's size depends on the workload. In a system with only one device and no other interrupts, the split's benefit is small (the other interrupts' latency is irrelevant if there are no other interrupts). In a system with many devices and many interrupts — a server, a workstation with a fast network adapter and a fast SSD — the split's benefit is large, because the blocked interrupts' latency is the system's responsiveness. The split is the kernel's answer to the tension, and it is the reason the modern kernel can handle high interrupt rates without losing responsiveness.

Observing interrupts on a live system

The Linux tools let an administrator observe the system's interrupts: the counts per device, the softirqs' activity, and the interrupt affinities.

# Show the interrupt counts per device and per CPU
cat /proc/interrupts

# The output shows each interrupt's number, its count per CPU,
# its type (IO-APIC, PCI-MSI, etc.), and its name.

# Show the softirq counts per CPU
cat /proc/softirqs

# The output shows each softirq's count per CPU
# (HI, TIMER, NET_TX, NET_RX, BLOCK, TASKLET, SCHED, RCU).

# Show a specific interrupt's affinity
cat /proc/irq/32/smp_affinity
cat /proc/irq/32/smp_affinity_list

# Run a network workload and observe the interrupt counts
ping -c 1000 -i 0.001 localhost > /dev/null
cat /proc/interrupts | grep -i eth

# Show the kernel's interrupt-related messages
dmesg | grep -iE 'irq|interrupt' | head -20

The /proc/interrupts output shows the interrupt distribution: which devices interrupt, how frequently, and on which processors. The /proc/softirqs output shows the deferred work: which softirqs run, and how frequently. The /proc/irq/<N>/smp_affinity interface shows and sets the interrupt's affinity, and the dmesg output shows the kernel's messages about the interrupt controllers' configuration.

06 Interactive activity

Activity · explore interrupts on a live system

The goal of this activity is to use the Linux command line to observe the system's interrupts, to measure the interrupt rate under different workloads, and to see the top-half / bottom-half split in the kernel's statistics. Work through the steps in your course VM.

  1. Inventory the system's interrupts. Use /proc/interrupts to list the interrupts and their counts.
    # Show the interrupt counts
    cat /proc/interrupts
    
    # The output has one row per interrupt and one column
    # per CPU. The columns show the count per CPU; the
    # last two columns show the type and the device name.
    How many interrupts are listed? Which devices have the highest counts? Which interrupts are message-signaled (their type is "PCI-MSI" or "PCI-MSIX") and which are legacy ("IO-APIC")? Write down what you find.
  2. Observe the interrupt rate under a disk workload. Record the interrupt counts, run a disk-heavy workload, and record the counts again.
    # Record the counts before the workload
    grep -E 'nvme|ahci|sd' /proc/interrupts
    
    # Run a disk read
    dd if=/dev/sda of=/dev/null bs=1M count=2000 2>/dev/null
    
    # Record the counts after
    grep -E 'nvme|ahci|sd' /proc/interrupts
    
    # Compare the two outputs.
    How many interrupts did the workload generate? How many bytes did it read? What is the average number of bytes per interrupt? Does the number suggest that the driver uses interrupt coalescing? Explain.
  3. Observe the softirqs' activity. Use /proc/softirqs to see the deferred work's activity.
    # Show the softirq counts
    cat /proc/softirqs
    
    # Run a network workload
    ping -c 10000 -i 0.0001 localhost > /dev/null
    
    # Show the softirq counts again
    cat /proc/softirqs
    
    # The NET_RX and NET_TX softirqs' counts should increase.
    Which softirqs have the highest counts? How did the counts change after the network workload? What does the change tell you about the relationship between the top half (the interrupt handler) and the bottom half (the softirq)?
  4. Examine an interrupt's affinity. Use /proc/irq/<N>/smp_affinity to see and set an interrupt's affinity.
    # Find an interrupt's number
    grep -E 'nvme|ahci|eth' /proc/interrupts | head -5
    
    # Show the interrupt's affinity (replace 32 with the
    # interrupt's number from the previous output)
    cat /proc/irq/32/smp_affinity
    cat /proc/irq/32/smp_affinity_list
    
    # The affinity is a bitmask (in the first file) or a
    # list of CPU numbers (in the second).
    
    # Show the irqbalance daemon's status
    systemctl status irqbalance 2>/dev/null | head -5
    What is the interrupt's affinity? Which CPU (or CPUs) is it delivered to? Is the irqbalance daemon running? How does the affinity affect the system's performance?
  5. Measure the interrupt latency. Use the cyclictest tool (if available) or a simple timer loop to measure the system's interrupt latency.
    # Check whether cyclictest is available
    which cyclictest
    
    # If it is, run it for a few seconds
    sudo cyclictest -t1 -p80 -n -i 1000 -l 10000 2>/dev/null | tail -5
    
    # The output shows the latency's minimum, average, and
    # maximum in microseconds.
    
    # If cyclictest is not available, use a simple timer
    # loop with the 'perf' tool
    sudo perf stat -e 'irq:softirq_entry' sleep 5 2>/dev/null
    What is the system's maximum interrupt latency? How does it compare to the values in the lesson's worked example? What does the latency tell you about the system's responsiveness?
  6. Observe the interrupt handling's overhead. Use the perf tool to measure the time the kernel spends handling interrupts.
    # Measure the interrupt-handling overhead during
    # a disk workload
    sudo perf stat -e 'irq:irq_handler_entry,irq:irq_handler_exit' \
        dd if=/dev/sda of=/dev/null bs=1M count=1000 2>/dev/null
    
    # Measure the softirq overhead during a network workload
    sudo perf stat -e 'irq:softirq_entry,irq:softirq_exit' \
        ping -c 10000 -i 0.0001 localhost 2>/dev/null
    
    # The output shows the number of events and the time
    # spent handling them.
    How many interrupts did the disk workload generate? How many softirqs did the network workload generate? What fraction of the processor's time did the handling consume? Is the fraction significant?
  7. Compare the interrupt behaviour of two devices. If the system has both a hard disk and an SSD (or two devices with different types), compare their interrupt behaviour.
    # Identify the storage devices
    lsblk -d -o NAME,SIZE,ROTA,TRAN,MODEL
    
    # Record the interrupt counts for each device
    grep -E 'nvme|ahci|sd' /proc/interrupts
    
    # Run a read on each device
    for dev in $(lsblk -dn -o NAME); do
        echo "=== /dev/$dev ==="
        dd if=/dev/$dev of=/dev/null bs=1M count=100 2>/dev/null
        grep -E 'nvme|ahci|sd' /proc/interrupts
    done
    Which device generated more interrupts per byte? Which generated fewer? Explain the difference in terms of the devices' transfer sizes and the drivers' coalescing policies.
  8. Read the man pages. Read the man pages that document the interrupt interfaces.
    man 5 proc
    man 1 perf
    man 8 irqbalance
    man 1 dmesg
    man 2 sigaction
    For each, write a sentence describing its main contribution to your understanding of interrupts. The man 5 proc page documents the /proc/interrupts and /proc/softirqs interfaces; the man 1 perf page documents the performance-analysis tool; the man 2 sigaction page documents the user-space signal mechanism, which is related to (but distinct from) the kernel's interrupt mechanism.

Reflect: interrupts are one of the operating system's foundational mechanisms, and they are invisible to the user. The user presses a key and the character appears; the user reads a file and the bytes arrive. The invisibility is the result of the interrupt's design: the device signals, the processor responds, the handler runs, the bottom half completes the work, and the process resumes. The mechanism's details — the IDT, the priority, the masking, the split — are the kernel's concern, not the user's. Which of the mechanisms from this lesson matter most for the systems you use? What would you change about the interrupt mechanism's design if you could? How would you explain the difference between a hardware interrupt and an exception to someone who has never thought about it? What is the right balance between the handler's urgency and the system's responsiveness?

07 Knowledge check

Check your understanding

Question 1

Define the interrupt and distinguish it from exceptions, traps, faults, and aborts. Why does the distinction matter for the kernel's design?

Show answer
An interrupt is an event that causes the processor to suspend its current instruction stream, save its state, and transfer control to a designated handler. The event may come from a hardware device (a hardware interrupt) or from the processor itself (an exception, a trap). The terminology distinguishes the events by their origin and their semantics. A hardware interrupt is asynchronous: it is generated by a device's controller, it can arrive at any time, between any two instructions, and it is unrelated to the instruction that is currently executing. An exception is synchronous: it is generated by the processor in response to the instruction that is currently executing, and it occurs at a well-defined point in the instruction stream. The x86 architecture divides exceptions into three kinds. A fault is an exception that can be corrected by the handler, after which the instruction can be restarted — a page fault is the classic example. A trap is an exception that occurs after the instruction completes; the handler runs after it, and the instruction stream resumes at the next instruction — a breakpoint or a system call is the classic example. An abort is an exception that cannot be corrected; the instruction cannot be restarted, and the program may have to be terminated — a hardware error or a double fault is the classic example. The distinction matters for the kernel's design because the handlers have different requirements. A hardware interrupt handler must be short (it blocks other interrupts while it runs) and must not sleep (it runs in interrupt context, which is not associated with any process). An exception handler has more freedom: it is caused by the current instruction, so the kernel knows what the instruction was doing and can take corrective action (for example, a page fault handler can load the missing page and restart the instruction). A system-call handler has even more freedom: it runs on behalf of a specific process, with a defined context, and it can sleep and be preempted. The kernel's design reflects the differences, and a handler that ignores them is a source of subtle bugs.

Question 2

Describe the hardware path of a hardware interrupt, from the device's assertion to the processor's entry into the handler. What role does each component play?

Show answer
The hardware path has six stages. Stage 1: the device asserts its interrupt request line. When the device's controller completes a command or needs attention, it asserts a signal on its interrupt request (IRQ) line. On legacy systems, each device has its own line; on modern systems, the line is virtual (the device sends a message-signaled interrupt). Stage 2: the interrupt controller receives the request. The interrupt controller collects the devices' requests, prioritizes them, and delivers them to the processor. On modern x86 systems, the controller is the APIC (Advanced Programmable Interrupt Controller), which has two parts: the I/O APIC, which collects the devices' requests and routes them, and the local APIC, which is built into each processor core and receives the interrupts directed at that core. On ARM systems, the equivalent is the GIC (Generic Interrupt Controller). Stage 3: the interrupt controller signals the processor. The local APIC asserts the processor's interrupt pin (or sends an interrupt message). The processor checks whether interrupts are enabled; if they are disabled, the request is held pending until they are enabled. If they are enabled, the processor acknowledges the interrupt and asks the local APIC for the interrupt's vector — a small integer that identifies the interrupt. Stage 4: the processor transfers control to the handler. The processor uses the vector as an index into the interrupt descriptor table (IDT), a table in memory whose entries describe the handler for each vector. The entry contains the handler's address, the code segment, and the privilege level. The processor saves the current instruction pointer, code segment, flags, and (if a privilege change occurs) the stack pointer and stack segment, then loads the new values from the IDT entry and begins executing the handler. Stage 5: the handler runs. The handler identifies the device (if the vector is shared), acknowledges the interrupt to the controller, reads the device's status register, performs the urgent work (or schedules it for the bottom half), and returns. Stage 6: the processor returns. The handler's return instruction (iret on x86) restores the saved values and resumes the interrupted instruction stream. Each component plays a distinct role: the device's controller generates the request, the interrupt controller collects and prioritizes it, the processor's IDT dispatches it, and the handler services it.

Question 3

Explain interrupt priority, masking, and nesting. Why does the processor need all three, and what are the consequences of getting them wrong?

Show answer
Priority determines which interrupt is delivered when several are pending. The interrupt controller assigns each interrupt a priority, and it delivers the highest-priority one first. The processor's current priority (its interrupt priority level) is part of its state; an interrupt whose priority is lower than the processor's current level is held pending until the level drops. The mechanism allows a high-priority interrupt (a timer tick, a real-time event) to preempt a low-priority one (a slow device's completion). Masking is the ability to disable interrupts selectively. The processor has a global interrupt-enable flag (IF on x86); when IF is clear, the processor ignores all maskable interrupts. The interrupt controller also has per-line masks, so the kernel can disable a specific device's interrupt line without disabling the others. Masking is what makes the critical sections possible: a kernel code path that manipulates a data structure shared with an interrupt handler disables the relevant interrupts while it holds the structure, so the handler cannot observe the structure in an inconsistent state. Nesting is the ability of a higher-priority interrupt to preempt a lower-priority handler. On x86, an interrupt gate clears IF when the handler is entered, so the handler runs with interrupts disabled and cannot be preempted by another maskable interrupt; a trap gate does not clear IF, so the handler can be preempted. The kernel chooses the gate type per vector: the hardware interrupt handlers use interrupt gates, and the system-call handler uses a trap gate. The processor needs all three because they serve different purposes: priority orders the interrupts when several are pending, masking protects the critical sections, and nesting allows the higher-priority interrupts to preempt the lower-priority handlers. Getting them wrong has consequences: a kernel that disables interrupts for too long increases the system's latency and can cause the devices to lose data (their buffers overflow, their packets are dropped); a kernel that allows nesting where it should not can leave a shared data structure in an inconsistent state; a kernel that gets the priority wrong can starve a low-priority device or delay a high-priority one. The kernel's design keeps the interrupt-disabled regions short, uses finer-grained synchronization where possible, and chooses the gate types carefully.

Question 4

Explain the top-half / bottom-half split. Why is the split necessary, and what are Linux's three deferred-work mechanisms?

Show answer
The top-half / bottom-half split is the kernel's way of reconciling two conflicting requirements: the interrupt handler must be short (it runs with interrupts disabled, so a long handler blocks other interrupts and increases the system's latency), but the device's work is often long. The resolution is to split the work into two halves. The top half is the hardware interrupt handler. It runs with interrupts disabled, and it does only the urgent work: acknowledging the interrupt to the controller, reading the device's status register, copying the data that must not be lost, and scheduling the rest of the work for the bottom half. The top half is short — typically a few dozen to a few hundred instructions — so the interrupt-disabled region is short and the system's latency is low. The bottom half is the deferred work. It runs later, with interrupts enabled, at a time the kernel chooses. The bottom half can be long, can access data structures that the top half could not, and (in the workqueue case) can sleep. The split is necessary because without it the handler would have to do all the work with interrupts disabled, and the system's latency would be unacceptable. Linux provides three mechanisms for deferred work. Softirqs are the lowest-level mechanism: statically defined functions that run in softirq context (not associated with any process), with interrupts enabled, and can run concurrently on several processors. They are used for the highest-throughput work (network processing, block I/O completion, timers). Tasklets are a wrapper around softirqs that provides a simpler interface: a tasklet is a dynamically registered function that runs in softirq context, but the kernel guarantees that a given tasklet runs on only one processor at a time, so the code does not need to be reentrant. They are used for medium-throughput work, particularly in device drivers. Workqueues defer work to a kernel thread. A workqueue is a queue of work items, and the kernel's worker threads execute the items one at a time. Because the work runs in process context (the worker thread's context), the work item can sleep — it can block, wait for I/O, take mutexes, and access user memory. Workqueues are used for the lowest-throughput work and for work that must sleep. The three mechanisms' contexts determine which one a driver should use: softirqs and tasklets for work that must not sleep and must complete quickly, workqueues for work that must sleep or must run in a process context.

Question 5

What is interrupt coalescing, and why is it necessary for high-throughput devices? What is the trade-off it introduces?

Show answer
Interrupt coalescing is the mechanism by which a device (or its driver) delays the interrupt and delivers one interrupt for several events. A high-throughput device generates many interrupts: a gigabit network adapter that receives a stream of minimum-size packets generates hundreds of thousands of interrupts per second, and an NVMe SSD that completes many small reads generates even more. Each interrupt costs the processor a few hundred to a few thousand cycles (the hardware delivery, the context save, the handler's work, and the return), so the aggregate cost can consume a significant fraction of the processor's time. At the extreme, the processor spends all its time entering and leaving handlers and has no time for the work the handlers schedule — the condition known as interrupt livelock. Coalescing reduces the interrupt rate by delivering one interrupt for several events. The coalescing can be count-based (deliver an interrupt after N events), time-based (deliver an interrupt after a timer expires), or hybrid (deliver an interrupt when either the count or the timer's deadline is reached, whichever comes first). The hybrid is the common choice, because it bounds the latency while still reducing the interrupt rate under high load. The trade-off is the classical latency / throughput trade-off. A larger coalescing window reduces the interrupt rate and the processor's overhead, but it increases the latency of the individual events: an event's completion is delayed until the window closes. For a throughput-oriented workload (a bulk file transfer, a backup), the latency is irrelevant and the coalescing is free; for a latency-sensitive workload (a database's transaction, an interactive application), the latency matters, and the coalescing must be conservative. The device's driver exposes the coalescing parameters through its interface (the network adapter's ethtool -C, the NVMe drive's queue parameters), and the administrator can tune them for the workload. The mechanism is the reason high-throughput devices can sustain their rates without overwhelming the processor.

08 Unit discussion

Discussion prompt

The interrupt is one of the operating system's oldest mechanisms, and its design has evolved through several distinct generations. The first systems polled their devices: the processor asked each device in turn whether it had work to do, and the device's answer determined the processor's next step. The interrupt replaced the polling with an asynchronous signal, so the processor could do useful work while the devices operated. The interrupt controller replaced the individual interrupt lines with a prioritized collection, so the system could have many devices without overwhelming the processor. The top-half / bottom-half split replaced the monolithic handler with a short urgent part and a longer deferred part, so the system's latency stayed low as the devices' work grew. Threaded interrupts replaced the deferred part with a kernel thread, so the work could be scheduled, prioritized, and preempted like any other thread. Message-signaled interrupts replaced the interrupt wires with memory writes, so a device could have hundreds of interrupts without consuming hundreds of lines. Each generation was a response to the previous one's limitations, and each was shaped by the technology of its time.

The trend is toward scheduling and abstraction. The interrupt's handling is increasingly treated as work to be scheduled rather than as a special event to be serviced immediately: the threaded interrupt runs as a kernel thread, the softirq runs at a designated point, and the PREEMPT_RT patch set converts most handlers to threaded handlers so the kernel's worst-case latency is bounded. The abstraction is the interrupt's interface: the device's driver registers a handler, the kernel's interrupt controller routes the interrupt, and the driver does not need to know whether the interrupt is a wire or a message, a legacy line or an MSI-X vector.

The trend raises a question about the mechanism's future. If the handling is scheduled like any other work, is the interrupt still necessary? Some systems have proposed polling as the primary mechanism for high-throughput devices: the driver polls the device's queue at a bounded rate, the device's controller exposes its queue directly to the driver (as NVMe does), and the interrupt is used only to wake the driver when the queue is idle. The approach — the same as NAPI's — combines the best of both worlds: the latency of an interrupt when the load is low, and the throughput of polling when the load is high. Some systems go further: the DPDK (Data Plane Development Kit) and SPDK (Storage Performance Development Kit) frameworks dedicate processor cores to polling the devices, and they bypass the kernel's interrupt mechanism entirely.

Should the interrupt remain the operating system's primary event mechanism, or should polling replace it for high-throughput devices? What is the right division of labour between the interrupt (which is reactive and low-latency) and the polling loop (which is proactive and high-throughput)? How should the kernel decide when to switch between them, and who should make the decision — the kernel, the driver, or the application? Post your response to the Unit 11 discussion forum and reply to at least two classmates. Supports CLO1, CLO2, CLO5, and CLO6.

09 Practice exercise

This ungraded exercise consolidates the lesson and prepares you for Lesson 11.3. Aim to spend 60–90 minutes on it.

  1. Trace a keyboard interrupt. A user presses a key on a keyboard connected to a USB port. Trace the interrupt from the keypress to the character's appearance in the application. For each stage, describe:
    • What the hardware does.
    • What the top half does.
    • What the bottom half does.
    • What the application receives.
    How does the trace differ from the disk read's trace in the lesson's worked example? Why?
  2. Compare interrupts and polling. Write a short paragraph (200–250 words) comparing interrupt-driven I/O and polling. Your paragraph should address: (a) the processor's involvement in each; (b) the latency of each; (c) the throughput of each; (d) the workloads for which each is best suited; (e) the modern mechanisms (NAPI, interrupt coalescing, DPDK) that combine the two.
  3. Measure the interrupt rate. Use /proc/interrupts to measure the interrupt rate of the system's devices under different workloads.
    # Record the counts before the workloads
    grep -E 'nvme|ahci|eth' /proc/interrupts
    
    # Disk workload
    dd if=/dev/sda of=/dev/null bs=1M count=2000 2>/dev/null
    grep -E 'nvme|ahci' /proc/interrupts
    
    # Network workload
    ping -c 10000 -i 0.0001 localhost > /dev/null
    grep -iE 'eth|net' /proc/interrupts
    
    # Timer workload (the timer interrupt always runs)
    sleep 5
    grep -iE 'timer|local' /proc/interrupts
    Which device has the highest interrupt rate? Which workload generated the most interrupts per second? How does the rate compare to the values in the lesson's worked example?
  4. Examine the softirqs. Use /proc/softirqs to see the deferred work's activity, and relate it to the top-half / bottom-half split.
    # Show the softirq counts
    cat /proc/softirqs
    
    # Run a network workload
    ping -c 50000 -i 0.00001 localhost > /dev/null
    
    # Show the softirq counts again
    cat /proc/softirqs
    
    # Which softirqs changed? Which did not? Explain the
    # difference in terms of the workloads and the
    # top-half / bottom-half split.
    Which softirqs have the highest counts? Which changed after the workload? What does the change tell you about the relationship between the top half and the bottom half?
  5. Investigate an interrupt's affinity. Use /proc/irq/<N>/smp_affinity to examine the interrupt affinities and their relationship to the system's performance.
    # List the interrupts and their numbers
    grep -E 'nvme|ahci|eth' /proc/interrupts
    
    # For each interrupt, show its affinity
    for irq in $(grep -E 'nvme|ahci|eth' /proc/interrupts | awk -F: '{print $1}'); do
        echo "IRQ $irq: $(cat /proc/irq/$irq/smp_affinity_list 2>/dev/null)"
    done
    
    # Show the CPUs' utilization
    mpstat -P ALL 1 1 2>/dev/null || cat /proc/stat | grep '^cpu'
    How are the interrupts distributed across the CPUs? Is the distribution balanced? Which interrupts are pinned to specific CPUs? How does the distribution affect the system's performance?
  6. Measure the interrupt latency. If the system has cyclictest or perf, measure the system's interrupt latency under different loads.
    # Measure the latency under an idle system
    sudo cyclictest -t1 -p80 -n -i 1000 -l 10000 2>/dev/null | tail -5
    
    # Measure the latency under a disk load
    dd if=/dev/sda of=/dev/null bs=1M count=5000 2>/dev/null &
    sudo cyclictest -t1 -p80 -n -i 1000 -l 10000 2>/dev/null | tail -5
    wait
    
    # Measure the latency under a network load
    ping -c 100000 -i 0.00001 localhost > /dev/null &
    sudo cyclictest -t1 -p80 -n -i 1000 -l 10000 2>/dev/null | tail -5
    wait
    How does the latency change under the different loads? Which load has the largest effect? What does the change tell you about the system's interrupt handling?
  7. Design an interrupt-handling strategy. A system is being built for a high-frequency trading application. The application requires the lowest possible latency for network packets, and it must not lose any packets. The system has a 100 Gb/s network adapter with multiple queues and MSI-X, and a real-time kernel with threaded interrupts.
    • How would you configure the interrupt affinities?
    • How would you configure the interrupt coalescing?
    • Would you use interrupts or polling (NAPI or DPDK)?
    • How would you handle the packet processing (top half, bottom half, or user space)?
    • How would you measure the system's latency?
    Justify your design.
  8. Reflect. In your own words, explain why the interrupt mechanism is necessary, and describe the top-half / bottom-half split and its benefits. Then describe one situation in your own experience — a program you have written or a system you have used — where the interrupt mechanism's design affected performance or responsiveness. What was the workload, and how would a different design have changed the outcome?

10 Summary

  • An interrupt is an event that causes the processor to suspend its current instruction stream, save its state, and transfer control to a designated handler. A hardware interrupt is asynchronous and generated by a device; an exception is synchronous and generated by the processor. Exceptions are divided into faults (the instruction can be restarted), traps (the instruction completes), and aborts (the instruction cannot be restarted).
  • A hardware interrupt's path has six stages: the device asserts its interrupt request line; the interrupt controller (the I/O APIC and the local APIC on x86, the GIC on ARM) collects and prioritizes the request; the controller signals the processor; the processor acknowledges the interrupt and reads the vector; the processor uses the vector as an index into the interrupt descriptor table (IDT) to find the handler; the handler runs and returns.
  • The IDT maps each vector to its handler, the code segment, and the privilege level. Its entries are of three kinds: task gates (legacy), interrupt gates (which clear the interrupt-enable flag, so the handler runs with interrupts disabled), and trap gates (which do not clear the flag). The dispatch is a direct lookup (O(1)) and a privileged operation.
  • Priority determines which interrupt is delivered when several are pending. Masking disables interrupts selectively (the global IF flag, the per-line masks). Nesting allows a higher-priority interrupt to preempt a lower-priority handler. The three controls let the kernel protect its critical sections and keep the system's latency low.
  • The top-half / bottom-half split reconciles the handler's need to be short with the device's work being long. The top half runs with interrupts disabled and does the urgent work; the bottom half runs later, with interrupts enabled, and does the substantial work. The split keeps the interrupt-disabled region short, so the system's latency is low.
  • Linux provides three deferred-work mechanisms. Softirqs are statically defined functions that run in softirq context, with interrupts enabled, and can run concurrently on several processors. Tasklets are a wrapper around softirqs with a simpler interface; a given tasklet runs on only one processor at a time. Workqueues defer work to a kernel thread; because the work runs in process context, it can sleep.
  • Threaded interrupts split the handler into a minimal hardirq handler and a threaded handler that runs as a kernel thread. The threaded handler can be scheduled, prioritized, and preempted, and it can sleep; the trade-off is the additional context switch.
  • Message-signaled interrupts (MSI and MSI-X) replace the interrupt wires with memory writes. The mechanism removes the limit on the number of interrupt lines, removes the need for sharing (and the polling to identify the source), and is naturally compatible with virtualization. PCI Express devices use MSI or MSI-X; NVMe's queue-based interface relies on MSI-X.
  • Interrupt coalescing delays the interrupt and delivers one interrupt for several events. The mechanism reduces the interrupt rate and the processor's overhead, at the cost of increased latency for the individual events. The coalescing can be count-based, time-based, or hybrid; the hybrid is the common choice. The NAPI mechanism in the Linux network stack switches between interrupts and polling depending on the load.
  • Interrupt latency is the time between the device's request and the handler's completion. It includes the hardware's delivery time, the processor's masking time, the handler's scheduling time, and the handler's own duration. A real-time system requires the latency to be bounded, not merely small; the PREEMPT_RT patch set and the threaded interrupts are the mechanisms that make the worst case bounded.

Looking ahead

This lesson presented interrupts: the mechanism itself, the hardware that delivers it, the IDT that dispatches it, the priority and masking that control it, the top-half / bottom-half split that keeps it short, and the modern mechanisms (threaded interrupts, MSIs, coalescing) that extend it. It referred several times to direct memory access — the mechanism by which a device's controller transfers data to or from memory without the processor's per-byte involvement — and to the interaction between DMA and interrupts. Lesson 11.3 presents DMA in detail: the DMA controller, the transfer's setup and completion, the physical-address problem, the scatter-gather lists and the IOMMU, the DMA's interaction with the caches (cache coherency), and the DMA's role in the modern I/O stack. The interrupt and the DMA are the two mechanisms that make the modern asynchronous I/O model possible, and they are the bridge between the hardware of this unit and the software of the next.

11 References

  • Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapter 1, Section 1.3.2, and Chapter 12, Section 12.2.
  • Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C. Operating Systems: Three Easy Pieces, Chapter 36, I/O Devices, and the interlude Into the Land of the Interrupts. Available online at ostep.org.
  • Tanenbaum, A. S., & Bos, H. Modern Operating Systems, 4th ed., Chapter 5, Section 5.1.4.
  • Love, R. Linux Kernel Development, 3rd ed., Chapter 7, Interrupts and Interrupt Handlers, and Chapter 8, Bottom Halves and Deferring Work.
  • Bovet, D. P., & Cesati, M. Understanding the Linux Kernel, 3rd ed. O'Reilly, 2005. Chapter 4, Interrupts and Exceptions.
  • Corbet, J., Rubini, A., & Kroah-Hartman, G. Linux Device Drivers, 3rd ed. O'Reilly, 2005. Chapter 10, Interrupt Handling.
  • Patterson, D. A., & Hennessy, J. L. Computer Organization and Design. The treatment of exceptions, interrupts, and the processor's response.
  • Intel Corporation. Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 3, Chapter 6, Interrupt and Exception Handling.
  • Linux man-pages: man 5 proc, man 1 watch, man 8 irqbalance, man 1 dmesg, man 2 sigaction, man 7 signal, man 1 perf, man 1 trace-cmd.
👤