Fundamentals 14 min read

Understanding Linux IRQ and SoftIRQ: From Hardware Interrupts to Deferred Handling

This article explains the fundamentals of Linux interrupt handling, covering hardware and software interrupt types, the IRQ processing flow, maskable versus non‑maskable interrupts, the need for deferred execution, and detailed mechanisms of softirqs, tasklets, and workqueues with practical code examples.

Liangxu Linux
Liangxu Linux
Liangxu Linux
Understanding Linux IRQ and SoftIRQ: From Hardware Interrupts to Deferred Handling

What Is an Interrupt?

In a time‑shared CPU, hardware tasks (e.g., disk I/O, keyboard input) and software tasks (e.g., network packet processing) compete for execution. When a task needs immediate attention, it sends an interrupt request (IRQ) to pre‑empt the current work, notifying the CPU of an event.

Types of Interrupts

External/hardware interrupts such as a key press.

Software‑generated interrupts caused by exceptions (e.g., division‑by‑zero).

Interrupts are managed by the Advanced Programmable Interrupt Controller (APIC).

Hardware Interrupt Handling Flow

Pre‑empt current task : the kernel pauses the running process.

Execute IRQ handler : the registered handler runs with the CPU.

Resume pre‑empted task : after the handler finishes, the original process continues.

Maskable vs. Non‑Maskable Interrupts

On x86_64, maskable interrupts can be disabled/enabled with cli and sti instructions:

static inline void native_irq_disable(void) {
    asm volatile("cli" : : : "memory"); // clear IF flag
}
static inline void native_irq_enable(void) {
    asm volatile("sti" : : : "memory"); // set IF flag
}

Maskable interrupts can be temporarily blocked (most IRQs). Non‑maskable interrupts cannot be blocked and are treated as higher‑priority events.

The Speed vs. Complexity Dilemma

IRQ handlers must execute extremely quickly to avoid lost events, yet they often need to perform complex work such as packet processing, creating an inherent conflict.

Deferred Interrupt Handling

To resolve this conflict, Linux splits interrupt processing into two parts:

Top half : the minimal, time‑critical work that must run in hard‑interrupt context.

Bottom half : the remaining work queued for later execution.

This deferred execution is now a generic term covering several mechanisms.

SoftIRQ Subsystem

Each CPU runs a kernel thread ksoftirqd that processes pending softirqs.

Softirqs are registered with open_softirq(softirq_id, handler). For example, network TX/RX handlers are registered as:

open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);

The CPU softirq load can be observed via top (the si column) or /proc/softirqs:

$ cat /proc/softirqs
          CPU0   CPU1  ...
HI:        2      0   ...
TIMER: 443727 467971 ...
NET_TX:  57919  65998 ...
NET_RX: 28728 5262341 ...

SoftIRQ Execution Path

When an IRQ handler finishes, do_IRQ() calls exiting_irq() which checks for pending softirqs.

If pending, invoke_softirq() wakes ksoftirqd, which runs __do_softirq() to process each pending softirq.

SoftIRQ Triggering

Softirqs are raised with raise_softirq():

void raise_softirq(unsigned int nr) {
    local_irq_save(flags);
    raise_softirq_irqoff(nr); // wake ksoftirqd
    local_irq_restore(flags);
}

For network packet reception, the IRQ handler calls napi_schedule(), which ultimately raises NET_RX_SOFTIRQ. The actual packet processing occurs in the softirq context.

Tasklet

Tasklets are a higher‑level abstraction built on top of softirqs, using the HI_SOFTIRQ and TASKLET_SOFTIRQ channels. They can be created at runtime:

void __init softirq_init(void) {
    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;
    }
    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

Tasklet structures:

struct tasklet_struct {
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};

Workqueue

Workqueues provide an asynchronous execution context similar to tasklets but run in process context, allowing sleeping and blocking operations. They are serviced by per‑CPU kworker threads.

$ systemd-cgls -k | grep kworker
├─ 5 [kworker/0:0H]
├─ 15 [kworker/1:0H]

Key structures:

struct worker_pool {
    spinlock_t lock;
    int cpu, node, id;
    unsigned int flags;
    struct list_head worklist;
    int nr_workers;
    ...
};

struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;
    struct lockdep_map lockdep_map;
};

References

Linux Inside – “Interrupts and Interrupt Handling” (https://0xax.gitbooks.io/linux-insides/content/Interrupts/linux-interrupts-9.html)

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

KernelLinuxInterruptssoftirqDeferred Execution
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.