[Linux] IRQ management

4 minute read

Introduction

This is an explanation of how interrupt management works in the Linux kernel (ver = 5.10).

 ,-->  hwirq
 |          |
 `--------> `<irq_desc>
  `           |       |
   `----------------> `<irq_data>
    `         |         |       |
     `------------------------> `<irq_chip>
              |         |                 .irq_startup
              .handler  |                 .irq_shutdown
                        .hwirq <--> irq   .irq_enable
                        .domain           .irq_disable
                        .parent_data      .irq_ack
                                          .irq_mask
                                          .irq_unmask
                                          .irq_eoi
                                          .irq_set_affinity
                                          ...
                                          .parent_device

hwirq

hwirq is the actual hardware-specific interrupt number.
The Linux kernel manages this by mapping it to a virtual interrupt number.
Developers define hardware interrupt numbers in the device-tree.

	label: device@10000000 {
		status = "okay";
		compatible = "samsung,device";
		interrupts = <GIC_SPI 38 IRQ_TYPE_LEVEL_HIGH>;
		...
	}


Number 38 is the hardware interrupt number, and this information can be found in the SoC or device datasheet.
In the Linux kernel, this is managed as hwirq.

IRQ Information Management

The Linux kernel manages IRQ information through various objects.
Structures containing interrupt information include irq_chip, irq_desc, and irq_data.

irq_desc

irq_desc is the top-level descriptor for an IRQ line.
It stores the overall state of the corresponding IRQ.
It also maps the handler information connected to that IRQ.

The basic structure looks like this (include/linux/irqdesc.h):

struct irq_desc {
    struct irq_common_data  irq_common_data;
    struct irq_data     irq_data;
    unsigned int __percpu   *kstat_irqs;
    irq_flow_handler_t  handle_irq;
    struct irqaction    *action;    /* IRQ action list */

    ...

    struct mutex        request_mutex;
    int         parent_irq;
    struct module       *owner;
    const char      *name;
} ____cacheline_internodealigned_in_smp;


Because it contains irq_data, if you know the irq_desc, you can retrieve the irq_data.
Conversely, you can use the container_of macro to get it.
This is implemented in the kernel.

irq_data

Inside irq_desc (or within its hierarchy), this structure holds the actual hardware IRQ number and information needed to control the IRQ (like irq_chip and hardware-specific flags).
It stores hardware-level IRQ numbers (hwirq), tree structure data (parent/child irq_data), and IRQ line options (masking, trigger types, etc.).
It can also store SoC or architecture-dependent data required when invoking methods provided by the irq_chip.

The basic structure looks like this (include/linux/irq.h):

struct irq_data {
    u32         mask;
    unsigned int        irq;
    unsigned long       hwirq;
    struct irq_common_data  *common;
    struct irq_chip     *chip;
    struct irq_domain   *domain;
#ifdef  CONFIG_IRQ_DOMAIN_HIERARCHY
    struct irq_data     *parent_data;
#endif
    void            *chip_data;
};


Because it contains irq_chip, if you know the irq_data, you can retrieve the irq_chip.
Conversely, you can use the container_of macro to get it.
This is implemented in the kernel.

irq_chip

This is a table of function pointers that implements operations such as activating/deactivating hardware interrupts (mask/unmask), ack, and eoi (End Of Interrupt).
It provides control functions for the corresponding IRQ line (or group).
Examples include irq_chip->irq_ack, irq_chip->irq_mask, irq_chip->irq_unmask, and irq_chip->irq_eoi.
Typically, different interrupt control logic is implemented for each hardware type, such as ARM GIC (Generic Interrupt Controller), GPIO-based interrupt controllers, or PCI interrupts.

The basic structure looks like this (include/linux/irq.h):

struct irq_chip {
    struct device   *parent_device;
    const char  *name;
    unsigned int    (*irq_startup)(struct irq_data *data);

    ...

    void        (*irq_ack)(struct irq_data *data);
    void        (*irq_mask)(struct irq_data *data);
    void        (*irq_mask_ack)(struct irq_data *data);
    void        (*irq_unmask)(struct irq_data *data);
    void        (*irq_eoi)(struct irq_data *data);

    int     (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);

    ...

    unsigned long   flags;
};


IRQ handler

In the Linux kernel, interrupt handler functions have the irqreturn_t type.
The handler for each IRQ is managed within the irqaction of the irq_desc.
(include/linux/interrupt.h)

typedef irqreturn_t (*irq_handler_t)(int, void *);

...

struct irqaction {
    irq_handler_t       handler;
    void            *dev_id;
    void __percpu       *percpu_dev_id;
    struct irqaction    *next;
    irq_handler_t       thread_fn;

    ...

    unsigned int        irq;
    unsigned int        flags;
    unsigned long       thread_flags;
    unsigned long       thread_mask;
    const char      *name;
    struct proc_dir_entry   *dir;
} ____cacheline_internodealigned_in_smp;


irqreturn

Interrupts interrupt the CPU’s normal scheduling and consume resources.
Therefore, interrupts must be processed very quickly and concisely.
Sometimes, there may be a need to perform many operations inside an interrupt handler.
The Linux kernel offers interrupt deferred processing mechanisms (such as threaded IRQs and worker threads) for this, but those will be omitted here.

Interrupt handlers must not perform complex calculations or call scheduling functions.
When the handler completes all its tasks, it returns IRQ_HANDLED.
(include/linux/irqreturn.h)

enum irqreturn {
    IRQ_NONE        = (0 << 0),
    IRQ_HANDLED     = (1 << 0),
    IRQ_WAKE_THREAD     = (1 << 1),
};


Every time a handler returns IRQ_HANDLED, the kernel increments the corresponding interrupt count by 1 in /proc/interrupts.

callstack

In the ARM-A architecture, most interrupts are managed by the GIC (Generic Interrupt Controller).
Therefore, a typical callstack looks like this:

=> ...
=> __handle_irq_event_percpu
=> handle_irq_event_percpu
=> handle_irq_event
=>handle_fasteoi_irq
=>generic_handle_irq
=>__handle_domain_irq
=>gic_handle_irq
=>el1_irq


Here, each driver’s interrupt handler is invoked within handle_fasteoi_irq.
By tracing the driver code, you can see that the interrupt handler is called in code similar to the following:

irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
    irqreturn_t retval = IRQ_NONE;
    unsigned int irq = desc->irq_data.irq;
    struct irqaction *action;

    record_irq_time(desc);

    for_each_action_of_desc(desc, action) {
        irqreturn_t res;

        ...

        trace_irq_handler_entry(irq, action);
        res = action->handler(irq, action->dev_id);
        trace_irq_handler_exit(irq, action, res);

        ...

        switch (res) {
        case IRQ_WAKE_THREAD:

        ...

            __irq_wake_thread(desc, action);

            fallthrough;    /* to add to randomness */
        case IRQ_HANDLED:
            *flags |= action->flags;
            break;
        default:
            break;
        }

        retval |= res;
    }

    return retval;
}


The part that actually calls the interrupt handler is res = action->handler(irq, action->dev_id);.
You can see that interrupt handlers are managed via the action structure, and it invokes the handler member of the action structure.

It also shows how the execution proceeds based on the handler’s return value.
It handles deferred interrupt processing if IRQ_WAKE_THREAD is returned, and then falls through to check for IRQ_HANDLED when interrupt processing is complete.

Leave a comment