gOdm@'s BLOG

linux中断系统基础使用

版权信息

warning

本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。


在 Linux 内核开发中,中断(Interrupt) 是 CPU 与硬件通信的“急救热线”。如果让 CPU 不断轮询(Polling)硬件状态,效率极低;而中断机制允许硬件在需要时“打断” CPU,极大提升了系统的并发处理能力。

本文将帮助您理解 Linux 中断处理的顶半部/底半部机制,掌握核心 API。

1. “顶半部”和“底半部”

Linux 中断处理的一个黄金法则 是:快进快出

当硬件触发中断时,CPU 会暂停当前任务,跳转到中断处理函数。如果这个函数执行时间过长(例如读取大量数据或进行复杂计算),系统就会卡顿,甚至丢失其他重要的中断信号。

为了解决这个问题,Linux 将中断处理分为两个部分:

1.1. 顶半部 (Top Half / Hard IRQ)

1.2. 底半部 (Bottom Half)

1.2.1. 常见的底半部机制

选择哪种底半部机制取决于您的具体需求:

机制 运行上下文 是否允许睡眠 适用场景
Softirq (软中断) 中断上下文 否 极高并发需求(如网卡驱动)。一般驱动很少直接用。
Tasklet 中断上下文 否 (旧) 轻量级任务。但在新内核中逐渐被 Workqueue 取代。
Workqueue (工作队列) 进程上下文 是 (推荐) 需要耗时操作、需要睡眠、需要申请大量内存时。
Threaded IRQ 进程上下文 是 (推荐) 现代驱动的首选,将中断处理直接线程化。

2. IRQ 资源编号与设备树

在现代 Linux 内核中,绝大多数 SoC 的中断都不再通过固定的“linux 内部 IRQ 号”访问,而是通过 设备树(Device Tree)描述硬件 → 内核解析 → 统一抽象(irq_domain) → 转换成 Linux IRQ 号 的方式使用。

这一层抽象非常重要,因为:

2.1. 设备树中的中断描述

在 DTS 中,中断通常由两个属性提供:

  1. interrupt-parent:指定使用哪个中断控制器
  2. interrupts:描述设备的中断号和触发方式

例如:

mydev: my-device@0 {
    compatible = "gdm,mydev";
    reg = <0x01c20000 0x1000>;

    interrupt-parent = <&gic>;
    interrupts = <33 IRQ_TYPE_LEVEL_LOW>;
};

“interrupts” 单元的含义依赖于中断控制器定义的 #interrupt-cells 属性。

2.1.1. “#interrupt-cells” 的定义

每个中断控制器节点都定义自己的中断参数格式。

例如 ARM GIC:

gic: interrupt-controller@1e001000 {
    compatible = "arm,cortex-a9-gic";
    #interrupt-cells = <3>;
    interrupt-controller;
};

表示这个中断控制器的 interrupts = <a b c> 有 3 个单元。
常见格式:

单元序号 含义
0 中断类型(0: SPI, 1: PPI)
1 硬件中断号
2 触发类型(edge/level)

2.1.2. GPIO 触发的中断

很多 GPIO 控制器也提供中断功能:

my_button {
    gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
    interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
    interrupt-parent = <&gpio1>;
};

注意:
gpios 与 interrupts 是两回事。
gpios表示驱动该设备的 GPIO 引脚,
interrupts表示触发中断的中断号。

2.2. 内核如何解析设备树中的中断

中断解析的三层结构:

DTS → irq_domain → Linux IRQ number

解析过程:

  1. 解析 interrupt-parent 属性
    内核找到你的设备是由哪个中断控制器管理。

  2. 将 interrupts 中的原生硬件 IRQ 参数交给 irq_domain
    中断控制器在初始化时,会创建对应的 irq_domain 对象,用于映射:

硬件中断号 (hwirq)
        ↓
Linux 虚拟 IRQ(virq)
  1. 返回驱动可用的 Linux IRQ
    这个 IRQ 是你在 request_irq() 中使用的。

  2. 你在驱动中拿到的 IRQ 号 不是硬件 IRQ,而是 Linux 分配的虚拟 IRQ(virq)
    所以不能写死 IRQ 数字,必须从 DTS 获取。

2.3. 在驱动中获取设备树中的 IRQ 号

2.3.1. 常用方式:platform_get_irq()

最推荐、最通用:

int irq = platform_get_irq(pdev, 0);
if (irq < 0)
    return irq;

对应 DTS:

interrupts = <33 IRQ_TYPE_LEVEL_LOW>;

如果有多个中断:

interrupts = <33 IRQ_TYPE_LEVEL_LOW>,
             <34 IRQ_TYPE_EDGE_RISING>;

驱动:

irq0 = platform_get_irq(pdev, 0);
irq1 = platform_get_irq(pdev, 1);

2.3.2. 一般 GPIO → IRQ:gpiod_to_irq()

如果是 GPIO:

irq-gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;

驱动:

gpio = gpiod_get(&pdev->dev, "irq", GPIOD_IN);
irq = gpiod_to_irq(gpio);

2.3.3. 旧方法(不推荐):irq_of_parse_and_map()

较低级 API,如今主要用于没有 platform_device 的情况。

irq = irq_of_parse_and_map(node, 0);

3. 常见API

3.1. request_irq():注册中断处理程序

int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev);

参数说明:

参数 说明
irq 中断号
handler 中断处理函数
flags 触发方式,如 IRQF_TRIGGER_FALLING
name /proc/interrupts 显示的名称
dev 传给 handler 的参数(通常设备结构体)

3.2. free_irq():释放中断

void free_irq(unsigned int irq, void *dev);

3.3. 中断处理函数(ISR)

irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct mydev *dev = dev_id;

    /* 处理硬件中断:读取状态寄存器、清中断标志… */
    ...

    return IRQ_HANDLED;
}

注意:

返回值:

3.4. enable_irq() / disable_irq()

用于在软件中暂时禁用/启用某个 IRQ:

disable_irq(irq);       // 会等待硬件中断执行完毕
disable_irq_nosync(irq); // 不等待
enable_irq(irq);

4. 底半部机制

4.1. tasklet(轻量底半部)

创建:

void my_tasklet_func(unsigned long data);  DECLARE_TASKLET(my_tasklet, my_tasklet_func, 0);

调度:

tasklet_schedule(&my_tasklet);

4.2. workqueue(更灵活)

相比 tasklet,它可以睡眠,也更能处理复杂任务。

创建工作:

static void my_work_func(struct work_struct *work)
{
    struct mydev *dev = container_of(work, struct mydev, work);
    /* 可以执行耗时任务 */
}

初始化:

INIT_WORK(&dev->work, my_work_func);

在中断里调度:

schedule_work(&dev->work);

这是最常用的底半部方式。

5. 中断处理中需要注意的事项

5.1. 不要睡眠

ISR 禁止使用:

5.2. 尽可能短

5.3. 触发方式匹配硬件

常见 flags:

6. Threaded IRQ(线程化中断)

Linux 支持把中断处理放到线程里(可睡眠)。适合驱动复杂逻辑。

request_threaded_irq(irq, NULL,
                     my_thread_fn,
                     IRQF_ONESHOT | IRQF_TRIGGER_FALLING,
                     "mydev", dev);

thread_fn 可以睡眠,非常灵活:

static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
    msleep(20);   // 可以
    return IRQ_HANDLED;
}

7. 查看中断情况

执行:

cat /proc/interrupts

可以看到:

例如:

33:      1023   GIC   edge   mydev_irq

共计约1.9k字。于2025/12/08首次发布,最后更新于2025/12/10。

本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

AI辅助创作:本文部分内容由 ChatGPT 5 生成,最终版本由作者审核与修改。了解该AI模型。

  1. 1. “顶半部”和“底半部”
    1. 1.1. 顶半部 (Top Half / Hard IRQ)
    2. 1.2. 底半部 (Bottom Half)
  2. 2. IRQ 资源编号与设备树
    1. 2.1. 设备树中的中断描述
    2. 2.2. 内核如何解析设备树中的中断
    3. 2.3. 在驱动中获取设备树中的 IRQ 号
  3. 3. 常见API
    1. 3.1. request_irq():注册中断处理程序
    2. 3.2. free_irq():释放中断
    3. 3.3. 中断处理函数(ISR)
    4. 3.4. enable_irq() / disable_irq()
  4. 4. 底半部机制
    1. 4.1. tasklet(轻量底半部)
    2. 4.2. workqueue(更灵活)
  5. 5. 中断处理中需要注意的事项
    1. 5.1. 不要睡眠
    2. 5.2. 尽可能短
    3. 5.3. 触发方式匹配硬件
  6. 6. Threaded IRQ(线程化中断)
  7. 7. 查看中断情况