Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 69

Interrupt Handling: Top Half & Bottom Half

Recommended resources

Hello! Welcome to your next lesson in the Embedded Interview Preparation Bootcamp.

In our last session, you learned how to write robust platform drivers using devm_* managed resources. We saw how functions like devm_request_irq can drastically simplify resource management by automatically handling cleanup. This provided us with a safe and modern way to register an interrupt handler.

Now, we need to address the crucial next question: what code should actually go inside that handler? Interrupt handlers operate in a special, highly constrained environment. Executing long-running or blocking tasks directly within them can cripple system performance. This lesson will teach you the standard Linux kernel pattern for solving this problem.

Your learning outcome for this lesson is to: Implement kernel interrupt handling using request_irq and partition the handler into a top half and a bottom half (workqueue).


1. The Challenge of Interrupt Context

When a hardware interrupt occurs, the CPU immediately stops its current task and jumps to execute the registered interrupt service routine (ISR). This execution context is known as interrupt context (or sometimes "atomic context" because it cannot be preempted by other tasks on the same CPU and cannot sleep).

This context imposes strict limitations on your handler code:

  • Must be fast: The longer your handler runs, the longer other interrupts (including those of higher priority) are potentially delayed, increasing system latency.
  • Cannot sleep or block: You cannot call any function that might sleep, such as kmalloc with GFP_KERNEL, copy_from_user, or acquiring a mutex. Doing so would lead to a system panic, as there is no process context to switch back to.
  • Limited stack: The interrupt stack is small and fixed in size.

Given your experience with automotive systems, you're likely familiar with the need for extremely low latency in components like airbag deployment or engine control. The Linux kernel's interrupt context rules are the software equivalent of ensuring that level of responsiveness.

To visualize this, consider the layers of execution:

Linux Kernel Interrupt Handling Layers
This diagram illustrates the conceptual separation. The "Top-half" runs in the high-priority, restrictive Interrupt context. The "Bottom-half" is deferred to run later in the more permissive Kernel context, where sleeping is allowed.

These restrictions mean that any significant processing must be deferred to a more suitable context. This is achieved by splitting the interrupt handling logic into two parts.


2. The Top-Half / Bottom-Half Model

The standard solution in Linux is to divide interrupt processing into a "top half" and a "bottom half."

Interrupt Handling Models Flowchart
This flowchart shows the overall flow. An interrupt triggers the fast **Top Half**. The Top Half then schedules work for one of several **Bottom Half** mechanisms (like a workqueue), which executes later.
  • Top Half: This is the actual ISR you register with request_irq. Its job is minimal and fast:

    1. Acknowledge the interrupt at the hardware level.
    2. Read critical status registers or copy data from a hardware FIFO to a RAM buffer.
    3. Schedule the bottom half to do the rest of the work.
    4. Exit quickly.
  • Bottom Half: This is a function that runs at a later time, in a more relaxed kernel context (process context in the case of workqueues). It can perform the time-consuming work:

    1. Process the data copied by the top half.
    2. Communicate with other parts of the kernel.
    3. Wake up user-space processes that might be waiting for data.
    4. Start the next hardware transaction.

There are several mechanisms for implementing bottom halves (softirqs, tasklets, workqueues). We will focus on workqueues, as they are highly flexible and a common choice in device drivers. Unlike tasklets, workqueues run in a process context, meaning they are schedulable and, most importantly, are allowed to sleep.

To deepen your understanding of this fundamental concept, please read the following section from the classic "Linux Device Drivers, 3rd Edition."

[PDF] Interrupt Handling - LDD3

This chapter from LDD3 provides an authoritative explanation of the 'Top and Bottom Halves' concept, explaining why it's necessary and introducing tasklets and workqueues as the primary implementation mechanisms.

Read the section titled 'Top and Bottom Halves' (starts on page 275). Focus on understanding the division of labor between the two halves and the key differences between tasklets and workqueues.


3. Implementation: request_irq with a Workqueue

Now let's translate this theory into practice. You'll need to define two functions: your top-half ISR and your bottom-half work function.

The Top Half: The Interrupt Handler

You register this function using request_irq (or preferably devm_request_irq as learned in the last lesson).

// Forward declaration for the bottom-half function
static void my_driver_bh(struct work_struct *work);

// Private driver data
struct my_driver_data {
    // ... other driver data ...
    struct work_struct work; // The work_struct for our bottom half
};

// TOP HALF: The Interrupt Service Routine
static irqreturn_t my_driver_isr(int irq, void *dev_id) {
    struct my_driver_data *data = dev_id;

    // 1. Minimal work: acknowledge IRQ, read status, etc.
    //    For this example, we'll just log that we were here.
    pr_info("Top half: Interrupt received!\n");

    // 2. Schedule the bottom half to run.
    schedule_work(&data->work);

    // 3. Return IRQ_HANDLED to tell the kernel we serviced the interrupt.
    return IRQ_HANDLED;
}

The dev_id parameter is crucial. It's the same pointer you pass to request_irq. As a best practice, you pass a pointer to your driver's private data structure, which allows the handler to access device-specific state and the associated work_struct.

For a quick refresher on the request_irq API and its parameters, please watch this segment.

Understanding Linux Interrupt Subsystem - Priya Dixit, Samsung Semiconductor India Research

This video from The Linux Foundation provides a clear overview of the generic interrupt APIs used in drivers.

Watch the segment from 00:29:31 to 00:34:24. Pay attention to the five parameters of request_irq and the meaning of the various IRQF_ flags, especially IRQF_SHARED (which corresponds to the SA_SHIRQ flag in the older LDD3 resource).

The Bottom Half: The Workqueue Function

The workqueue infrastructure provides a simple and powerful way to defer work.

  1. Data Structures: You embed a struct work_struct inside your driver's private data structure.
  2. Initialization: In your probe function, you initialize this structure, associating it with your bottom-half C function.
  3. Scheduling: The top half calls schedule_work() on your work_struct. This places your work function onto a global queue.
  4. Execution: At some later point, a kernel worker thread will pick your function from the queue and execute it.
// BOTTOM HALF: The work function
static void my_driver_bh(struct work_struct *work) {
    // Cast the work_struct pointer back to our private data structure
    struct my_driver_data *data = container_of(work, struct my_driver_data, work);

    pr_info("Bottom half: Processing deferred work...\n");

    // Here you would do the heavy lifting:
    // - Process data from a buffer filled by the top half.
    // - Acquire a mutex and access a shared resource.
    // - Perform I/O to disk.
    // - Call wake_up_interruptible() to signal a user-space process.
}

Putting It All Together

The EmbeTronicX tutorial provides a complete, runnable example of a character driver that uses this exact pattern.

Workqueue in Linux Kernel Part 1

This tutorial provides a complete character driver example demonstrating a workqueue being scheduled from an interrupt handler. It's a perfect practical demonstration of today's learning outcome.

Read through the article, paying close attention to the 'Driver Source Code'. Identify the top half (irq_handler), the bottom half (workqueue_fn), the work declaration (DECLARE_WORK), the request_irq call in the init function, and the schedule_work call in the handler. Finally, examine the dmesg output to see the sequence of events.

Notice that the example from EmbeTronicX uses request_irq and free_irq. Based on our previous lesson, how would you improve this? You would use devm_kzalloc for the private data and devm_request_irq in the probe function, which would make the free_irq call in the exit function unnecessary. This is the modern, robust approach expected in production code.


4. Interview Perspective

Understanding interrupt handling is non-negotiable for an embedded Linux role at a semiconductor company. You can expect questions like:

  • "Why is it necessary to split an interrupt handler into a top and bottom half?"
    • Your answer should focus on minimizing interrupt latency by performing only time-critical actions in the restricted interrupt context, and deferring longer, blocking tasks to the bottom half.
  • "Describe a situation where you would choose a workqueue over a tasklet for bottom-half processing."
    • The key differentiator is the need to sleep. You should state that if the deferred work involves acquiring a mutex, performing file I/O, or allocating memory with GFP_KERNEL, a workqueue is the correct choice because it runs in a schedulable process context.
  • "Walk me through the code you would write to handle a GPIO interrupt that signals 'data ready'."
    • You should describe the top-half/bottom-half structure: The top half reads a status register and calls schedule_work. The probe function would use devm_request_irq and INIT_WORK to set everything up. The bottom half (work function) would then retrieve the data from a buffer and perhaps call wake_up_interruptible to notify a user-space process that is blocked on a read() call.


Conclusion

You have now learned one of the most fundamental patterns in Linux device driver development. By separating your interrupt logic into a fast, non-blocking top half and a deferrable bottom half implemented with a workqueue, you can build drivers that are both highly responsive and capable of performing complex tasks.

Key Takeaways:

  • Interrupt Context is Restricted: Handlers must be extremely fast and are forbidden from sleeping.
  • Top/Bottom Half Pattern: The solution is to split the work. The top half (ISR) does the bare minimum and schedules the bottom half.
  • Workqueues for Deferred Work: Workqueues are a flexible bottom-half mechanism that runs in a process context, allowing them to perform complex operations and sleep.
  • Implementation Flow:
    1. Initialize a struct work_struct in your probe function, linking it to your bottom-half function (INIT_WORK).
    2. Register your top-half handler using devm_request_irq.
    3. In the top half, call schedule_work() to queue the bottom half.
    4. Implement the complex logic in the bottom-half function.

Preview of the Next Lesson:

Now that we understand the generic interrupt handling framework, our next step is to apply it to a specific, ubiquitous hardware interface. In the next lesson, we will focus on how to use the modern GPIO framework (gpiod_* API) to control GPIO lines from a kernel driver, which often serve as the source of these interrupts.