Introduction
Welcome to the first lesson of our new module! In the previous module, we delved deep into the Linux memory management subsystem, culminating in our discussion of page reclamation. We saw how the kernel manages its own resources to maintain system stability. Now, we shift our focus from the kernel's internal world to its interface with the external world: hardware.
Today's lesson addresses the learning outcome: Explain the interrupt handling flow in Linux, including top-half and bottom-half processing. For an embedded systems developer, understanding how the CPU handles asynchronous events from hardware is fundamental. It's the cornerstone of writing efficient and responsive device drivers and diagnosing performance-critical issues. We will explore why interrupts are necessary, the challenges they present, and the elegant two-part strategy the kernel uses to manage them effectively.
1. The Need for Interrupts
Imagine you're developing firmware for a device that needs to react when a button is pressed. One way is to constantly check the button's status in a loop. This is called polling.
// Simplified Polling Example
while (1) {
if (is_button_pressed()) {
handle_button_press();
}
// do other work...
}
While simple, polling is inefficient. The CPU wastes cycles repeatedly checking the status, even when nothing is happening. A far more efficient approach is for the hardware to tell the CPU when an event has occurred. This notification mechanism is an interrupt.
The following video provides a clear, practical explanation of what interrupts are and why they are preferable to polling.
The video "Linux Kernel: Interrupts" from the Write your own Operating System channel provides an excellent analogy using a hard drive to explain the fundamental purpose of interrupts.
Watch the segment from 02:23 to 05:13. Focus on why polling a slow device like a hard drive is inefficient and how an interrupt allows the CPU to perform other tasks while waiting.
Interrupts are not all the same. They can be categorized in several ways, which is important for understanding their behavior in different hardware contexts.
- Hardware vs. Software Interrupts: Hardware interrupts are triggered by external devices (timers, network cards). Software interrupts are triggered by instructions (e.g., system calls).
- Maskable vs. Non-Maskable Interrupts (NMI): Most interrupts are maskable, meaning the CPU can temporarily ignore them. NMIs are reserved for critical events like severe hardware errors and cannot be ignored.
- Edge-triggered vs. Level-triggered: An edge-triggered interrupt is signaled by a change in voltage (a pulse). A level-triggered interrupt is signaled as long as the voltage is at a certain level. Level-triggered interrupts are easier to share between devices but require careful handling to acknowledge and clear them.
This next video offers a more formal classification of interrupt types.
Understanding Linux Interrupt Subsystem - Priya Dixit, Samsung Semiconductor India Research
The video "Understanding Linux Interrupt Subsystem" by Priya Dixit provides a concise overview of the different interrupt classifications.
Watch from 01:16 to 08:19. This section will systematically introduce the different types of interrupts. Pay attention to the distinction between edge-triggered and level-triggered, as this has practical implications in driver development.
2. The Core Problem: Latency and the Top/Bottom Half Solution
When the CPU receives an interrupt, it immediately stops its current work, saves its state, and jumps to a special function called an Interrupt Service Routine (ISR). To prevent race conditions and ensure the ISR can run without being interrupted itself, the kernel typically disables further interrupts on that CPU while the ISR is executing.
This creates a significant problem: What if the ISR takes a long time to complete?
If a driver for a high-speed network card takes several milliseconds to process a packet inside its ISR, the system will be unable to service other important interruptsโlike timer ticks or interrupts from other devicesโduring that time. This can lead to missed deadlines, poor system performance, and data loss. This is unacceptable in many embedded systems.
To solve this, Linux splits the work of interrupt handling into two parts:
-
The Top Half (or Hard IRQ): This is the ISR itself. It runs immediately upon interrupt arrival with other interrupts disabled. Its job is designed to be as short as possible. It should only perform time-critical tasks, such as acknowledging the interrupt to the hardware, reading data from a device's hardware buffer, and scheduling the "bottom half" to run later.
-
The Bottom Half (or Deferred Work): This part of the work is scheduled by the top half to run at a later time, when interrupts are re-enabled. The bottom half performs the longer, non-time-critical processing of the data (e.g., passing a network packet up the protocol stack).
Let's return to the "Linux Kernel: Interrupts" video, which explains this concept brilliantly.
This segment clearly illustrates the problem of long-running ISRs and introduces the top-half/bottom-half model as the solution.
Watch from 10:45 to 14:45. The explanation using a network card that receives data faster than it can be processed is a classic example that perfectly frames the problem.
This flowchart provides a great visual summary of the entire process.
3. The Detailed Interrupt Flow
Now that we have the high-level model, let's trace the path of an interrupt in more detail, from the hardware signal to the execution of the ISR.
Phase 1: Architecture-Specific Handling (The IDT)
When an interrupt controller (like an APIC) signals the CPU, the CPU needs to know which code to run. It doesn't call a function by name. Instead, the interrupt controller provides a number, or vector. The CPU uses this vector as an index into a special, architecture-specific table called the Interrupt Descriptor Table (IDT). Each entry in the IDT contains the address of the handler for that specific vector.
The setup and structure of the IDT are highly dependent on the CPU architecture (e.g., x86 vs. ARM). For an embedded developer working close to the hardware, understanding this layer is valuable.
The following reading explains the generic interrupt phases and defines the constraints of running in "interrupt context."
Interrupts โ The Linux Kernel documentation
The Linux Kernel documentation provides a concise, technical description of the interrupt handling phases and the concept of interrupt context.
Please read the sections titled "Generic interrupt handling in Linux" and "Interrupt context". Focus on: The three phases: critical, immediate, and deferred. This maps directly to the top-half/bottom-half model. The properties of "interrupt context", especially the rule that you are not allowed to sleep or perform any action that might trigger a context switch. This is a critical rule for any kernel developer.
Phase 2: The Generic IRQ Subsystem
Once the initial architecture-specific entry point is called, execution quickly transitions to the generic, architecture-independent interrupt subsystem in the kernel. This layer provides a unified way for device drivers to register and handle interrupts, regardless of the underlying hardware.
The core of this subsystem revolves around a few key data structures:
irq_desc: The main descriptor for each interrupt line, which holds information like the status, the interrupt controller chip, and a pointer to the chain of registered handlers (irq_action).irq_chip: A structure of function pointers that abstracts the low-level interrupt controller hardware (e.g., functions to mask, unmask, and acknowledge an IRQ).irq_domain: A system for translating hardware interrupt numbers to the kernel's virtual IRQ numbers, essential on complex SoCs with multiple interrupt controllers.
The following video provides an excellent, in-depth walkthrough of this generic flow and the associated data structures. Given your background, this level of detail should connect well with your experience designing complex systems.
Understanding Linux Interrupt Subsystem - Priya Dixit, Samsung Semiconductor India Research
The "Understanding Linux Interrupt Subsystem" video details the generic interrupt handling flow and the key data structures involved.
Watch the segment from 23:04 to 29:38. This part explains the journey of an interrupt through the generic C functions like handle_irq and handle_irq_event, and shows how the registered driver's handler is finally called.
4. Interview Scenario: Diagnosing a Driver Issue
Let's apply this knowledge to a typical interview problem that tests your understanding of these concepts.
Scenario: "You are tasked with debugging a driver for a custom high-speed data acquisition card on an embedded board. The card signals data availability via an interrupt. Users report that under heavy load, the system is dropping data. The card has a small 256-byte hardware FIFO buffer. What is the first thing you would suspect in your interrupt handler design, and what steps would you take to fix it?"
Click for Analysis
A strong answer would demonstrate a clear understanding of the top-half/bottom-half paradigm and its performance implications.
-
Initial Hypothesis: The most likely culprit is that the interrupt service routine (the top half) is doing too much work and taking too long to execute. While the ISR is running, interrupts are disabled, so the CPU cannot service the next interrupt from the card. If new data arrives faster than the ISR can complete, the small hardware FIFO will overflow, leading to data loss.
-
Diagnostic Steps:
- Code Review: The first step is to inspect the ISR code. Look for any operations that are not strictly necessary for acknowledging the interrupt and capturing the data. Are there complex calculations, memory allocations (
kmalloc), or any functions that could potentially sleep? - Tracing: Use kernel tracing tools like
ftraceto measure the execution time of the ISR. This would provide quantitative evidence. If the ISR's execution time is approaching the interval at which new data arrives, this confirms the hypothesis.
- Code Review: The first step is to inspect the ISR code. Look for any operations that are not strictly necessary for acknowledging the interrupt and capturing the data. Are there complex calculations, memory allocations (
-
Proposed Solution: Refactor the handler into a proper top-half and bottom-half.
- New Top Half (ISR):
- It should be minimal. Disable the interrupt on the device itself to prevent re-entrance.
- Quickly read all available data from the hardware's 256-byte FIFO into a larger, software-managed kernel buffer (like a
kfifo). - Acknowledge the interrupt at the interrupt controller level.
- Schedule a bottom half (e.g., a
taskletorworkqueue) to process the data in the software buffer. - Re-enable the device interrupt and return
IRQ_HANDLED.
- New Bottom Half (Deferred Work):
- This function will run later in a context that allows for longer processing times.
- It will pull data from the software buffer and perform the heavy lifting: processing the data, copying it to user space, etc.
- New Top Half (ISR):
This separation ensures that the time spent with interrupts disabled is minimized, preventing the FIFO from overflowing and resolving the data loss issue. This answer shows you can not only identify the problem but also propose a concrete, best-practice solution.
Conclusion
In this lesson, we have dissected the Linux interrupt handling flow. We started with the fundamental need for interrupts and then addressed the critical performance challenge they pose, leading to the elegant top-half and bottom-half design pattern.
Key Takeaways:
- Interrupts are a hardware mechanism that allows devices to notify the CPU of an event, which is far more efficient than polling.
- Long-running Interrupt Service Routines (ISRs) can block other critical interrupts, causing high system latency and data loss.
- To solve this, Linux splits interrupt handling into a top half (the fast, minimal ISR) and a bottom half (deferred work that runs later with interrupts enabled).
- The top half runs in interrupt context, where sleeping is forbidden. Its only job is to do the absolute minimum work required and schedule the bottom half.
- The flow begins with an architecture-specific lookup in the IDT and quickly transitions to the generic IRQ subsystem, which orchestrates calling the correct driver handler.
We've established why work must be deferred from the hard IRQ context. In our next lesson, we will explore the how. We will compare softirqs, tasklets, and workqueues, and select the appropriate deferred work mechanism for a given scenario.