Timeline
Timeline
2025-11-14
init
This article introduces the basic concepts of Linux interrupts and the top-half/bottom-half processing mechanism, discusses in detail the complete interrupt subsystem framework consisting of the user layer, generic layer, hardware-related layer, and hardware layer, and briefly summarizes the version features of the interrupt controller GIC.
Linux Driver Notes
| Table of Contents | Links |
|---|---|
| 1. Linux Driver Framework | |
| 2. Linux Driver Loading Logic | |
| 3. Character Device Basics | |
| 4. Concurrency and Race Conditions | |
| 5. Advanced Character Device Progression | |
| 6. Interrupts | |
| 7. Platform Bus | |
| 8. Device Tree | |
| 9. Device Model | |
| 10. Hot Plug | |
| 11. pinctrl Subsystem | |
| 12. GPIO Subsystem | |
| 13. Input Subsystem | |
| 14. Single Bus | |
| 15. I2C | |
| 16. SPI | |
| 17. UART | |
| 18. PWM | |
| 19. RTC | |
| 20. Watchdog | |
| 21. CAN | |
| 22. Network Device | |
| 23. ADC | |
| 24. IIO | |
| 25. USB | |
| 26. LCD |
Interrupt Concept
An interrupt is a mechanism triggered by external or internal events during the normal operation of the CPU. When an interrupt occurs, the CPU stops the currently executing program and instead executes the interrupt handler that triggered the interrupt. After the interrupt handler completes, the CPU returns to the point where the interrupt occurred and continues executing the interrupted program. The interrupt mechanism allows the CPU to respond to external or internal events in real time while maintaining the ability to handle other tasks.
The interrupt mechanism gives us the ability to handle unexpected situations, and if we make full use of this mechanism, we can accomplish multiple tasks simultaneously.
The interrupt mechanism enables us to handle multiple tasks in an orderly manner simultaneously, thereby improving concurrent processing capability. Similarly, computer systems also use the interrupt mechanism to respond to various external events. For example, during keyboard input, an interrupt signal is sent to the CPU to promptly respond to the user’s operation. This way, the CPU does not have to constantly poll the keyboard status and can focus on other tasks. The interrupt mechanism can also be used to handle events such as hard disk read/write completion and network packet reception, improving system resource utilization and concurrent processing capability.
Top and Bottom Halves of Interrupts
Interrupt execution requires a fast response, but not all interrupts can be completed quickly. If the interrupt handling time is too long, it will cause problems.
To enable the system to better handle interrupt events and improve real-time performance and responsiveness, the interrupt service routine is divided into two context parts:
- Interrupt Top Halfis the first part of the interrupt service routine, which mainlyhandles urgent tasks that require quick response。
- Its characteristic is a short execution time, aiming to complete interrupt handling as quickly as possible. These tasks may include saving register states, updating counters, etc., so that the execution can correctly return to the pre-interrupt location after the interrupt is processed.
- Interrupt Bottom Halfis the second part of the interrupt service routine, which mainlyhandles relatively time-consuming tasks。
- Since the top half needs to complete quickly, the bottom half is responsible for tasks that cannot be done immediately and require more time. These tasks may include complex calculations, accessing external devices, or performing lengthy data processing.
Interrupt Subsystem Framework
A complete interrupt subsystem framework can be divided into four layers, from top to bottom: user layer, generic layer, hardware-related layer, and hardware layer. The introduction of each layer is as follows:
- User Layer: The user layer is the user of interrupts, mainly including various device drivers.
These drivers apply for and register interrupts through interrupt-related interfaces. When a peripheral triggers an interrupt, the user-layer driver performs corresponding callback processing and executes specific operations.
Generic Layer: The generic layer, also known as the framework layer, is hardware-independent.
The code of the generic layer is common across all hardware platforms and does not depend on specific hardware architectures or interrupt controllers. It provides unified interfaces and functions for managing and handling interrupts, enabling drivers to be reused across different hardware platforms.Hardware-Related Layer: The hardware-related layer consists of two parts of code.
One part is code specific to a particular processor architecture, such as interrupt handling code for ARM64 processors. This code handles the interrupt mechanism of the specific architecture, including the interrupt vector table and interrupt handlers. The other part is the driver code for the interrupt controller, used to communicate with and configure the interrupt controller. This code is specific to the interrupt controller hardware.Hardware Layer: The hardware layer is at the bottom and is related to specific hardware connections.
It includes the physical connections between peripherals and the SoC (System on Chip). Interrupt signals are transmitted from peripherals to the interrupt controller, which manages and routes them to the processor. The design and implementation of the hardware layer determine the transmission method of interrupt signals and the hardware’s interrupt handling capability.

Interrupt Controller GIC
Reference:
| Version | Key Features | Common Cores |
|---|---|---|
| GICv1 | Supports up to 8 processor cores (PE) - Supports up to 1020 interrupt IDs | ARM Cortex-A5 MPCore, ARM Cortex-A9 MPCore, ARM Cortex-R7 MPCore |
| GICv2 | Includes all features of GICv1 - Supports virtualization | ARM Cortex-A7 MPCore, ARM Cortex-A15 MPCore, ARM Cortex-A53 MPCore, ARM Cortex-A57 MPCore |
| GICv3 | Includes all features of GICv2 - Supports more than 8 processor cores - Supports message-based interrupts - Supports more than 1020 interrupt IDs - System register access to CPU interface registers - Enhanced security model, separating secure and non-secure Group 1 interrupts | ARM Cortex-A53 MPCore, ARM Cortex-A57 MPCore, ARM Cortex-A72 MPCore |
| GICv4 | Includes all features of GICv3 - Supports direct injection of virtual interrupts | ARM Cortex-A53 MPCore, ARM Cortex-A57 MPCore, ARM Cortex-A72 MPCore |
Interrupt number
In the Linux kernel, weuse two IDs, the IRQ number and the HW interrupt ID, to identify an interrupt from a peripheral:
- IRQ number: The CPU needs to number each peripheral interrupt, which we call the IRQ number. ThisIRQ number is a virtual interrupt ID, independent of hardware, used only by the CPU to identify a peripheral interrupt。
- HW interrupt ID: For the GIC interrupt controller, it collects interrupt request lines from multiple peripherals and forwards them upward. Therefore, the GIC interrupt controller needs to encode peripheral interrupts.The GIC interrupt controller uses the HW interrupt ID to identify peripheral interrupts。If there is only one GIC interrupt controller, the IRQ number and HW interrupt ID can correspond one-to-one。
But in the case of cascaded GIC interrupt controllers, using only the HW interrupt ID cannot uniquely identify a peripheral interrupt; it is also necessary to know the GIC interrupt controller to which the HW interrupt ID belongs (HW interrupt IDs can be encoded repeatedly on different interrupt controllers)
For driver engineers, our perspective is the same as the CPU’s: we only want to obtain an IRQ number, without caring about which GIC interrupt controller and which HW interrupt ID it corresponds to. The advantage of this is that when interrupt-related hardware changes, the driver software does not need to be modified. Therefore,the interrupt subsystem in the Linux kernel needs to provide a mechanism to map HW interrupt IDs to IRQ numbers, which is the irq domain。
Interrupt request
request_irq()
include/linux/interrupt.h
1 | /** |
| Item | Description |
|---|---|
| Function Definition | int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev); |
| Header File | #include <linux/interrupt.h> |
| Parameter irq | The interrupt line (IRQ) number to be allocated |
| Parameter handler | The handler function called when an interrupt occurs (for threaded interrupts, the main handler) |
| Parameter flags | Flags used when handling the interrupt |
| Parameter name | The name of the device generating the interrupt |
| Parameter dev | Context information passed to the handler (usually the device context) |
| Function | Allocate a handler for the specified interrupt line. For threaded interrupts, userequest_threaded_irq()implementation. |
| Return value | Returns 0 on success; returns a negative error code on failure. |
irqThe parameter specifies the interrupt number to request. For example, the GPIO interrupt number needs to be obtained through the gpio_to_irq function mapping GPIO pins.
irq_handler_t handlerThe parameter is a function pointer pointing to the interrupt handler function. The interrupt handler is a function called when an interrupt event occurs, used to handle the interrupt event.
1
typedef irqreturn_t (*irq_handler_t)(int, void *);
unsigned long flags: Flags for the interrupt handler
- IRQF_TRIGGER_NONE: No trigger method, indicating that the interrupt will not be triggered.
- IRQF_TRIGGER_RISING: Rising edge trigger method, indicating that the interrupt is triggered on the rising edge of the signal.
- IRQF_TRIGGER_FALLING: Falling edge trigger method, indicating that the interrupt is triggered on the falling edge of the signal.
- IRQF_TRIGGER_HIGH: High level trigger method, indicating that the interrupt is triggered when the signal is at a high level.
- IRQF_TRIGGER_LOW: Low level trigger method, indicating that the interrupt is triggered when the signal is at a low level.
- IRQF_SHARED: Interrupt sharing method, indicating that the interrupt can be shared by multiple devices, used for interrupt lines shared by two or more devices. All devices sharing this interrupt line must set this flag. If ignored, only one handler can be registered for this interrupt line.
- IRQF_TIMER: Notify the kernel that this handler is triggered by the system timer interrupt.
- IRQ_ONESHOT: Mainly used in threaded interrupts, it requires the kernel not to re-enable the interrupt until the hard interrupt handler has completed. The interrupt remains disabled until the threaded handler runs.
1 |
- name: Used by the kernel to identify
/proc/interruptsand/proc/irqdrivers in - dev: Its main purpose is to be passed as a parameter to the interrupt handler, which is unique to each interrupt handler because it identifies the device. For non-shared interrupts, it can be NULL, but for shared interrupts it cannot be NULL. A common way to use it is to provide the device structure, as it is both unique and potentially useful to the handler. That is, a pointer to a device data structure is sufficient.
Example:
1 | struct my_data { |
When writing an interrupt handler, there is no need to worry about reentrancy. To avoid interrupt nesting, the interrupt line serviced by the interrupt handler is disabled by the kernel on all processors.
gpio_to_irq()
include/linux/gpio.h
1 | static inline int gpio_to_irq(unsigned int gpio) |
gpio_to_The irq function is used toConvert the GPIO pin number to the corresponding interrupt request number。
| Item | Description |
|---|---|
| Function Definition | int gpio_to_irq(unsigned int gpio); |
| Header File | #include <linux/gpio.h> |
| Parameter gpio | GPIO (General Purpose Input/Output) number |
| Function | Converts a GPIO pin to the corresponding interrupt line number. This function depends on the underlying implementation__gpio_to_irq()to perform the conversion. |
| Return value | The corresponding interrupt line number, or a negative value if the conversion fails. |
free_irq()
include/linux/interrupt.h
1 | extern const void *free_irq(unsigned int, void *); |
| Item | Description |
|---|---|
| Function definition | void free_irq(unsigned int irq, void *dev_id); |
| Header file | #include <linux/interrupt.h> |
| Parameter irq | The interrupt line number to be released |
| Parameter dev_id | The device context associated with this interrupt (usually a device pointer or identifier) |
| Function | Release an allocated interrupt line and cancel the interrupt handler associated with that interrupt line. |
| Return Value | No return value. |
If the specified IRQ is not shared, then
free_irqthe interrupt handler will not be removed, only the interrupt line will be disabled.If the IRQ is shared, only the interrupt handler identified by dev_id (which should be the same as
request_irqthe one used in) will be removed, but the interrupt line remains until the last interrupt handler is deleted, after which the interrupt line will be disabled.
free_irqIt will block until all executing interrupts for the specified IRQ complete. It must be avoided in interrupt context.request_irqandfree_irq。
Interrupt Handler
include/linux/interrupt.h
1 | typedef irqreturn_t (*irq_handler_t)(int, void *); |
- Function Description:
The handler function is an interrupt service routine used to handle specific interrupt events. It is called by the operating system or hardware when an interrupt event occurs, executing necessary operations to respond to and process the interrupt request.
Parameter Description:
irq: Indicates the interrupt number or identifier of the interrupt source. It specifies the hardware device or interrupt controller that triggered the interrupt.dev_id: is a void pointer used to pass device-specific data or identifiers. It is typically used to distinguish different devices or resources in an interrupt handler.
Return value:
- irqreturn_tis an enumeration value of a specific type,used to indicate the return status of the interrupt service function. It can take the following values:
- IRQ_NONE: indicates that the interrupt service function did not handle the interrupt, and the interrupt controller can continue processing other interrupt requests.
- IRQ_HANDLED: indicates that the interrupt service function has successfully handled the interrupt, and the interrupt controller does not need further processing.
- IRQ_WAKE_THREAD: indicates that the interrupt service function has handled the interrupt and requests to wake up a kernel thread to continue further processing. This is used in some interrupt cases that require long processing time.
- irqreturn_tis an enumeration value of a specific type,used to indicate the return status of the interrupt service function. It can take the following values:
Interrupt Handlers and Locks
Interrupt handlers run in an atomic context and can only use spinlocks to control concurrency. Whenever there is global data accessible to user code (user tasks, i.e., system calls) and interrupt code, this shared data should be protected in user code byspin_lock_irqsave()protection.
The priority of interrupt handlers is always higher than that of user tasks. Even if the task holds a spinlock, merely disabling IRQs is insufficient, as interrupts may occur on another CPU. If a user task updating
data is interrupted by an interrupt handler trying to access the same data, it would be a disaster. Usingspin_lock_irqsave()will disable all interrupts on the local CPU, preventing system calls from being interrupted by any type of interrupt:
1 | ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos){ |
When sharing data between two different interrupt handlers (i.e., the same driver manages two or more devices, each with its own interrupt line), you should also usespin_lock_irqsave()to protect shared data, preventing other IRQs from triggering and useless spinning.
Example
The iTOP-RK3568 has 5 GPIO banks: GPIO0 ~ GPIO4, each bank is further distinguished by numbers A0 ~ A7, B0 ~ B7, C0 ~ C7, D0 ~ D7. The following formula is commonly used to calculate the pin:
The interrupt pin corresponding to the LCD touch screen is labeled TP_INT_L_GPIO3_A5, and the corresponding calculation process is as follows:
GPIO pin calculation formula:
GPIO group number calculation formula:
For GPIO3_A5, 3 means bank=3, A means group=0, 5 means X=5
1 |
|
Interrupt request function and data structure
request_irq()
include/linux/irq.h
1 | static inline int __must_check |
request_threaded_irq()
kernel/irq/manage.c
request_threaded_irqThe function is a powerful function provided by the Linux kernel, used to request allocation of an interrupt and associate the interrupt handler with that interrupt. Its main role is to register an interrupt handler function in the system to respond to the occurrence of the corresponding interrupt.
The following isrequest_threaded_irqa detailed introduction to the function’s features and role:
- Interrupt request:
request_threaded_irqThe function is used to request an interrupt. It registers the interrupt handler function for the corresponding interrupt number with the kernel and allocates necessary resources for that interrupt. The interrupt number is a unique identifier for a specific hardware interrupt. - Interrupt handler association: Through
handlerparameter associates the interrupt handler with the interrupt number. The interrupt handler is a predefined function used to handle interrupt events. When an interrupt occurs, the kernel calls this function to process the interrupt event. - Threaded interrupt handling:
request_threaded_irqThe function also supports using threaded interrupt handlers. By specifying thethread_fnparameter, longer interrupt processing or delay-sensitive work can be executed asynchronously in a kernel thread context. This helps avoid blocking for too long in the interrupt context. - Interrupt attribute settings: Through the
irqflagsparameter, various attributes and flags of interrupt handling can be set. For example, the interrupt trigger method (rising edge, falling edge, edge-triggered, etc.), interrupt type (edge-triggered interrupt, level-triggered interrupt, etc.), and other specific interrupt behaviors can be specified. - Device identifier association: Through the
dev_idparameter, the interrupt handling can be associated with a specific device. This allows access to device-related data in the interrupt handler. The device identifier can be a pointer to a device structure or other device-related data. - Error handling:
request_threaded_irqThe function returns an integer value indicating the result of the interrupt request. If the interrupt request is successful, the return value is 0; if it fails, a negative error code is returned, indicating the reason for the failure.
1 | int request_threaded_irq(unsigned int irq, irq_handler_t handler, |
struct irq_desc
include/linux/irqdesc.h
irq_The desc structure is one of the data structures used in the Linux kernel to describe interrupts. Each hardware interrupt has a corresponding irq_desc instance, which is used to record various information and status related to the interrupt. The main function of this structure is to manage interrupt handlers, interrupt behaviors, and other data related to interrupt handling.
1 | struct irq_desc { |
irq_descThe main role and function of the structure:
- Interrupt handler management:
irq_descin the structhandle_irqThe field stores a pointer to the interrupt handler function. When hardware triggers an interrupt, the kernel calls this function to handle the interrupt event. - Interrupt Behavior Management:
irq_descin the structactionThe field is a pointer to a list of interrupt behaviors. Interrupt behaviors are a set of callback functions used to register, unregister, and handle interrupt-related events. - Interrupt Statistics:
irq_descin the structkstat_irqsThe field is a pointer to interrupt statistics. This information is used to record the occurrence count and handling status of interrupt events, helping to analyze the performance and behavior of interrupts. - Interrupt Data Management:
irq_descin the structirq_dataThe field stores interrupt-related data, such as interrupt number and interrupt type. This data is used to identify and manage interrupts. - Generic Interrupt Data Management:
irq_descin the structirq_common_dataThe field stores generic data related to interrupt handling, such as interrupt controller and interrupt mask. This data is used to handle and control interrupt behavior. - Interrupt Status Management:
irq_descOther fields in the structure are used to manage the state of interrupts, such as nested interrupt disable count, wake-up enable count, etc. These state information help the kernel track and manage changes in interrupt states. By usingirq_descstructure, the kernel can effectively manage and handle hardware interrupts in the system. It provides a unified interface for registering and processing interrupt handlers, managing interrupt behavior, and offers necessary information and data structures to monitor and control the behavior and state of interrupts.
In theirq_descstructure, the most important is theactionfield.
struct irqaction
include/linux/interrupt.h
The irqaction structure is one of the data structures in the Linux kernel used to describe interrupt behavior. It is used to define callback functions and related attributes during interrupt handling. The main function of the irqaction structure is to manage the behavior and handlers associated with a specific interrupt.
1 | struct irqaction { |
The following isirqactionthe main roles and functions of the structure:
- Interrupt handler management:
irqactionIn the structure,handlerthe field stores the pointer to the interrupt handler function. This function is called when an interrupt occurs to handle the interrupt event. - Interrupt handler flag management:
irqactionIn the structure,flagsThe field is used to specify various attributes and flags for interrupt handling. These flags control the behavior of interrupt handling, such as trigger mode, interrupt type, etc. - Device Identifier Management:
irqactionIn the structure,dev_idThe field is used to store the device identifier associated with interrupt handling. It can be a pointer to a device structure or other device-related data, used to associate interrupt handling with a specific device. - Interrupt Action Linked List Management:
irqactionIn the structure,nextThe field is a pointer to the nextirqactionstructure pointer, used to build a linked list of interrupt actions. This allows multiple interrupt handlers to be linked together so that they are called in sequence when an interrupt occurs.
Work Deferral Mechanism
Deferral is a method of scheduling work to be executed in the future, postponing the operation. It allows delayed invocation and execution of any type of function.
- SoftIRQ: Executed in atomic context.
- Tasklet: Executed in atomic context.
- WorkQueue: Executed in process context.
The Softirq (soft interrupt or software interrupt) deferral mechanism is used only for fast processing because itruns under a disabled scheduler (in interrupt context). Softirqs are rarely (almost never) used directly; only the network and block device subsystems use them.
A Tasklet is an instance of Softirq, and for almost every situation requiring Softirq, a Tasklet is sufficient.
In most cases, Softirqs are scheduled in hardware interrupts that occur faster than they can be serviced, so the kernel queues them for later processing. Ksoftirqd handles the deferred execution (in process context). Ksoftirqd is a per-CPU kernel thread that processes unserviced software interrupts. If CPU resources are heavily consumed by Ksoftirqd, it indicates the system is overloaded or under an interrupt storm.
tasklet
In the Linux kernel,a tasklet is a special soft interrupt mechanism, widely used to handle tasks related to the bottom half of interrupts. The Tasklet deferral mechanism is mostly found in DMA, network, and block device drivers.
Tasklets are inherently non-reentrant. Code is called reentrant if it can be interrupted at any point during execution and then safely called again. Tasklets are designed to run on only one CPU (even on SMP systems), specifically the CPU that scheduled them. Different Tasklets can run simultaneously on different CPUs.
This is a common and effective method to avoid concurrency issues on multi-core processing systems.The function bound to a Tasklet can only run on one CPU at a time, thus avoiding concurrency conflicts。
Note
Calling it on a Tasklet that has already been scheduled but has not yet started execution
tasklet_schedulewill do nothing, and the Tasklet will ultimately execute only once.It is possible to call within a Tasklet
tasklet_schedule, which means the tasklet can reschedule itself.High-priority tasklets are always executed before normal-priority tasklets. Abusing high-priority tasks increases system latency. Only use them when truly fast execution is needed.
Functions that may cause sleep cannot be called within the function bound to a tasklet, otherwise it may cause a kernel exception.
This API is deprecated.
include/linux/interrupt.h
1 | /* Tasklets --- multithreaded analogue of BHs. |
tasklet_structThe structure contains the following members:
next: Pointer to the next tasklet, used to form a linked list structure so that the kernel can manage multiple tasklets simultaneously.state: Indicates the current state of the tasklet.count: Used for reference counting to ensure correct handling when the tasklet is scheduled or canceled from multiple places.func: Pointer to the function bound to the tasklet, which will be called when the tasklet executes.data: Parameter passed to the function bound to the tasklet; in kernel version 5.10, the function is named callback.
Additionally, for convenience, the typetasklet_tis defined as an alias forstruct tasklet_struct.
Static initialization DECLARE_TASKLET
1 |
Here, name is the name of the tasklet, func is the handler function of the tasklet, and data is the parameter passed to the handler function (no data in version 5.10). The initial state is enabled.
1 |
Here, name is the name of the tasklet, func is the handler function of the tasklet, and data is the parameter passed to the handler function (no data in version 5.10). The initial state is disabled.
Example
1 |
|
In the above example,my_taskletis the name of the tasklet,my_tasklet_handleris the handler function of the tasklet
However, it should be noted that,a tasklet statically initialized with DECLARE_TASKLET cannot be dynamically destroyed at runtime, so this method should be avoided when the tasklet is not needed. If you need to destroy the tasklet at runtime, you should usetasklet_initandtasklet_killfunctions for dynamic initialization and destruction.
Dynamic initialization task_init
1 | extern void tasklet_init(struct tasklet_struct *t, |
Here, t is a pointer to the tasklet structure, func is the handler function of the tasklet, and data is the parameter passed to the handler function
Example
1 |
|
In the example, we first definedmy_tasklet_handleras the tasklet handler function. Then, we declared a tasklet structure namedmy_tasklet. Next, by calling thetasklet_initfunction, we perform dynamic initialization.
By using thetasklet_initfunction, we can dynamically create and initialize tasklets at runtime. This allows us to flexibly manage and control the lifecycle of tasklets as needed. When a tasklet is no longer needed, we can use thetasklet_killfunction to destroy it and release related resources.
Disable function tasklet_disable
1 | static inline void tasklet_disable(struct tasklet_struct *t) |
Here, t is a pointer to the tasklet structure.
Example
1 |
|
In the above example, we first definedmy_tasklet_handleras the tasklet handler function. Then, we declared a tasklet structure namedmy_taskletand initialized it using thetasklet_initfunction.
Finally, by calling thetasklet_disablefunction, we disabled themy_tasklet。
After disabling the tasklet, even if thetasklet_schedulefunction is called to trigger the tasklet, the tasklet’s handler function will no longer be executed. This canUsed to temporarily pause or stop the execution of a tasklet until it is re-enabled(by calling the tasklet_enable function).
Note that disabling a tasklet does not destroy the tasklet structure, so it can be re-enabled at any time by calling thetasklet_enablefunction to re-enable the tasklet, or call thetasklet_killfunction to destroy the tasklet
Enable function tasklet_enable
1 | static inline void tasklet_enable(struct tasklet_struct *t) |
Here, t is a pointer to the tasklet structure
Example
1 |
|
After enabling the tasklet, if thetasklet_schedulefunction is called to trigger the tasklet, the tasklet’s handler function will be executed. Thus, the tasklet will begin executing its processing logic as scheduled.
It should be noted that enabling a tasklet does not automatically trigger its execution; instead, it is triggered by calling thetasklet_schedulefunction. At the same time, thetasklet_disablefunction can be used to temporarily suspend or stop the execution of the tasklet.
If you need to permanently stop the execution of the tasklet and release related resources, you should call thetasklet_killfunction to destroy the tasklet.
Scheduling function tasklet_schedule
1 | extern void __tasklet_schedule(struct tasklet_struct *t); |
Here, t is a pointer to the tasklet structure.
The kernel maintains normal-priority and high-priority tasklets in two separate linked lists.tasklet_scheduleadds the tasklet to the normal-priority linked list, usingTASKLET_SOFTIRQto flag the scheduling of the related Softirq.
tasklet_hi_scheduleadds the tasklet to the high-priority linked list, usingHI_SOFTIRQto flag the scheduling of the related Softirq. High-priority tasklets are intended for softirq handlers with low-latency requirements.
Example
1 |
|
It should be noted that scheduling a taskletonly marks the tasklet as needing execution,does not immediately execute the tasklet’s handler function. The actual execution time depends on the kernel’s scheduling and processing mechanism。
Destruction function tasklet_kill
1 | extern void tasklet_kill(struct tasklet_struct *t); |
Here, t is a pointer to the tasklet structure
Example
1 |
|
Calltasklet_killThe function releases the resources occupied by the tasklet and marks the tasklet as invalid. Therefore, a destroyed tasklet cannot be used again.
It should be noted thatbefore destroying a tasklet, ensure that the tasklet has been disabled(by calling thetasklet_disablefunction). Otherwise, destroying a tasklet that is currently executing may cause a kernel crash or other errors.
Once a tasklet is destroyed, if it needs to be used again, it must be reinitialized (by calling thetasklet_initfunction)
Example
1 |
|
Softirq
Software interrupt = an exception/interrupt mechanism triggered by software, used to enter the kernel.
Softirq = a bottom-half execution mechanism within the Linux kernel, used to handle high-frequency short tasks.
Softirq is a type of mechanism provided by the Linux kernelBottom-half interrupt mechanism, used to execute high-frequency, deferrable tasks.
It is not a CPU interrupt or an exception, but the kernel’s own scheduling mechanism.
1 | // include/linux/interrupt.h |
The above code defines an enumeration type used to identify different types or priorities of softirqs. Each enumeration constant corresponds to a specific softirq type.
Meaning of enumeration constants:
HI_SOFTIRQ: High-priority softirqTIMER_SOFTIRQ: Timer softirqNET_TX_SOFTIRQ: Network transmit softirqNET_RX_SOFTIRQ: Network receive softirqBLOCK_SOFTIRQ: Block device softirqIRQ_POLL_SOFTIRQ: Interrupt polling softirqTASKLET_SOFTIRQ:Tasklet softirqSCHED_SOFTIRQ: Scheduler softirqHRTIMER_SOFTIRQ: Unused, but kept as tools rely on the numbering. Sigh!RCU_SOFTIRQ: Preferable RCU should always be the last softirqNR_SOFTIRQS: Indicates the total number of softirqs, used for data indicating softirq types
The smaller the priority number of an interrupt, the higher its priority. In driver code, we can use the softirqs mentioned above in Linux driver code, and of course we can also add our own softirqs. We add a custom softirq as shown below, where TEST_SOFTIRQ is the custom-added softirq.
1 | // include/linux/interrupt.h |
Although adding a custom softirq is very simple,the Linux kernel developers do not recommend doing this. If we need to use softirqs, it is advised to use tasklets。
After adding a softirq, the Linux source code must be recompiled. Moreover, the interface functions for softirqs do not have exported symbol tables; to use them, you need tokernel/softirq.c:
1 | // kernel/softirq.c |
Softirq interface functions
open_softirq
To register a softirq, use the open_softirq function, whose prototype is as follows:
1 | void open_softirq(int nr, void (*action)(struct softirq_action *)); |
nr: The number or priority of the softirq. It is an integer representing the identifier of the softirq to be registered.action: A pointer to a function that will serve as the handler for the softirq. This function accepts a structsoftirq_actiontype parameter.
raise_softirq
To trigger a softirq, use theraise_softirqfunction, whose prototype is as follows:
1 | void raise_softirq(unsigned int nr); |
nr: The number or priority of the softirq. It is an integer representing the identifier of the softirq to be registered.
raise_softirq_irqoff
To trigger a softirq with hardware interrupts disabled, use the raise_softirq_irqoff function, whose prototype is as follows:
1 | void raise_softirq_irqoff(unsigned int nr); |
nr: The number or priority of the softirq. It is an integer representing the identifier of the softirq to be registered.
Example
1 |
|
Tasklet analysis
Tasklet is a softirq mechanism in the Linux kernel, which can be regarded as a lightweight deferred processing mechanism. It is implemented through the softirq control structure, hence it is also referred to as a softirq.
1 | // kernel/softirq.c |
for_each_possible_cpu(cpu): Iterate over each possible CPU. In a multi-core system, this loop is used to initialize each CPU’stasklet_vecandtasklet_hi_vec。per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head;: Set each CPU’stasklet_vectail pointer to the initial position of the corresponding head pointer. This is done toensure that the initial state of tasklet_vec is empty。per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head;: Set each CPU’stasklet_hi_vectail pointer to the initial position of the corresponding head pointer. This is done toensure that the initial state of tasklet_hi_vec is empty。open_softirq(TASKLET_SOFTIRQ, tasklet_action);:Register the TASKLET_SOFTIRQ soft interrupt, and specify the corresponding handler as tasklet_action. Thus, when TASKLET_SOFTIRQ is triggered, the tasklet_action function will be called to handle the corresponding tasks。open_softirq(HI_SOFTIRQ, tasklet_hi_action);: RegisterHI_SOFTIRQsoft interrupt, and specify the corresponding handler astasklet_hi_action. Thus, whenHI_SOFTIRQis triggered, it will calltasklet_hi_actionfunction to handle the corresponding task.
When executing__init softirq_init function, it triggersTASKLET_SOFTIRQ, and then callstasklet_actionfunction,tasklet_actionfunction is as follows:
1 | // kernel/softirq.c |
The above function callstasklet_action_commonfunction
1 | // kernel/softirq.c |
In the above code,tasklet_action_common()The function processes each tasklet in the task list. It first disables local interrupts, obtains the head pointer of the task list, clears the task list, and resets the tail pointer. Then it iterates through the task list, processing each tasklet. If the tasklet lock is acquired successfully and the counter is 0, it executes the tasklet handler function and clears the state flag. If the lock acquisition fails or the counter is not 0, it adds the tasklet to the tail of the task list and triggers the specified softirq. Finally, it enables local interrupts, completing the task processing procedure.
The tasklet is added to the list via the__tasklet_schedule_common()function.
1 | // kernel/softirq.c |
Through the above code,__tasklet_schedule_common()the function successfully adds the tasklet to the end of the linked list.
When the softirq is triggered, the system traverses the linked list and processes each tasklet. Therefore, after being added to the linked list, the tasklet will be scheduled and executed by the system at the appropriate time.
Based on the above analysis, it can be said that a tasklet is a special type of softirq.
Advantages of tasklet
Simplified interface and programming model: Tasklet provides a simple interface and programming model, making it easier to handle deferred work in the kernel. Compared to adding softirqs manually, Tasklet offers a higher-level abstraction.
Low latency: Tasklet executes in the softirq context, avoiding the context switch overhead of kernel threads, thus providing low latency. This is crucial for latency-sensitive tasks that require quick responses.
Adaptive scheduling: Tasklet has the feature of adaptive scheduling. When multiple tasklets are in a waiting state, the kernel merges them to reduce unnecessary context switches. This scheduling mechanism can improve system efficiency.
Disadvantages of tasklet
- Cannot handle long-running tasks:Tasklets are suitable for short-duration deferred work, and if long-running tasks need to be handled, they may block the execution of other tasks. For longer operations, work queues or kernel threads may be needed.
- Lack of flexibility: The execution of taskletsis limited to the softirq context, and is not suitable for all types of deferred work. In some cases, a more flexible scheduling and execution mechanism may be needed, where custom softirqs might be more appropriate.
- Resource limitation: The number of tasklets is limited,the number of tasklets available in the system depends on the architecture and kernel configuration. If a large amount of deferred work needs to be processed, it may be constrained by the number of tasklets.
Work queues
Work queues are one of the mechanisms for implementing the bottom half of interrupts, and are a data structure or mechanism for managing tasks.
The basic principle of work queues is to arrange tasks to be executed in order in a queue and provide a set of worker threads or worker processes to handle the tasks in the queue.
When new tasks arrive, they are added to the end of the queue, and worker threads or worker processes fetch tasks from the head of the queue and perform the corresponding processing operations.
Tasklets are also one of the mechanisms for implementing the bottom half of interrupts. If sleeping is needed in the bottom half of an interrupt, work queues are the only choice, becauseA tasklet cannot sleep, while a work queue can sleep.Therefore, tasklets can be used to handle relatively time-consuming tasks, whilework queues can handle even more time-consuming tasks.。
After a work queue defers work, it hands it over to a kernel thread for execution.During startup, Linux creates a worker kernel thread, which remains in a sleep state after creation. When there is work to be processed, this thread is awakened to handle it.。
In the kernel, work queues includeshared work queuesandcustom work queuesthese two types. These two types of work queues have different characteristics and uses.
- Shared work queuesare handled by a set of kernel threads, each running on a CPU. Once a work task needs to be scheduled, it is queued in the global work queue and will be executed at an appropriate time.
- Custom work queuesrun work queues within dedicated kernel threads. This means that whenever a work queue handler needs to be executed, a dedicated kernel thread is awakened to handle it, rather than one of the default predefined threads.
Shared work queues
The shared queue is a global work queue managed by the kernel,used to handle some system-level tasks in the kernel. Unless there is no other choice, or critical performance is required, or control
over every detail from work queue initialization to work scheduling is needed, otherwise, if tasks are only submitted occasionally, the shared work queue provided by the kernel should be used. This queue is shared by the entire system and can be used, but should not be exclusively occupied for a long time.
The shared work queue is a default work queue in the kernel that can be shared and used by multiple kernel components and drivers.
Since tasks pending on the queue are executed serially on each CPU, tasks should not sleep for a long time. Because before it wakes up, other tasks on the queue cannot run, and a task does not even know which tasks it shares the work queue with, so a task may take a long time to get the CPU.
Work in the shared work queue is executed by threads created by the kernel on each CPUevents/nthread execution
work_struct
1 | // include/linux/workqueue.h |
INIT_WORK()
| Item | Description |
|---|---|
| Macro definition | INIT_WORK(_work, _func) |
| Header file | #include <linux/workqueue.h> |
| Parameter work | Work item to initialize (work_structstructure) |
| Parameter func | Handler function corresponding to the work item |
| Function | Initialize a work item, binding it to the specified execution function, in preparation for subsequent scheduling. |
| Return value | No return value |
DECLARE_WORK()
| Item | Description |
|---|---|
| Macro definition | DECLARE_WORK(name, func); |
| Header file | #include <linux/workqueue.h> |
| Parameter name | Work item (work_structtype) variable name |
| Parameter func | The handler function corresponding to the work item |
| Function | Define and initialize a work item, equivalent to definingstruct work_structand usingINIT_WORK()to initialize. |
| Return value | No return value |
schedule_work()
| Item | Description |
|---|---|
| Function definition | bool schedule_work(struct work_struct *work); |
| Header file | #include <linux/workqueue.h> |
| Parameter work | Pointer to the work item to be scheduled |
| Function | Add the work item to the system work queue, requesting the scheduler to execute it at an appropriate time. |
| Return value | true: Successfully submitted to the work queue;false: Submission failed or the work item is already in the queue |
cancel_work_sync()
| Item | Description |
|---|---|
| Function definition | bool cancel_work_sync(struct work_struct *work); |
| Header file | #include <linux/workqueue.h> |
| Parameter work | Pointer to the work item to be canceled |
| Function | Synchronously cancel work item scheduling: remove it if in the queue; if executing, wait for completion before returning. |
| Return value | true: Successfully canceled;false: The work is not in a waiting or running state |
flush_work()
| Item | Description |
|---|---|
| Function Definition | bool flush_work(struct work_struct *work); |
| Header File | #include <linux/workqueue.h> |
| Parameter work | Pointer to the work item to wait for completion |
| Function | Wait for the specified work item to finish execution (if it is running or in the queue), ensuring it is no longer pending or running. Commonly used for synchronous cleanup before module unloading. |
| Return Value | true: The work item was pending or executing and has now completed;false: The work item was not scheduled or has already completed (no need to wait) |
Example
1 |
|
Custom Work Queue
A custom work queue is a specific work queue created by the kernel or a driver to handle specific tasks.。
Custom work queues are typically associated with specific kernel modules or drivers to execute tasks related to that module or driver.
workqueue_struct
The kernel usesstruct workqueue_structa structure to describe a work queue
1 | // kernel/workqueue.c |
create_workqueue()
| Item | Description |
|---|---|
| Function Definition | struct workqueue_struct *create_workqueue(const char *name); |
| Header File | #include <linux/workqueue.h> |
| Parameter name | Name of the created work queue |
| Function | Creates a per-CPU work queue (one worker thread per CPU). |
| Return Value | Success:workqueue_struct*pointer;Failure: NULL |
create_singlethread_workqueue()
| Item | Description |
|---|---|
| Macro definition | create_singlethread_workqueue(name) Macro expands to: alloc_workqueue("%s", WQ_SINGLE_THREAD, 1, name) |
| Header file | #include <linux/workqueue.h> |
| Parameter name | Work queue name |
| Function | Create a work queue bound to a single CPU and a single worker thread. |
| Return value | Success:workqueue_struct*pointer;Failure: NULL |
alloc_workqueue()
| Project | Description |
|---|---|
| Function Definition | struct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active, …); |
| Header File | #include <linux/workqueue.h> |
| Parameter fmt | Name format string for the workqueue (similar toprintf), used to identify the workqueue in the kernel (e.g., appearing in/proc/workqueueor logs). |
| Parameter flags | Flags controlling workqueue behavior, commonly including: • WQ_UNBOUND: Not bound to a specific CPU, dynamically assigned by the scheduler;• WQ_MEM_RECLAIM: Allows execution in memory reclaim paths to avoid deadlocks;• WQ_HIGHPRI: High-priority workqueue;• WQ_CPU_INTENSIVE: Marked as CPU-intensive, affecting concurrent scheduling. |
| Parameter max_active | Specifies the maximum number of work items that can be executed concurrently on each CPU for this work queue. • Set to 1indicates serial execution (at most one work runs at a time);• Set to 0indicates using the default value (usuallyWQ_MAX_ACTIVE = 512); (forWQ_UNBOUNDqueue, indicates the global maximum concurrency.) |
| Variable arguments … | are used withfmtformat string (e.g.,alloc_workqueue("my_wq_%d", ..., id)), iffmthas no format specifiers, it can be omitted. |
| Function | Dynamically creates and initializes a dedicated work queue (workqueue), returning its pointer. Compared to the deprecatedcreate_workqueue(), it provides finer control and better scalability. |
| Return Value | On success, returns a pointer tostruct workqueue_struct;On failure, returns NULL(e.g., insufficient memory). |
| Associated Operations | • Submitting work:queue_work(wq, &work)• Destroying: destroy_workqueue(wq)• Synchronous waiting: flush_work(),flush_workqueue() |
Typical Usage:
1 | // Create a custom work queue that is memory-reclaimable, CPU-unbounded, and executes serially. |
The following macro functions in the kernel are implemented by callingalloc_workqueue:
alloc_ordered_workqueue- Create a strictly serial (ordered) work queue.
- All work submitted to this queue will be executed in submission order, never concurrently.
- Use WQ_UNBOUND: Not bound to a specific CPU; the kernel scheduler freely selects the CPU.
max_active = 1: Ensures that at most one work is executed at a time.__WQ_ORDERED | __WQ_ORDERED_EXPLICIT: Internal flag that forcibly enables ‘ordered’ semantics.
1 |
create_workqueue- This is a macro replacement for the deprecated function create_workqueue() (deprecated since Linux 2.6.36).
- It actually creates a bound workqueue with one worker thread per CPU (but via
__WQ_LEGACYsimulates the old behavior). WQ_MEM_RECLAIM: Allows scheduling in the memory reclaim path to avoid deadlocks.max_active = 1: Note! This is inconsistent with earlier kernel behavior.
⚠️ In very old kernels, create_workqueue() defaults to multi-threaded concurrency (one thread per CPU, allowing parallel execution). However, modern kernels map it to max_active=1, so the actual behavior has become serial!
1 |
create_freezable_workqueue- Creates a workqueue that can be frozen when the system suspends (freezes).
WQ_FREEZABLE: When the system enterssuspend/hibernate, the work in this queue is paused until the system resumes.WQ_UNBOUND: Not bound to a CPU.max_active = 1: Serial execution.- Also with
__WQ_LEGACY, it is a compatibility interface.
1 |
create_singlethread_workqueue- Create a single-threaded, serial execution work queue.
- Essentially a
alloc_ordered_workqueue(..., max_active=1)wrapper. - With
__WQ_LEGACY, indicating this is a replacement for the old API. - Ensure all work is completed sequentially in the same execution context.
1 |
queue_work()
| Item | Description |
|---|---|
| Function Definition | bool queue_work(struct workqueue_struct *wq, struct work_struct *work); |
| Header File | #include <linux/workqueue.h> |
| Parameter wq | Target work queue pointer |
| Parameter work | The work item to be added to the work queue |
| Function | Adds a work item to the specified work queue, and the kernel scheduler selects an appropriate CPU for execution. |
| Return value | true: Successfully added to the queue;false: Failed to add (e.g., the work item is already pending in the queue) |
queue_work_on()
| Item | Description |
|---|---|
| Function definition | bool queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work); |
| Header file | #include <linux/workqueue.h> |
| Parameter cpu | Specifies on which CPU to execute the work item |
| Parameter wq | Pointer to the target work queue |
| Parameter work | The work item to be added to the work queue |
| Function | Adds a work item to the specified work queue of a designated CPU, awaiting execution. |
| Return value | true: Successfully added to the queue;false: Failed to add (e.g., already in the queue) |
cancel_work_sync()
| Item | Description |
|---|---|
| Function definition | bool cancel_work_sync(struct work_struct *work); |
| Header file | #include <linux/workqueue.h> |
| Parameter work | Pointer to the work item to be canceled |
| Function | Synchronously cancels a work item; if the work is executing, waits for completion before returning. |
| Return value | true: Successfully canceled;false: Job not in queue or not cancelable |
flush_work_queue()
| Item | Description |
|---|---|
| Function definition | void flush_workqueue(struct workqueue_struct *wq); |
| Header file | #include <linux/workqueue.h> |
| Parameter wq | Work queue to flush |
| Function | Flush the work queue, waiting for all submitted but not yet executed jobs to complete. |
| Return value | No return value |
destroy_work_queue()
| Item | Description |
|---|---|
| Function Definition | void destroy_workqueue(struct workqueue_struct *wq); |
| Header File | #include <linux/workqueue.h> |
| Parameter wq | Work queue to be destroyed |
| Function | Delete the work queue and release its resources. Before calling, ensure there are no pending work items in the queue (can be used withflush_workqueue()). |
| Return Value | No return value |
Example
1 |
|
Delayed work
Delayed work is atechnique that delays the execution of work to a later point in time for processing.
Typically,**when a task takes a long time, does not need to be executed immediately, or needs to be executed on time,**delayed work comes in handy.
The basic idea of delayed work is to put tasks into a queue, and then have background worker processes or a task scheduler handle the tasks in the queue. Tasks can be executed after a specified delay, or sorted and processed based on priority, task type, or other conditions.
- Delayed work is often used to handle tasks that take a long time, such as sending emails, processing images, etc. By putting these tasks into a queue and delaying their execution, it avoids blocking the main thread of the application and improves system responsiveness.
- Delayed work can be used to execute scheduled tasks, such as regularly backing up a database. By setting tasks to execute at a future point in time, it improves system reliability and efficiency.
Button debounce
Ideally, after pressing the button:

In actual situations:

- At time t1, the button is pressed, but due to jitter, it does not stabilize until time t2. The period from t1 to t2 is the jitter.
- This period is generally around ten to twenty milliseconds. From the diagram above, it can be seen that multiple triggers occur during the jitter period. If this jitter is not eliminated, the software will misjudge: although the button was pressed only once, the software reads the IO value and detects multiple level changes, mistakenly thinking it was pressed multiple times.
- Therefore,we need to skip this jitter period before reading the button’s IO value, meaning we should read the IO value at least after time t2
Timer debouncing:

In the button interrupt, start a timer with a period of 10ms. When the timer expires, it triggers a timer interrupt. Finally, in the timer interrupt handler, read the button value. If the button is still pressed, it indicates a valid button press.
In the diagram above, the period from t1 to t3 is the button jitter, which needs to be eliminated. Set the button to trigger on the falling edge, so the button interrupt will be triggered at times t1, t2, and t3. Each time the interrupt handler is entered, the timer interrupt is restarted, so the timer interrupt is started at times t1, t2, and t3.
However, the periods t1~t2 and t2~t3 are shorter than the timer interrupt period we set (i.e., the debounce time, e.g., 10ms). So although the timer is started at t1, it is reset at t2 before the timer expires. Ultimately, only the timer started at t3 completes the full timing period and triggers the interrupt. Then we can handle the button press in the interrupt handler. This is the principle of timer-based button debouncing, and the button driver in Linux uses this principle!
Besides using a timer for debouncing, delayed work can also be used. In the interrupt, delay the work by 3ms before reading the GPIO level state.
delayed_work
1 | // include/linux/workqueue.h |
DECLARE_DELAYED_WORK()
| Item | Description |
|---|---|
| Macro Definition | DECLARE_DELAYED_WORK(n, f); |
| Header File | #include <linux/workqueue.h> |
| Parameter n | Delayed Work (delayed_work) Variable Name |
| Parameter f | Handler Function for Delayed Work (void (*f)(struct work_struct *)) |
| Function | Statically define and initialize a delayed work item. |
| Return Value | No Return Value |
INIT_DELAYED_WORK()
| Item | Description |
|---|---|
| Macro definition | INIT_DELAYED_WORK(work, func); |
| Header file | #include <linux/workqueue.h> |
| Parameter work | Pointer to the delayed work structure to be initialized (delayed_work) |
| Parameter func | Handler function for the delayed work |
| Function | Dynamically initialize a delayed work item, while initializing the internalwork_structandtimer。 |
| Return value | No return value |
schedule_delayed_work()
| Item | Description |
|---|---|
| Function definition | bool schedule_delayed_work(struct delayed_work *dwork, unsigned long delay); |
| Header file | #include <linux/workqueue.h> |
| parameter dwork | The delayed work item to be scheduled |
| parameter delay | Delay time (in jiffies) |
| Function | Submit the delayed work to the system default work queue and execute it after the specified delay. |
| Return value | true: Successfully submitted;false: Submission failed |
queue_delayed_work()
| Item | Description |
|---|---|
| Function definition | bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay); |
| Header file | #include <linux/workqueue.h> |
| parameter wq | Specify the target work queue for execution |
| Parameter dwork | The delayed work item to be scheduled |
| Parameter delay | Delay time (in jiffies) |
| Function | Adds delayed work to a custom work queue and executes it after the specified delay. |
| Return value | true: Successfully submitted;false: Submission failed |
cancel_delayed_work_sync()
| Item | Description |
|---|---|
| Function definition | bool cancel_delayed_work_sync(struct delayed_work *dwork); |
| Header file | #include <linux/workqueue.h> |
| Parameter dwork | The delayed work item to be canceled |
| Function | Synchronously cancel delayed work scheduling: if the work has been executed or is being executed, wait for its completion. |
| Return value | true: successfully canceled;false: unable to cancel or work already completed |
1 | if ( !cancel_delayed_work( &thework) ){ |
Must check if the function’s return value is true. Ensure the work does not re-enqueue itself, then explicitly flush the work queue:
Example
1 |
|
Passing parameters to work queue
The main idea relies oncontainer_ofmacro to place work_struct into a custom structure
Example:
1 |
|
Concurrent managed work queues
When using work queues, we first define a work structure, then add the work to the workqueue, and finally the worker thread executes the workqueue.
When new work is generated in the work queue, the worker thread executes each work item in the queue. After finishing execution, the worker thread goes to sleep until a new interrupt occurs, then work is added to the queue again, and the worker thread executes each work item, repeating the cycle.
Single-core system
In a single-core thread system, a worker thread is typically initialized for each CPU (core) and associated with a work queue. This default setup ensures that each CPU has a dedicated thread to handle work items on its bound work queue.
Multi-core system
In a multi-core thread system, the design of work queues differs from that in a single-core thread system.**In a multi-core thread system, there are typically multiple work queues, each bound to a worker thread.**This allows full utilization of the parallel processing capabilities of multiple cores.
When a new work item is generated, the system needs to decide which work queue to assign it to. A common strategy is to use aload balancing algorithm, which balances the distribution of work items based on the load of each work queue, to prevent any single queue from becoming overloaded and causing performance degradation.
Each work queue independently manages its own work items. When a new work item is added to a work queue, the worker thread retrieves the pending work item from its associated queue and executes the corresponding processing function.
In a multi-core thread system, multiple worker threads can simultaneously execute work items from their respective bound work queues. This enables parallel processing, improving overall system performance and responsiveness.
Drawbacks of work queues
- When work item w0 is executing or even sleeping, work items w1 and w2 are queued and waiting. In a busy system, the work queue may accumulate a large number of pending work items, causing delays in task scheduling, which can affect system responsiveness and increase processing time for work items.
- In a work queue, different work items may have varying processing times and resource requirements. If the processing times of work items differ significantly, some worker threads may be constantly busy handling long-duration work items while others remain idle, leading to unbalanced resource utilization.
- In a multi-threaded environment, multiple worker threads accessing and modifying a work queue simultaneously can lead to race conditions. To ensure data consistency and correctness, appropriate synchronization mechanisms such as locks or atomic operations must be used to protect shared data, but this may introduce additional synchronization overhead.
- Work queues typically process work items in a first-in-first-out (FIFO) manner, lacking fine-grained control over work item priority. In some scenarios, priority scheduling based on the importance or urgency of work items may be required, but the work queue itself cannot provide this level of priority control.
- When worker threads fetch work items from the work queue and execute them, frequent context switches may be required, switching the processor’s execution context from one thread to another. This context switching overhead can impact system performance and efficiency.
Concurrency Managed Workqueue
CMWQThe full name is Concurrency Managed Workqueue, meaning a concurrency-managed work queue. A concurrency-managed work queue is a concurrent programming pattern used to efficiently manage and schedule tasks or work items to be executed. It is commonly used in multi-threaded or multi-process environments to achieve concurrent execution and improve system performance.

When we need to process multiple tasks or jobs simultaneously in a system, using a concurrency-managed work queue is an effective approach.
alloc_workqueue()
1 | //include/linux/workqueue.h |
| Item | Description |
|---|---|
| Function Definition | struct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active, …); |
| Header File | #include <linux/workqueue.h> |
| Parameter fmt | Format string for the work queue name (similar toprintf), used to identify the work queue in the kernel (e.g., appearing in/proc/workqueueor logs). |
| Parameter flags | Flags controlling the behavior of the work queue, commonly including: • WQ_UNBOUND: CPU not restricted, dynamically allocated by the scheduler;• WQ_MEM_RECLAIM: allowed to execute in memory reclaim path to avoid deadlocks;• WQ_HIGHPRI: high-priority workqueue;• WQ_CPU_INTENSIVE: marked as CPU-intensive, affecting concurrent scheduling. |
| Parameter max_active | specifies the maximum number of concurrently executing work items per CPU for this workqueue. • Set to 1indicates serial execution (at most one work running at a time);• Set to 0indicates using the default value (usuallyWQ_MAX_ACTIVE = 512); (forWQ_UNBOUNDqueue, indicates global maximum concurrency.) |
| Variable arguments … | used withfmtformat string (e.g.,alloc_workqueue("my_wq_%d", ..., id)), iffmtno format specifier can be ignored. |
| Function | Dynamically create and initialize a dedicated workqueue, returning its pointer. Compared to the deprecatedcreate_workqueue(), it provides finer control and better scalability. |
| Return Value | On success, returns a pointer tostruct workqueue_struct;on failure, returns NULL(e.g., insufficient memory). |
| Associated Operations | • Submitting work:queue_work(wq, &work)• Destroying: destroy_workqueue(wq)• Synchronous waiting: flush_work(),flush_workqueue() |
Typical usage:
1 | // Create a custom work queue that is reclaimable, CPU-unbound, and executes serially. |
The following macro functions in the kernel are all implemented by callingalloc_workqueue:
alloc_ordered_workqueue- Create a strictly ordered work queue.
- All work submitted to this queue will be executed in submission order, never concurrently.
- Use WQ_UNBOUND: not bound to a specific CPU, the kernel scheduler freely selects the CPU.
max_active = 1: Ensures at most one work is executing at any time.__WQ_ORDERED | __WQ_ORDERED_EXPLICIT: Internal flag to enforce ordered semantics.
1 |
create_workqueue- This is a macro replacement for the deprecated function create_workqueue() (deprecated since Linux 2.6.36).
- It actually creates a per-CPU bound work queue (but via
__WQ_LEGACYto simulate old behavior). WQ_MEM_RECLAIM: Allows scheduling in the memory reclaim path to avoid deadlocks.max_active = 1: Note! This is inconsistent with early kernel behavior.
⚠️ In very old kernels, create_workqueue() was multi-threaded and concurrent by default (one thread per CPU, parallel execution). But modern kernels, for simplicity, map it to max_In the form of active=1, the actual behavior has become serial!
1 |
create_freezable_workqueue- Create a work queue that can be frozen when the system is suspended (freeze).
WQ_FREEZABLE: When the system enterssuspend/hibernate, the work in this queue will be paused until the system resumes.WQ_UNBOUND: Not bound to a specific CPU.max_active = 1: Serial execution.- Also with
__WQ_LEGACY, it is a compatibility interface.
1 |
create_singlethread_workqueue- Create a single-threaded, serial execution work queue.
- Essentially a
alloc_ordered_workqueue(..., max_active=1)wrapper. - With
__WQ_LEGACY, indicating this is a replacement for the old API. - Ensure all work is completed sequentially in the same execution context.
1 |
Example
1 |
|
Interrupt threading technology
Interrupt threading (threaded IRQ) is an optimization technique used to improve the performance of multithreaded programs. The main goal isto minimize the time during which interrupts are disabled。
In multithreaded programs, hardware or other events sometimes send interrupt signals, interrupting the currently executing thread and requiring a switch to the interrupt handler to process these events. Thisfrequent interrupt switching leads to additional overhead and latency, affecting program performance.
To solve this problem, interrupt threading proposes an optimization scheme. It separates the interrupt handler from the main thread and creates a dedicated thread to handle these interrupt events. In this way, the main thread is no longer disturbed by interrupts and can focus on its own work without being frequently interrupted.
The core idea of interrupt threading isto separate interrupt handling from the main thread’s work, allowing them to execute in parallel. The interrupt thread is responsible for handling interrupt events, while the main thread handles the primary work tasks. This not only reduces switching overhead but also improves the overall program’s responsiveness and performance.
The processing of interrupt threading can still be seen as dividing the original interrupt into a top half and a bottom half.
The top half is still used to handle urgent matters, and the bottom half also handles relatively time-consuming operations, but the bottom half is handed over to a dedicated kernel thread. This kernel thread is used only for this interrupt.
When an interrupt occurs, this kernel thread is awakened, and then the kernel thread executes the functions of the bottom half of the interrupt。
It should be noted that interrupt threading also needs to handle synchronization and data sharing between threads. Because the interrupt thread and the main thread may simultaneously access and modify shared data, proper synchronization operations are required to ensure data consistency and correctness.
request_threaded_riq()
1 | // include/linux/interrupt.h |
| Project | Description |
|---|---|
| Function Definition | int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id); |
| Header File | #include <linux/interrupt.h> |
| Parameter irq | Interrupt number (IRQ line number) |
| Parameter handler | Top-half handler: the fast handler executed first when an interrupt occurs. If it returns IRQ_WAKE_THREADthen the bottom-half thread is woken up.Can be set to NULLto use the system default handler. |
| Parameter thread_fn | Threaded interrupt handler (bottom-half), executed in a kernel thread. If it isNULLthen interrupt threading is not used. |
| Parameter irqflags | Interrupt attribute flags, such asIRQF_TRIGGER_RISING、IRQF_SHAREDetc. |
| Parameter devname | Interrupt name, used to identify the device |
| Parameter dev_id | Device identifier passed to the handler function (usually a device structure pointer) |
| Function | Register a threaded interrupt, including top-half and bottom-half thread processing, to improve the real-time performance and schedulability of interrupt handling. |
| Return value | Success: returns 0; Failure: returns a negative error code |
handlerFunction: This is the same as usingrequest_irq()The function used during registration. It represents the top-half function, which runs in atomic context (or hard interrupt). If it can handle the interrupt quickly enough, it may not need a bottom-half at all, and it should returnIRQ_HANDLED. However, if the interrupt handling requires100μsor more, as mentioned earlier, a bottom-half should be used. In this case, it should returnIRQ_WAKE_THREAD, thus causing schedulingthread_fnfunction (must be provided).thread_fnfunction: This represents the bottom half, scheduled by the top half. When the hard interrupt handler (handler function) returnsIRQ_WAKE_THREAD, the kernel thread associated with this bottom half will be scheduled, and when the kernel thread runs, it calls the thread_fn function. The thread_fn function must return when completedIRQ_HANDLED. After execution, the interrupt is re-triggered, and before the hard interrupt returnsIRQ_WAKE_THREAD, the kernel thread will not be scheduled again.
Threaded interrupts can be used anywhere a work queue can schedule a bottom half. A true threaded interrupt must define both handler and thread_fn.
If handler is NULL and thread_fn is not NULL, the kernel will install a default hard interrupt handler that simply returns IRQ_WAKE_THREAD to schedule the bottom half.
When the interrupt handler executes, the interrupt it serves is always disabled on all CPUs, and is re-enabled when the hard interrupt (top half) completes.
However, if for some reason you need to not re-enable the interrupt line after the top half and keep it disabled until the threaded handler runs, you should request a threaded interrupt with the IRQF_ONESHOT flag. This way, the interrupt line will wait until the bottom half completes before being re-enabled.
Example
1 |
|

