Cover image for Linux Interrupts

Linux Interrupts


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 ContentsLinks
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.

Four Layers of the Interrupt Subsystem Framework
Four Layers of the Interrupt Subsystem Framework

Interrupt Controller GIC

Reference:

VersionKey FeaturesCommon Cores
GICv1Supports up to 8 processor cores (PE) - Supports up to 1020 interrupt IDsARM Cortex-A5 MPCore, ARM Cortex-A9 MPCore, ARM Cortex-R7 MPCore
GICv2Includes all features of GICv1 - Supports virtualizationARM Cortex-A7 MPCore, ARM Cortex-A15 MPCore, ARM Cortex-A53 MPCore, ARM Cortex-A57 MPCore
GICv3Includes 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 interruptsARM Cortex-A53 MPCore, ARM Cortex-A57 MPCore, ARM Cortex-A72 MPCore
GICv4Includes all features of GICv3 - Supports direct injection of virtual interruptsARM 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 interruptsIf 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* request_irq - Add a handler for an interrupt line
* @irq: The interrupt line to allocate
* @handler: Function to be called when the IRQ occurs.
* Primary handler for threaded interrupts
* If NULL, the default primary handler is installed
* @flags: Handling flags
* @name: Name of the device generating this interrupt
* @dev: A cookie passed to the handler function
*
* This call allocates an interrupt and establishes a handler; see
* the documentation for request_threaded_irq() for details.
*/
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

ItemDescription
Function Definitionint request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
Header File#include <linux/interrupt.h>
Parameter irqThe interrupt line (IRQ) number to be allocated
Parameter handlerThe handler function called when an interrupt occurs (for threaded interrupts, the main handler)
Parameter flagsFlags used when handling the interrupt
Parameter nameThe name of the device generating the interrupt
Parameter devContext information passed to the handler (usually the device context)
FunctionAllocate a handler for the specified interrupt line. For threaded interrupts, userequest_threaded_irq()implementation.
Return valueReturns 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
2
3
4
5
6
7
8
#define IRQF_TRIGGER_NONE	0x00000000
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING 0x00000002
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct my_data {
struct input_dev *idev;
struct i2c_client *client;
char name[64];
char phys[32];
};

static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_data *md = dev_id;
unsigned char nextstate = read_state(lp);
/* Check whether my device raised the irq or no */
[...]
return IRQ_HANDLED;
}

/* At certain points in the probe function */
int ret;
struct my_data *md;
md = kzalloc(sizeof(*md), GFP_KERNEL);
ret = request_irq(client->irq, my_irq_handler, IRQF_TRIGGER_LOW | IRQF_ONESHOT, DRV_NAME, md);

/* In the release function*/
free_irq(client->irq, md);

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
2
3
4
static inline int gpio_to_irq(unsigned int gpio)
{
return __gpio_to_irq(gpio);
}

gpio_to_The irq function is used toConvert the GPIO pin number to the corresponding interrupt request number

ItemDescription
Function Definitionint gpio_to_irq(unsigned int gpio);
Header File#include <linux/gpio.h>
Parameter gpioGPIO (General Purpose Input/Output) number
FunctionConverts a GPIO pin to the corresponding interrupt line number. This function depends on the underlying implementation__gpio_to_irq()to perform the conversion.
Return valueThe 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 *);
ItemDescription
Function definitionvoid free_irq(unsigned int irq, void *dev_id);
Header file#include <linux/interrupt.h>
Parameter irqThe interrupt line number to be released
Parameter dev_idThe device context associated with this interrupt (usually a device pointer or identifier)
FunctionRelease an allocated interrupt line and cancel the interrupt handler associated with that interrupt line.
Return ValueNo return value.
  • If the specified IRQ is not shared, thenfree_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 asrequest_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.

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos){
unsigned long flags;
/* some code */
[...]
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
data++;
spin_unlock_irqrestore(&my_lock, flags)
[...]
}

static irqreturn_t my_interrupt_handler(int irq, void *p){
/*
* Disable preemption while running the interrupt handler
* servicedIRQthe line is disabled,until the handler completes
* there is no need to disable all otherIRQ,only use spin_lock and spin_unlock
*/
spin_lock(&my_lock);
/* process data */
[...]
spin_unlock(&my_lock);
return IRQ_HANDLED;
}

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

pin=bank32+numberpin = bank * 32 + number

GPIO group number calculation formula:

number=group8+Xnumber = group * 8 + X

For GPIO3_A5, 3 means bank=3, A means group=0, 5 means X=5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <linux/module.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>

#define GPIO_PIN 101

// Interrupt handler function
static irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
printk(KERN_INFO "Interrupt occurred on GPIO %d\n", GPIO_PIN);
printk(KERN_INFO "This is irq_handler\n");
return IRQ_HANDLED;
}

static int __init interrupt_init(void)
{
int irq_num;

printk(KERN_INFO "Initializing GPIO Interrupt Driver\n");

// Map GPIO pin to interrupt number
irq_num = gpio_to_irq(GPIO_PIN);
printk(KERN_INFO "GPIO %d mapped to IRQ %d\n", GPIO_PIN, irq_num);
if (irq_num < 0){
return -ENODEV;
}

// Request interrupt
if (request_irq(irq_num, gpio_irq_handler, IRQF_TRIGGER_RISING, "irq_test", NULL) != 0) {
printk(KERN_ERR "Failed to request IRQ %d\n", irq_num);

// Request interrupt failed, release GPIO pin
gpio_free(GPIO_PIN);
return -ENODEV;
}
return 0;
}

static void __exit interrupt_exit(void)
{
int irq_num = gpio_to_irq(GPIO_PIN);

// Release interrupt
free_irq(irq_num, NULL);
printk(KERN_INFO "GPIO Interrupt Driver exited successfully\n");
}

module_init(interrupt_init);
module_exit(interrupt_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

Interrupt request function and data structure

request_irq()

include/linux/irq.h

1
2
3
4
5
6
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

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 requestrequest_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: Throughhandlerparameter 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 handlingrequest_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 theirqflagsparameter, 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 thedev_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 handlingrequest_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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)
{
struct irqaction *action;// Interrupt action structure pointer
struct irq_desc *desc;// Interrupt descriptor pointer
int retval; // Return value
// Check whether the interrupt number is in an unconnected state
if (irq == IRQ_NOTCONNECTED)
return -ENOTCONN;

/*
* Sanity-check: shared interrupts must pass in a real dev-ID,
* otherwise we'll have trouble later trying to figure out
* which interrupt is which (messes up the interrupt freeing
* logic etc).
*
* Also shared interrupts do not go well with disabling auto enable.
* The sharing interrupt might request it while it's still disabled
* and then wait for interrupts forever.
*
* Also IRQF_COND_SUSPEND only makes sense for shared interrupts and
* it cannot be set along with IRQF_NO_SUSPEND.
*/
// Check the validity of the interrupt flag
if (((irqflags & IRQF_SHARED) && !dev_id) ||
((irqflags & IRQF_SHARED) && (irqflags & IRQF_NO_AUTOEN)) ||
(!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
return -EINVAL;
// Get the interrupt descriptor for the interrupt number
desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
// Check whether the interrupt setting can perform interrupt requests and whether a unique device ID is assigned to each CPU
if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;
// If no interrupt handler is specified, use the default main handler
if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
// Allocate and initialize the interrupt action data structure
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;

action->handler = handler;// Interrupt handler
action->thread_fn = thread_fn;// Thread handler
action->flags = irqflags;// Interrupt flag
action->name = devname;// Device name
action->dev_id = dev_id;// Device ID

// Get the power management reference count of the interrupt
retval = irq_chip_pm_get(&desc->irq_data);
if (retval < 0) {
kfree(action);
return retval;
}
// Set the interrupt and associate the interrupt action with the interrupt descriptor
retval = __setup_irq(irq, desc, action);
// Handle the case of interrupt setting failure
if (retval) {
irq_chip_pm_put(&desc->irq_data);//Call irq_chip_The pm_put function releases the power management reference count of the interrupt
kfree(action->secondary);//Release the memory space of the secondary interrupt action
kfree(action);//Release the memory space of the interrupt action
}

#ifdef CONFIG_DEBUG_SHIRQ_FIXME
if (!retval && (irqflags & IRQF_SHARED)) {
/*
* It's a shared IRQ -- the driver ought to be prepared for it
* to happen immediately, so let's make sure....
* We disable the irq to make sure that a 'real' IRQ doesn't
* run in parallel with our fake.
*/
unsigned long flags;

disable_irq(irq);
local_irq_save(flags);

handler(irq, dev_id);

local_irq_restore(flags);
enable_irq(irq);
}
#endif
return retval;
}
EXPORT_SYMBOL(request_threaded_irq);

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
struct irq_desc {
struct irq_common_data irq_common_data; // Common interrupt data, including the interrupt identifier, type, and other information.
struct irq_data irq_data; // Specific interrupt data, including the interrupt status, trigger method, etc.

unsigned int __percpu *kstat_irqs; // Statistics of the interrupt, statistical data on each CPU.

irq_flow_handler_t handle_irq; // Pointer to the interrupt handler, the actual function that handles the interrupt.

struct irqaction *action; // Linked list of interrupt handling actions, used to store associated interrupt handlers.

unsigned int status_use_accessors; // Status field, indicating whether to read/modify the status through accessors.

unsigned int core_internal_state__do_not_mess_with_it; // Internal status field, direct manipulation may cause instability.

unsigned int depth; // Counter for nested interrupt disable.

unsigned int wake_depth; // Counter for nested wake-up enable.

unsigned int tot_count; // Total count of interrupts.

unsigned int irq_count; // Used to detect problematic interrupt counts.

unsigned long last_unhandled; // Timer for handling unhandled interrupts.

unsigned int irqs_unhandled; // Records the count of unhandled interrupts.

atomic_t threads_handled; // Atomic count representing the number of threads being processed.

int threads_handled_last; // Last processed thread count (for debugging/statistics).

raw_spinlock_t lock; // Spinlock used to protect this interrupt descriptor.

struct cpumask *percpu_enabled; // CPU mask enabled on each CPU.

const struct cpumask *percpu_affinity; // Interrupt affinity mask on each CPU.

#ifdef CONFIG_SMP
const struct cpumask *affinity_hint; // Hint for interrupt affinity.
struct irq_affinity_notify *affinity_notify; // Notification structure for handling affinity changes.

#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask; // Indicates the mask of currently pending interrupts.
#endif
#endif

unsigned long threads_oneshot; // Used to record the status of one-shot threads.

atomic_t threads_active; // Current number of active threads (atomic operation).

wait_queue_head_t wait_for_threads; // Queue head for threads waiting to be processed.

#ifdef CONFIG_PM_SLEEP
unsigned int nr_actions; // Number of interrupt handling actions (related to power management).
unsigned int no_suspend_depth; // The depth at which interrupts cannot be suspended.
unsigned int cond_suspend_depth; // The depth at which interrupts are conditionally suspended.
unsigned int force_resume_depth; // The depth at which interrupts are forcibly resumed.
#endif

#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir; // The proc filesystem entry for interrupts, used for debugging and monitoring.
#endif

#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
struct dentry *debugfs_file; // The debugfs file entry for interrupts, used for debugging.
const char *dev_name; // The name of the device, typically used for debugging.
#endif

#ifdef CONFIG_SPARSE_IRQ
struct rcu_head rcu; // The RCU head used for deferred release of interrupt resources.
struct kobject kobj; // The kernel object used to associate and manage this interrupt descriptor.
#endif

struct mutex request_mutex; // The mutex used when requesting interrupts, protecting concurrent access to interrupt requests.

int parent_irq; // If this interrupt is a child interrupt, its parent interrupt ID.

struct module *owner; // The associated module pointer, indicating which module is responsible for this interrupt.

const char *name; // The name of the interrupt, for debugging and identification.
} ____cacheline_internodealigned_in_smp; // Used to ensure cache alignment of this structure in SMP (Symmetric Multiprocessing) systems.

irq_descThe main role and function of the structure:

  • Interrupt handler managementirq_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 Managementirq_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 Statisticsirq_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 Managementirq_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 Managementirq_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 Managementirq_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct irqaction {
irq_handler_t handler; // Pointer to the interrupt handler function. A callback function used to handle interrupts, typically a regular Interrupt Service Routine (ISR).

void *dev_id; // Device ID. Usually a pointer to a device structure (or other type of identifier) used to uniquely identify the device associated with the interrupt.

void __percpu *percpu_dev_id; // Device ID per CPU, applicable to multiprocessor systems, storing the device identifier on each CPU.

struct irqaction *next; // Link for shared interrupts. Each interrupt number may correspond to multiple irqactions, and this field points to the next irqaction.

irq_handler_t thread_fn; // Handler function for threaded interrupts. For threaded interrupts,`thread_fn`is used to handle the associated work.

struct task_struct *thread; // Thread pointer. For threaded interrupts, this field points to the thread assigned to the interrupt.

struct irqaction *secondary; // Pointer to a secondary irqaction. Used to force certain interrupt operations to be handled in a thread.

unsigned int irq; // Interrupt number, identifying the interrupt associated with this irqaction.

unsigned int flags; // Interrupt flag.`IRQF_*`The flag is used to control the behavior of interrupts, such as whether sharing is allowed, whether it is handled in kernel threads, etc.

unsigned long thread_flags; // Thread flag, indicating settings or status related to threaded interrupts.

unsigned long thread_mask; // Thread mask, used to track the active status of threads.

const char *name; // The name of the interrupt handler action, usually the device name, used for debugging and identification.

struct proc_dir_entry *dir; // Pointer to the proc filesystem entry, used in`/proc/irq/NN/name`to display interrupt information.
} ____cacheline_internodealigned_in_smp; // Ensure that the structure is aligned to cache lines in multi-core systems.

The following isirqactionthe main roles and functions of the structure:

  • Interrupt handler managementirqactionIn 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 managementirqactionIn 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 ManagementirqactionIn 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 ManagementirqactionIn 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 executiontasklet_schedulewill do nothing, and the Tasklet will ultimately execute only once.

  • It is possible to call within a Tasklettasklet_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/* Tasklets --- multithreaded analogue of BHs.

This API is deprecated. Please consider using threaded IRQs instead:
https://lore.kernel.org/lkml/20200716081538.2sivhkj4hcyrusem@linutronix.de

Main feature differing them of generic softirqs: tasklet
is running only on one CPU simultaneously.

Main feature differing them of BHs: different tasklets
may be run simultaneously on different CPUs.

Properties:
* If tasklet_schedule() is called, then tasklet is guaranteed
to be executed on some cpu at least once after this.
* If the tasklet is already scheduled, but its execution is still not
started, it will be executed only once.
* If this tasklet is already running on another CPU (or schedule is called
from tasklet itself), it is rescheduled for later.
* Tasklet is strictly serialized wrt itself, but not
wrt another tasklets. If client needs some intertask synchronization,
he makes it with spinlocks.
*/

struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
bool use_callback;
union {
void (*func)(unsigned long data);
void (*callback)(struct tasklet_struct *t);
};
unsigned long data;
};

#define DECLARE_TASKLET(name, _callback) \
struct tasklet_struct name = { \
.count = ATOMIC_INIT(0), \
.callback = _callback, \
.use_callback = true, \
}

#define DECLARE_TASKLET_DISABLED(name, _callback) \
struct tasklet_struct name = { \
.count = ATOMIC_INIT(1), \
.callback = _callback, \
.use_callback = true, \
}

#define from_tasklet(var, callback_tasklet, tasklet_fieldname) \
container_of(callback_tasklet, typeof(*var), tasklet_fieldname)

#define DECLARE_TASKLET_OLD(name, _func) \
struct tasklet_struct name = { \
.count = ATOMIC_INIT(0), \
.func = _func, \
}

#define DECLARE_TASKLET_DISABLED_OLD(name, _func) \
struct tasklet_struct name = { \
.count = ATOMIC_INIT(1), \
.func = _func, \
}

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
2
3
4
5
6
#define DECLARE_TASKLET(name, _callback)		\
struct tasklet_struct name = { \
.count = ATOMIC_INIT(0), \
.callback = _callback, \
.use_callback = true, \
}

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
2
3
4
5
6
#define DECLARE_TASKLET_DISABLED(name, _callback)	\
struct tasklet_struct name = { \
.count = ATOMIC_INIT(1), \
.callback = _callback, \
.use_callback = true, \
}

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
2
3
4
5
6
7
8
9
10
11
12
#include <linux/interrupt.h>

// Define the tasklet handler function
void my_tasklet_handler(struct tasklet_struct *t)
{
// Tasklet processing logic
// ...
}

// Static initialization of tasklet
DECLARE_TASKLET(my_tasklet, my_tasklet_handler);
// Other code of the driver

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
2
extern void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data);

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
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <linux/interrupt.h>
// Define the tasklet handler function
void my_tasklet_handler(unsigned long data)
{
// Tasklet processing logic
// ...
}

// Declare the tasklet structure
static struct tasklet_struct my_tasklet;

// Initialize the tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);
// Other code of the driver

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
2
3
4
5
6
static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t);
tasklet_unlock_wait(t);
smp_mb();
}

Here, t is a pointer to the tasklet structure.

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <linux/interrupt.h>

// Define the tasklet handler function
void my_tasklet_handler(unsigned long data)
{
// Tasklet processing logic
// ...
}

// Declare the tasklet structure
static struct tasklet_struct my_tasklet;

// Initialize the tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);

// Disable the tasklet
tasklet_disable(&my_tasklet);

// Other code of the driver

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
2
3
4
5
static inline void tasklet_enable(struct tasklet_struct *t)
{
smp_mb__before_atomic();
atomic_dec(&t->count);
}

Here, t is a pointer to the tasklet structure

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <linux/interrupt.h>

// Define the tasklet handler function
void my_tasklet_handler(unsigned long data)
{
// Tasklet processing logic
// ...
}

// Declare the tasklet structure
static struct tasklet_struct my_tasklet;

// Initialize the tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);

// Enable the tasklet
tasklet_enable(&my_tasklet);

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extern void __tasklet_schedule(struct tasklet_struct *t);

static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}

extern void __tasklet_hi_schedule(struct tasklet_struct *t);

static inline void tasklet_hi_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_hi_schedule(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <linux/interrupt.h>
// Define the tasklet handler function
void my_tasklet_handler(unsigned long data)
{
// Tasklet processing logic
// ...
}

// Declare the tasklet structure
static struct tasklet_struct my_tasklet;
// Initialize the tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);
// Schedule the tasklet for execution
tasklet_schedule(&my_tasklet);
// Other code of the driver

It should be noted that scheduling a taskletonly marks the tasklet as needing executiondoes 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
2
extern void tasklet_kill(struct tasklet_struct *t);
extern void tasklet_kill_immediate(struct tasklet_struct *t, unsigned int cpu);

Here, t is a pointer to the tasklet structure

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <linux/interrupt.h>
// Define the tasklet handler function
void my_tasklet_handler(unsigned long data)
{
// Tasklet processing logic
// ...
}

// Declare the tasklet structure
static struct tasklet_struct my_tasklet;
// Initialize the tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);
tasklet_disable(&my_tasklet);
// Destroy the tasklet
tasklet_kill(&my_tasklet);
// Other code of the driver

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>

#define TEST_GPIO_PIN 101

static int irq;
static struct tasklet_struct mytasklet;

irqreturn_t test_irq_handler(int irq, void *dev_id)
{
pr_info("test_irq_handler is called\n");
tasklet_schedule(&mytasklet);
return IRQ_RETVAL(IRQ_HANDLED);
}
void mytasklet_func(unsigned long data)
{
pr_info("data is %lu\n", data);
}

static int __init tasklet_test_init(void)
{
int ret;
irq = gpio_to_irq(TEST_GPIO_PIN);
pr_info("irq is %d\n", irq);
if (irq < 0)
return -ENODEV;
ret = request_irq(irq, test_irq_handler, IRQF_TRIGGER_RISING, "test", NULL);
if (ret < 0)
return -ENODEV;

tasklet_init(&mytasklet, mytasklet_func, 1);
tasklet_enable(&mytasklet);// optional: after initialization, it is enabled by default

return 0;
}

static void __exit tasklet_test_exit(void)
{
tasklet_disable(&mytasklet);
tasklet_kill(&mytasklet);
free_irq(irq, NULL);
printk("bye\n");
}

module_init(tasklet_test_init);
module_exit(tasklet_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("This is a test sample for tasklet");

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// include/linux/interrupt.h
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ, // Softirq of tasklet
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */

NR_SOFTIRQS
};

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 softirq
  • TIMER_SOFTIRQ: Timer softirq
  • NET_TX_SOFTIRQ: Network transmit softirq
  • NET_RX_SOFTIRQ: Network receive softirq
  • BLOCK_SOFTIRQ: Block device softirq
  • IRQ_POLL_SOFTIRQ: Interrupt polling softirq
  • TASKLET_SOFTIRQTasklet softirq
  • SCHED_SOFTIRQ: Scheduler softirq
  • HRTIMER_SOFTIRQ: Unused, but kept as tools rely on the numbering. Sigh!
  • RCU_SOFTIRQ: Preferable RCU should always be the last softirq
  • NR_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// include/linux/interrupt.h
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on thenumbering. Sigh! */
TEST_SOFTIRQ, // Custom-added softirq
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */

NR_SOFTIRQS
};

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
2
3
4
5
6
// kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
EXPORT_SYMBOL(open_softirq)

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 struct
  • softirq_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>


int irq;

// Softirq handler
void testsoft_func(struct softirq_action *softirq_action)
{
printk("This is testsoft_func\n");
}

irqreturn_t test_interrupt(int irq, void *args)
{
printk("This is test_interrupt\n");
raise_softirq(TEST_SOFTIRQ); // Trigger softirq
return IRQ_RETVAL(IRQ_HANDLED);
}

static int interrupt_irq_init(void)
{
int ret;
irq = gpio_to_irq(101); // Map GPIO to interrupt number
printk("irq is %d\n", irq);
// Request interrupt
ret = request_irq(irq, test_interrupt, IRQF_TRIGGER_RISING, "test", NULL);
if (ret < 0)
{
printk("request_irq is error\n");
return -1;
}
// Register softirq handler function
open_softirq(TEST_SOFTIRQ, testsoft_func);
return 0;
}

static void interrupt_irq_exit(void)
{
free_irq(irq, NULL); // Release interrupt
printk("bye bye\n");
}

module_init(interrupt_irq_init);
module_exit(interrupt_irq_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("Tis is a test sample for softirq");

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// kernel/softirq.c
void __init softirq_init(void)
{
int cpu;
// Initialize tasklet for each possible CPU_vec and tasklet_hi_vec
// Set the tail pointer to the initial position of the corresponding head pointer
for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
}
// Register TASKLET_SOFTIRQ softirq, and specify the corresponding handler as tasklet_action
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
// Register HI_SOFTIRQ soft interrupt, and specify the corresponding handler as tasklet_hi_action
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
  • 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
2
3
4
5
6
7
8
9
10
// kernel/softirq.c
static __latent_entropy void tasklet_action(struct softirq_action *a)
{
tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ);
}

static __latent_entropy void tasklet_hi_action(struct softirq_action *a)
{
tasklet_action_common(a, this_cpu_ptr(&tasklet_hi_vec), HI_SOFTIRQ);
}

The above function callstasklet_action_commonfunction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// kernel/softirq.c
static void tasklet_action_common(struct softirq_action *a,
struct tasklet_head *tl_head,
unsigned int softirq_nr)
{
struct tasklet_struct *list;
// Disable local interrupts
local_irq_disable();
// Get the task list from tasklet_head
list = tl_head->head;
// Clear the task list in tasklet_head
tl_head->head = NULL;
// Point the tail pointer back to the head pointer
tl_head->tail = &tl_head->head;
// Enable local interrupts
local_irq_enable();
// Traverse the task list and process each tasklet
while (list) {
struct tasklet_struct *t = list;
// Get the next tasklet and update the list
list = list->next;

if (tasklet_trylock(t)) {// Attempt to acquire the tasklet lock
if (!atomic_read(&t->count)) {// Check if the count counter is 0
if (!test_and_clear_bit(TASKLET_STATE_SCHED,
&t->state))
BUG();// If the state flag is incorrect, an error occurs
if (t->use_callback)
t->callback(t);
else
t->func(t->data);// Execute the tasklet handler function
tasklet_unlock(t);// Unlock the tasklet
continue;
}
tasklet_unlock(t);// Unlock the tasklet
}
// Disable local interrupts
local_irq_disable();
// Add the current tasklet to the tail of tasklet_head
t->next = NULL;
// Update the tail pointer
*tl_head->tail = t;
tl_head->tail = &t->next;
// Trigger a softirq
__raise_softirq_irqoff(softirq_nr);
// Enable local interrupts
local_irq_enable();
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// kernel/softirq.c
static void __tasklet_schedule_common(struct tasklet_struct *t,
struct tasklet_head __percpu *headp,
unsigned int softirq_nr)
{
struct tasklet_head *head;
unsigned long flags;
// Save the current interrupt state and disable local interrupts
local_irq_save(flags);
// Get the tasklet_head pointer of the current CPU
head = this_cpu_ptr(headp);
// Add the current tasklet to the tail of tasklet_head
t->next = NULL;
// Update the tail pointer of tasklet_head
*head->tail = t;
head->tail = &(t->next);
// Trigger the specified softirq
raise_softirq_irqoff(softirq_nr);
// Restore the interrupt state
local_irq_restore(flags);
}


void __tasklet_schedule(struct tasklet_struct *t)
{
__tasklet_schedule_common(t, &tasklet_vec,
TASKLET_SOFTIRQ);
}
EXPORT_SYMBOL(__tasklet_schedule);

void __tasklet_hi_schedule(struct tasklet_struct *t)
{
__tasklet_schedule_common(t, &tasklet_hi_vec,
HI_SOFTIRQ);
}
EXPORT_SYMBOL(__tasklet_hi_schedule);

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

  1. 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.

  2. 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.

  3. 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

  1. 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.
  2. 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.
  3. 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 kernelused 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
2
3
4
5
6
7
8
9
10
11
// include/linux/workqueue.h
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;/* work queue handler function */
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};

typedef void (*work_func_t)(struct work_struct *work);//work function

INIT_WORK()

ItemDescription
Macro definitionINIT_WORK(_work, _func)
Header file#include <linux/workqueue.h>
Parameter workWork item to initialize (work_structstructure)
Parameter funcHandler function corresponding to the work item
FunctionInitialize a work item, binding it to the specified execution function, in preparation for subsequent scheduling.
Return valueNo return value

DECLARE_WORK()

ItemDescription
Macro definitionDECLARE_WORK(name, func);
Header file#include <linux/workqueue.h>
Parameter nameWork item (work_structtype) variable name
Parameter funcThe handler function corresponding to the work item
FunctionDefine and initialize a work item, equivalent to definingstruct work_structand usingINIT_WORK()to initialize.
Return valueNo return value

schedule_work()

ItemDescription
Function definitionbool schedule_work(struct work_struct *work);
Header file#include <linux/workqueue.h>
Parameter workPointer to the work item to be scheduled
FunctionAdd the work item to the system work queue, requesting the scheduler to execute it at an appropriate time.
Return valuetrue: Successfully submitted to the work queue;
false: Submission failed or the work item is already in the queue

cancel_work_sync()

ItemDescription
Function definitionbool cancel_work_sync(struct work_struct *work);
Header file#include <linux/workqueue.h>
Parameter workPointer to the work item to be canceled
FunctionSynchronously cancel work item scheduling: remove it if in the queue; if executing, wait for completion before returning.
Return valuetrue: Successfully canceled;
false: The work is not in a waiting or running state

flush_work()

ItemDescription
Function Definitionbool flush_work(struct work_struct *work);
Header File#include <linux/workqueue.h>
Parameter workPointer to the work item to wait for completion
FunctionWait 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 Valuetrue: 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <linux/module.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/delay.h>

#define TEST_GPIO_PIN 101

static int irq;
struct work_struct test_work;

void test_work_func(struct work_struct *work)
{
msleep(1000);
pr_info("test_work_func is called\n");
}

irqreturn_t test_irq_handler(int irq, void *dev_id)
{
pr_info("test_irq_handler is called\n");
// Bottom Half of Interrupt
// Submit work item to work queue
schedule_work(&test_work);
return IRQ_RETVAL(IRQ_HANDLED);
}

static int __init workqueue_test_init(void)
{
int ret;
irq = gpio_to_irq(TEST_GPIO_PIN);
if (irq < 0)
return -ENODEV;
pr_info("irq is %d\n", irq);
ret = request_irq(irq, test_irq_handler, IRQF_TRIGGER_RISING, "test", NULL);
if (ret < 0) {
free_irq(irq, NULL);
return -ENODEV;
}

INIT_WORK(&test_work, test_work_func);

return 0;
}

static void __exit workqueue_test_exit(void)
{
free_irq(irq, NULL);
// Before module exit, ensure all submitted work items have completed and remove them from the work queue
cancel_work_sync(&test_work);

}

module_init(workqueue_test_init);
module_exit(workqueue_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("This is a test for workqueue");

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// kernel/workqueue.c
/*
* The externally visible workqueue. It relays the issued work items to
* the appropriate worker_pool through its pool_workqueues.
*/
struct workqueue_struct {
struct list_head pwqs; /* WR: all pwqs of this wq */
struct list_head list; /* PR: list of all workqueues */

struct mutex mutex; /* protects this wq */
int work_color; /* WQ: current work color */
int flush_color; /* WQ: current flush color */
atomic_t nr_pwqs_to_flush; /* flush in progress */
struct wq_flusher *first_flusher; /* WQ: first flusher */
struct list_head flusher_queue; /* WQ: flush waiters */
struct list_head flusher_overflow; /* WQ: flush overflow list */

struct list_head maydays; /* MD: pwqs requesting rescue */
struct worker *rescuer; /* MD: rescue worker */

int nr_drainers; /* WQ: drain in progress */
int saved_max_active; /* WQ: saved pwq max_active */

struct workqueue_attrs *unbound_attrs; /* PW: only for unbound wqs */
struct pool_workqueue *dfl_pwq; /* PW: only for unbound wqs */

#ifdef CONFIG_SYSFS
struct wq_device *wq_dev; /* I: for sysfs interface */
#endif
#ifdef CONFIG_LOCKDEP
char *lock_name;
struct lock_class_key key;
struct lockdep_map lockdep_map;
#endif
char name[WQ_NAME_LEN]; /* I: workqueue name */

/*
* Destruction of workqueue_struct is RCU protected to allow walking
* the workqueues list without grabbing wq_pool_mutex.
* This is used to dump all workqueues from sysrq.
*/
struct rcu_head rcu;

/* hot fields used during command issue, aligned to cacheline */
unsigned int flags ____cacheline_aligned; /* WQ: WQ_* flags */
struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */
struct pool_workqueue __rcu *numa_pwq_tbl[]; /* PWR: unbound pwqs indexed by node */
};

create_workqueue()

ItemDescription
Function Definitionstruct workqueue_struct *create_workqueue(const char *name);
Header File#include <linux/workqueue.h>
Parameter nameName of the created work queue
FunctionCreates a per-CPU work queue (one worker thread per CPU).
Return ValueSuccess:workqueue_struct*pointer;
Failure:NULL

create_singlethread_workqueue()

ItemDescription
Macro definitioncreate_singlethread_workqueue(name)
Macro expands to:alloc_workqueue("%s", WQ_SINGLE_THREAD, 1, name)
Header file#include <linux/workqueue.h>
Parameter nameWork queue name
FunctionCreate a work queue bound to a single CPU and a single worker thread.
Return valueSuccess:workqueue_struct*pointer;
Failure:NULL

alloc_workqueue()

ProjectDescription
Function Definitionstruct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active, …);
Header File#include <linux/workqueue.h>
Parameter fmtName format string for the workqueue (similar toprintf), used to identify the workqueue in the kernel (e.g., appearing in/proc/workqueueor logs).
Parameter flagsFlags 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_activeSpecifies the maximum number of work items that can be executed concurrently on each CPU for this work queue.
• Set to1indicates serial execution (at most one work runs at a time);
• Set to0indicates 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.
FunctionDynamically creates and initializes a dedicated work queue (workqueue), returning its pointer. Compared to the deprecatedcreate_workqueue(), it provides finer control and better scalability.
Return ValueOn success, returns a pointer tostruct workqueue_struct;
On failure, returnsNULL(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
2
// Create a custom work queue that is memory-reclaimable, CPU-unbounded, and executes serially.
wq = alloc_workqueue("my_custom_wq", WQ_MEM_RECLAIM | WQ_UNBOUND, 1);

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
2
3
#define alloc_ordered_workqueue(fmt, flags, args...)			\
alloc_workqueue(fmt, WQ_UNBOUND | __WQ_ORDERED | \
__WQ_ORDERED_EXPLICIT | (flags), 1, ##args)
  • 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
2
#define create_workqueue(name)						\
alloc_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, 1, (name))
  • 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
2
3
#define create_freezable_workqueue(name)				\
alloc_workqueue("%s", __WQ_LEGACY | WQ_FREEZABLE | WQ_UNBOUND | \
WQ_MEM_RECLAIM, 1, (name))
  • create_singlethread_workqueue
    • Create a single-threaded, serial execution work queue.
    • Essentially aalloc_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
2
#define create_singlethread_workqueue(name)				\
alloc_ordered_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, name)

queue_work()

ItemDescription
Function Definitionbool queue_work(struct workqueue_struct *wq, struct work_struct *work);
Header File#include <linux/workqueue.h>
Parameter wqTarget work queue pointer
Parameter workThe work item to be added to the work queue
FunctionAdds a work item to the specified work queue, and the kernel scheduler selects an appropriate CPU for execution.
Return valuetrue: Successfully added to the queue;
false: Failed to add (e.g., the work item is already pending in the queue)

queue_work_on()

ItemDescription
Function definitionbool queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work);
Header file#include <linux/workqueue.h>
Parameter cpuSpecifies on which CPU to execute the work item
Parameter wqPointer to the target work queue
Parameter workThe work item to be added to the work queue
FunctionAdds a work item to the specified work queue of a designated CPU, awaiting execution.
Return valuetrue: Successfully added to the queue;
false: Failed to add (e.g., already in the queue)

cancel_work_sync()

ItemDescription
Function definitionbool cancel_work_sync(struct work_struct *work);
Header file#include <linux/workqueue.h>
Parameter workPointer to the work item to be canceled
FunctionSynchronously cancels a work item; if the work is executing, waits for completion before returning.
Return valuetrue: Successfully canceled;
false: Job not in queue or not cancelable

flush_work_queue()

ItemDescription
Function definitionvoid flush_workqueue(struct workqueue_struct *wq);
Header file#include <linux/workqueue.h>
Parameter wqWork queue to flush
FunctionFlush the work queue, waiting for all submitted but not yet executed jobs to complete.
Return valueNo return value

destroy_work_queue()

ItemDescription
Function Definitionvoid destroy_workqueue(struct workqueue_struct *wq);
Header File#include <linux/workqueue.h>
Parameter wqWork queue to be destroyed
FunctionDelete 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 ValueNo return value

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <linux/module.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/delay.h>

#define TEST_GPIO_PIN 101

static int irq;

static struct workqueue_struct *test_wq;
static struct work_struct test_work;
// Interrupt bottom half, work handler function
irqreturn_t test_irq_handler(int irq, void *dev_id)
{
pr_info("test_irq_handler is called\n");
queue_work(test_wq, &test_work);
// queue_work_on(0, test_wq, &test_work);
return IRQ_RETVAL(IRQ_HANDLED);
}
// Interrupt top half, interrupt handler function
void test_work_func(struct work_struct *work)
{
msleep(1000);
pr_info("test_work_func is called\n");
}

static int __init custom_workqueue_test_init(void)
{
int ret;

irq = gpio_to_irq(TEST_GPIO_PIN);
if (irq < 0) {
ret = -ENODEV;
goto get_irq_fail;
}

ret = request_irq(irq, test_irq_handler, IRQF_TRIGGER_RISING, "test", NULL);
if (ret < 0)
goto request_irq_fail;

// Create work queue
test_wq = create_workqueue("test");
if (IS_ERR_OR_NULL(test_wq))
goto create_workqueue_fail;
// Initialize work item
INIT_WORK(&test_work, test_work_func);

return 0;

create_workqueue_fail:
free_irq(irq, NULL);
request_irq_fail:
get_irq_fail:
return ret;
}

static void __exit custom_workqueue_test_exit(void)
{
free_irq(irq, NULL);// Disable new interrupts
// Cancel work item
// flush_work(&test_work);
// Flush the work queue, waiting for all submitted but not yet executed work to complete
flush_workqueue(test_wq);
// Destroy the work queue to release resources
destroy_workqueue(test_wq);

}

module_init(custom_workqueue_test_init);
module_exit(custom_workqueue_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("This is a test for custom workqueue");

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.

  1. 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.
  2. 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:

Voltage change after pressing the button under ideal conditions
Voltage change after pressing the button under ideal conditions

In actual situations:

In actual situations, the voltage will exhibit jitter
In actual situations, the voltage will exhibit jitter

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

Using a timer for debouncing
Using a timer for 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
2
3
4
5
6
7
8
9
// include/linux/workqueue.h
struct delayed_work {
struct work_struct work;
struct timer_list timer;// Timer, used for delayed execution of work

/* target workqueue and CPU ->timer uses to queue ->work */
struct workqueue_struct *wq;
int cpu;
};

DECLARE_DELAYED_WORK()

ItemDescription
Macro DefinitionDECLARE_DELAYED_WORK(n, f);
Header File#include <linux/workqueue.h>
Parameter nDelayed Work (delayed_work) Variable Name
Parameter fHandler Function for Delayed Work (void (*f)(struct work_struct *)
FunctionStatically define and initialize a delayed work item.
Return ValueNo Return Value

INIT_DELAYED_WORK()

ItemDescription
Macro definitionINIT_DELAYED_WORK(work, func);
Header file#include <linux/workqueue.h>
Parameter workPointer to the delayed work structure to be initialized (delayed_work
Parameter funcHandler function for the delayed work
FunctionDynamically initialize a delayed work item, while initializing the internalwork_structandtimer
Return valueNo return value

schedule_delayed_work()

ItemDescription
Function definitionbool schedule_delayed_work(struct delayed_work *dwork, unsigned long delay);
Header file#include <linux/workqueue.h>
parameter dworkThe delayed work item to be scheduled
parameter delayDelay time (in jiffies)
FunctionSubmit the delayed work to the system default work queue and execute it after the specified delay.
Return valuetrue: Successfully submitted;
false: Submission failed

queue_delayed_work()

ItemDescription
Function definitionbool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay);
Header file#include <linux/workqueue.h>
parameter wqSpecify the target work queue for execution
Parameter dworkThe delayed work item to be scheduled
Parameter delayDelay time (in jiffies)
FunctionAdds delayed work to a custom work queue and executes it after the specified delay.
Return valuetrue: Successfully submitted;
false: Submission failed

cancel_delayed_work_sync()

ItemDescription
Function definitionbool cancel_delayed_work_sync(struct delayed_work *dwork);
Header file#include <linux/workqueue.h>
Parameter dworkThe delayed work item to be canceled
FunctionSynchronously cancel delayed work scheduling: if the work has been executed or is being executed, wait for its completion.
Return valuetrue: successfully canceled;
false: unable to cancel or work already completed
1
2
3
4
if ( !cancel_delayed_work( &thework) ){
flush_workqueue(myqueue);
destroy_workqueue(myqueue);
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <linux/module.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/delay.h>

#define TEST_GPIO_PIN 101
static int irq;

void test_delayed_work_func(struct work_struct *work)
{
pr_info("test_delayed_work_func is called\n");
msleep(1000);
}

struct delayed_work test_delayed_work;

irqreturn_t test_irq_handler(int irq, void *dev_id)
{
pr_info("test_irq_handler is called\n");
// Submit to the default work queue
schedule_delayed_work(&test_delayed_work, 3 * HZ);
return IRQ_RETVAL(IRQ_HANDLED);
}

static int __init delayed_work_test_init(void)
{
int ret;
irq = gpio_to_irq(TEST_GPIO_PIN);

if (irq < 0)
return -ENODEV;

ret = request_irq(irq, test_irq_handler, IRQF_TRIGGER_RISING, "test", NULL);

if (ret < 0)
return -ENODEV;

INIT_DELAYED_WORK(&test_delayed_work, test_delayed_work_func);

return 0;
}

static void __exit delayed_work_test_exit(void)
{
free_irq(irq, NULL);
// Ensure work completes before exiting
flush_delayed_work(&test_delayed_work);
}

module_init(delayed_work_test_init);
module_exit(delayed_work_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@163.com>");
MODULE_DESCRIPTION("This is a test for delayed work");

Passing parameters to work queue

The main idea relies oncontainer_ofmacro to place work_struct into a custom structure

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <linux/module.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
#include <linux/delay.h>

#define TEST_GPIO_PIN 101

struct drv_data {
int irq;
struct work_struct test_work;
int arg;
};

static struct drv_data *drv_dat;

void test_work_func(struct work_struct *work)
{
struct drv_data *drv_dat = container_of(work, struct drv_data, test_work);
pr_info("test_work_func is called, arg is %d\n", drv_dat->arg);
msleep(1000);
}

irqreturn_t test_irq_handler(int irq, void *dev_id)
{
pr_info("test_irq_handler is called\n");
schedule_work(&drv_dat->test_work);
return IRQ_RETVAL(IRQ_HANDLED);
}

static int __init test_arg_pass_init(void)
{
int ret;
drv_dat = kzalloc(sizeof(struct drv_data), GFP_KERNEL);
if (drv_dat == NULL) {
ret = -ENOMEM;
goto kzalloc_fail;
}
drv_dat->irq = gpio_to_irq(TEST_GPIO_PIN);
if (drv_dat->irq < 0) {
ret = -ENODEV;
goto fail;
}

ret = request_irq(drv_dat->irq, test_irq_handler, IRQF_TRIGGER_RISING, "test", NULL);
if (ret < 0) {
ret = -ENODEV;
goto fail;
}
drv_dat->arg = 0x6;
INIT_WORK(&drv_dat->test_work, test_work_func);

return 0;
fail:
kfree(drv_dat);
kzalloc_fail:
return ret;
}

static void __exit test_arg_pass_exit(void)
{
free_irq(drv_dat->irq, NULL);
flush_work(&drv_dat->test_work);
kfree(drv_dat);
}

module_init(test_arg_pass_init);
module_exit(test_arg_pass_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("This is a test for passing args to work func");

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

CMWQ
CMWQ

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//include/linux/workqueue.h
/**
* alloc_workqueue - allocate a workqueue
* @fmt: printf format for the name of the workqueue
* @flags: WQ_* flags
* @max_active: max in-flight work items, 0 for default
* remaining args: args for @fmt
*
* Allocate a workqueue with the specified parameters. For detailed
* information on WQ_* flags, please refer to
* Documentation/core-api/workqueue.rst.
*
* RETURNS:
* Pointer to the allocated workqueue on success, %NULL on failure.
*/
struct workqueue_struct *alloc_workqueue(const char *fmt,
unsigned int flags,
int max_active, ...);

ItemDescription
Function Definitionstruct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active, …);
Header File#include <linux/workqueue.h>
Parameter fmtFormat 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 flagsFlags 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_activespecifies the maximum number of concurrently executing work items per CPU for this workqueue.
• Set to1indicates serial execution (at most one work running at a time);
• Set to0indicates 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.
FunctionDynamically create and initialize a dedicated workqueue, returning its pointer. Compared to the deprecatedcreate_workqueue(), it provides finer control and better scalability.
Return ValueOn success, returns a pointer tostruct workqueue_struct;
on failure, returnsNULL(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
2
// Create a custom work queue that is reclaimable, CPU-unbound, and executes serially.
wq = alloc_workqueue("my_custom_wq", WQ_MEM_RECLAIM | WQ_UNBOUND, 1);

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
2
3
#define alloc_ordered_workqueue(fmt, flags, args...)			\
alloc_workqueue(fmt, WQ_UNBOUND | __WQ_ORDERED | \
__WQ_ORDERED_EXPLICIT | (flags), 1, ##args)
  • 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
2
#define create_workqueue(name)						\
alloc_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, 1, (name))
  • 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
2
3
#define create_freezable_workqueue(name)				\
alloc_workqueue("%s", __WQ_LEGACY | WQ_FREEZABLE | WQ_UNBOUND | \
WQ_MEM_RECLAIM, 1, (name))
  • create_singlethread_workqueue
    • Create a single-threaded, serial execution work queue.
    • Essentially aalloc_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
2
#define create_singlethread_workqueue(name)				\
alloc_ordered_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, name)
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/workqueue.h>

#define TEST_GPIO_IN 101
static int irq;
static struct work_struct test_work;
static struct workqueue_struct *cmwq;

void test_work_func(struct work_struct *work)
{
msleep(1000);
pr_info("test_work_func is called\n");
}

irqreturn_t test_irq_handler(int irq, void *dev_id)
{
pr_info("test_irq_handler is called\n");
queue_work(cmwq, &test_work);
return IRQ_HANDLED;
}

static int __init cmwq_test_init(void)
{
int ret;
irq = gpio_to_irq(TEST_GPIO_IN);
if (irq < 0)
return -ENODEV;

ret = request_irq(irq, test_irq_handler, IRQF_TRIGGER_RISING, "test_irq", NULL);
if (ret < 0)
return -ENODEV;

cmwq = alloc_workqueue("test_workqueue", WQ_UNBOUND | WQ_SYSFS, 0);
if (cmwq == NULL) {
free_irq(irq, NULL);
return -EFAULT;
}
INIT_WORK(&test_work, test_work_func);

return 0;
}

static void __exit cmwq_test_exit(void)
{
free_irq(irq, NULL);
flush_workqueue(cmwq);
destroy_workqueue(cmwq);
}

module_init(cmwq_test_init);
module_exit(cmwq_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("This is a test for Concurrency Managed Work Queue");

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
2
3
4
5
// include/linux/interrupt.h
extern int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags, const char *name, void *dev);
ProjectDescription
Function Definitionint 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 irqInterrupt number (IRQ line number)
Parameter handlerTop-half handler: the fast handler executed first when an interrupt occurs.
If it returnsIRQ_WAKE_THREADthen the bottom-half thread is woken up.
Can be set toNULLto use the system default handler.
Parameter thread_fnThreaded interrupt handler (bottom-half), executed in a kernel thread. If it isNULLthen interrupt threading is not used.
Parameter irqflagsInterrupt attribute flags, such asIRQF_TRIGGER_RISINGIRQF_SHAREDetc.
Parameter devnameInterrupt name, used to identify the device
Parameter dev_idDevice identifier passed to the handler function (usually a device structure pointer)
FunctionRegister a threaded interrupt, including top-half and bottom-half thread processing, to improve the real-time performance and schedulability of interrupt handling.
Return valueSuccess: 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/delay.h>

#define TEST_GPIO_PIN 101

static int irq;

irqreturn_t test_irq_top_half_handler(int irq, void *dev_id)
{
pr_info("test_irq_handler is called\n");
// Defer interrupt work to the bottom half, handled by kernel threads
return IRQ_WAKE_THREAD;
}
irqreturn_t test_irq_bottom_half_handler(int irq, void *dev_id)
{
msleep(1000);
pr_info("threaded_irq_handler is called\n");
return IRQ_HANDLED;
}

static int __init threaded_irq_test_init(void)
{
int ret;
irq = gpio_to_irq(TEST_GPIO_PIN);
if (irq < 0)
return -ENODEV;
ret = request_threaded_irq(irq, test_irq_top_half_handler, test_irq_bottom_half_handler,
IRQF_TRIGGER_RISING, "test", NULL);
if (ret < 0)
return ret;
return 0;
}

static void __exit threaded_irq_test_exit(void)
{
free_irq(irq, NULL);
}

module_init(threaded_irq_test_init);
module_exit(threaded_irq_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("This is a test sample for threaded irq");