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
kmallocwithGFP_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:

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."
-
Top Half: This is the actual ISR you register with
request_irq. Its job is minimal and fast:- Acknowledge the interrupt at the hardware level.
- Read critical status registers or copy data from a hardware FIFO to a RAM buffer.
- Schedule the bottom half to do the rest of the work.
- 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:
- Process the data copied by the top half.
- Communicate with other parts of the kernel.
- Wake up user-space processes that might be waiting for data.
- 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.
- Data Structures: You embed a
struct work_structinside your driver's private data structure. - Initialization: In your
probefunction, you initialize this structure, associating it with your bottom-half C function. - Scheduling: The top half calls
schedule_work()on yourwork_struct. This places your work function onto a global queue. - 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.
- 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
- "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. Theprobefunction would usedevm_request_irqandINIT_WORKto set everything up. The bottom half (workfunction) would then retrieve the data from a buffer and perhaps callwake_up_interruptibleto notify a user-space process that is blocked on aread()call.
- You should describe the top-half/bottom-half structure: The top half reads a status register and calls
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:
- Initialize a
struct work_structin yourprobefunction, linking it to your bottom-half function (INIT_WORK). - Register your top-half handler using
devm_request_irq. - In the top half, call
schedule_work()to queue the bottom half. - Implement the complex logic in the bottom-half function.
- Initialize a
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.