In-Depth Reading: Chapter 10 of ‘Linux Device Drivers’ – Interrupt Handling

Hello everyone, I am a programmer.

This article is about 2700 words long. Interrupt handling is one of the most core and complex parts of driver development, directly affecting the responsiveness of the driver and the stability of the system. I haven’t delved deeply into interrupt handling; I only check the interrupt data under /proc/interrupts when I encounter a no-video phenomenon during development to determine if there is an issue with the data collected from the hardware sensor or if there is a chip coding fault. I don’t know much more about interrupts. Just in time to read this chapter, I will read it carefully and organize my study notes to understand it well.

Follow our public account to receive e-books related to Linux (including the third edition of ‘Linux Device Drivers’) and commonly used development tools. A document list is provided at the end of the article.

In-Depth Reading: Chapter 10 of 'Linux Device Drivers' - Interrupt Handling

1. Concept and Function of Interrupts

An “interrupt” is merely a signal that can be sent when hardware needs the processor’s attention. It is an asynchronous mechanism for communication between hardware and the kernel. When hardware needs the processor to handle a specific event (such as data arrival or operation completion), it sends an electrical signal to the CPU, which then pauses its current task to execute the specific service routine associated with that interrupt, and resumes the original task afterward.

The main purpose of introducing interrupts is to solve efficiency issues.

Speed mismatch: The processing speed of the CPU is much faster than that of external hardware. If the CPU actively and continuously polls the hardware status, it will waste a significant amount of computational resources.

Event uncertainty: The occurrence of external events is random, making polling inefficient.

Therefore, the interrupt mechanism allows the CPU to process other tasks in parallel without waiting for hardware, only being notified when the hardware is truly “ready,” greatly enhancing the overall efficiency of the system.

2. Characteristics and Constraints of Interrupt Handlers

An Interrupt Handler is a special piece of C code that runs in the interrupt context. This fundamentally differs from process context and brings a series of strict limitations:

[1]. Execution Restrictions:

Cannot communicate directly with user space: because it is not associated with any specific process.

Cannot perform operations that may cause blocking or sleeping: for example, cannot call wait_event, cannot acquire a semaphore (down operation), cannot use kmalloc(GFP_KERNEL) for potentially blocking memory allocation. Any function that may cause the scheduler to switch processes cannot be used.

Cannot call schedule().

Processing must be as fast as possible: Long interrupt handling will delay the response to other interrupts and may even affect system real-time performance.

[2]. Core Tasks:

Interact with hardware, clear the interrupt pending bit of the device to prevent the device from continuously generating interrupts.

Save the received data to a buffer or prepare data to be sent.

Wake up processes waiting for data on the device.

3. Installing Interrupt Handlers

Since interrupt lines (IRQ) are a limited system-level resource, drivers must follow the principle of “request-use-release.”

/* Requesting an interrupt */int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *dev_name, void *dev_id);

Interface parameter description:

irq: The interrupt number to request.

handler: A pointer to the interrupt handler function.

flags: Important flags that control interrupt behavior.

IRQF_SHARED: Indicates that the interrupt line can be shared by multiple devices.

IRQF_PROBE_SHARED: Used for probing shared interrupt lines.

(Note: The SA_* series flags mentioned in LDD3, such as SA_INTERRUPT, SA_SHIRQ, have been replaced by IRQF_* in modern kernels).

dev_name: Device identifier, which will be displayed in /proc/interrupts.

dev_id: A unique identifier for shared interrupts. It will be returned in the interrupt handler to distinguish which device generated the interrupt. Even if not shared, it is recommended to pass a pointer to device private data instead of NULL.

// Releasing an interrupt: void free_irq(unsigned int irq, void *dev_id);

Description: When releasing, the irq and dev_id must match those used during the request.

Note: It is recommended to request interrupts when the device is first opened and release them when the device is last closed. This helps save valuable interrupt resources and avoids occupying IRQ lines when the module is loaded but the device is not in use.

4. Top Half and Bottom Half Mechanism

Linux (along with many other systems) solves this problem by dividing the interrupt handling routine into two parts: the “top half” and the “bottom half.” This is the core idea of interrupt handling design, used to resolve the conflict between “fast processing” and “heavy work.”

Top Half: This is the interrupt handler registered through request_irq. It only runs when interrupts are disabled, performing the most urgent and non-deferable tasks, such as reading the status from hardware, clearing interrupt flags, and then scheduling the bottom half before quickly exiting.

Bottom Half: Scheduled by the top half, it executes those time-consuming tasks, such as processing data buffers and waking up processes, at a safer and more relaxed time (usually when interrupts have been re-enabled). The bottom half mechanism allows for operations like sleeping.

Note: The main difference between the top half and bottom half handling routines is that when the bottom half handling routine is executed, all interrupts are enabled—this is what is meant by running at a safer time.

Almost every strict interrupt handling routine is divided into these two parts.

Main bottom half implementation mechanisms:

Tasklet:

Executed in soft interrupt context, still cannot sleep.

Tasklets of the same type cannot run simultaneously on multiple CPUs, simplifying synchronization design.

Suitable for tasks that are executed frequently, processed quickly, but are not suitable for completion in the top half.

Declaration and scheduling:

DECLARE_TASKLET(tasklet_name, tasklet_function, data);tasklet_schedule(&tasklet_name);

Work Queue:

Executed in process context, can sleep.

Suitable for delayed tasks that require potentially blocking operations (such as I/O operations, acquiring semaphores).

Delays and overhead are relatively larger than tasklets.

Initialization and scheduling:

struct work_struct my_work;INIT_WORK(&my_work, work_function);schedule_work(&my_work);

5. Interrupt Control Methods: Enabling and Disabling Interrupts

In certain critical code paths, it is necessary to temporarily disable interrupts to prevent race conditions (see 【In-Depth Reading】: Chapter 5 of ‘Linux Device Drivers’ – Understanding Race Conditions).

Main interface functions:

// Disable a single specified interrupt line and wait for the currently running handler to complete.void disable_irq(unsigned int irq);// Immediately return without waiting for the handler to completevoid disable_irq_nosync(unsigned int irq);// Re-enable the interruptvoid enable_irq(unsigned int irq);// Disable all interrupts on the local CPU: local_irq_save(flags);... // Save the current interrupt state and disable, then restore the previous statelocal_irq_restore(flags);// This is the recommended and safe way.// Unconditionally disable/enablelocal_irq_disable();... local_irq_enable();

Note: For shared interrupt lines, these functions should be avoided as they will affect other devices sharing that IRQ.

6. Interrupt Sharing

In modern systems (such as PCI devices), it is common and necessary for multiple devices to share a single interrupt line.

Key points for implementing sharing:

When requesting an interrupt, the IRQF_SHARED flag must be set.

The dev_id parameter must be unique and cannot be NULL. In the interrupt handler, the dev_id passed in is used to determine whether the interrupt comes from this device.

The interrupt handler needs to be able to detect the source of the interrupt. When an interrupt occurs, all registered handlers on that interrupt line will be called one by one. The driver’s handler needs to check the hardware registers, and if it finds that the interrupt was not generated by its own device, it must immediately return IRQ_NONE; if it is, it processes and returns IRQ_HANDLED.

7. /proc Interface

The kernel provides two useful files to view interrupt information:

/proc/interrupts: Displays the number of interrupts for each IRQ number, as well as the names of devices registered on that IRQ. For multi-CPU systems, it will also show the number of interrupts handled by each CPU.

/proc/stat: Records some low-level statistics of system activity, including (but not limited to) the number of interrupts received since the system started.

Note: If the driver being tested requests and releases interrupts during each open and close cycle of the device, /proc/stat is more useful than /proc/interrupts.

Summary of this chapter:

Interrupt handling is the “nerve endings” of the driver, and the quality of its design and implementation is crucial.

Core Principles:

[1]. Fast in and out: The top half only does the most necessary and urgent work.

[2]. Bottom half priority: Any potentially time-consuming operations should be deferred to the bottom half.

[3]. Clear context: Always be aware of whether the code is running in interrupt context or process context, and adhere to the corresponding restrictions.

[4]. Make good use of sharing: When supported, use interrupt sharing reasonably to save system resources.

Practical considerations:

[1]. During driver initialization, you can confirm whether the device’s interrupt number has been correctly recognized by using cat /proc/interrupts.

[2]. Make full use of printk (note that excessive use of printk in interrupt handlers may introduce delays, so use with caution) and dmesg to debug the interrupt handling process.

[3]. For complex devices, carefully read the data sheet to clarify the interrupt status bits and clearing methods, which is a prerequisite for writing correct interrupt handlers.

This concludes the content.

Previous articles (welcome to subscribe to the technical sharing column for all articles):[Project Practice] Locating the issue of Flash entering hardware write protection in embedded software systems and the implementation method to unlock the write lock using software[Project Practice] A nanny-level tutorial: PWM control of white light lamps and brightness adjustment[Project Practice] Methods and issues encountered in enabling virtual bridge functionality under Linux and solutions[Project Practice] Troubleshooting and solutions for audio intercom functionality delays and stuttering under Linux[Project Practice] Troubleshooting and solutions for OTA upgrade failures causing system bricking under SPI Nor FlashIn-Depth Reading: Chapter 10 of 'Linux Device Drivers' - Interrupt HandlingThank you for reading this far

This is the notebook of a female programmer

15+ years embedded software engineer and a mother of two

Sharing reading insights, work experiences, self-growth, and lifestyle.

I hope my words can be helpful to you

Leave a Comment