Linux驱动——中断子系统框架

一、中断

轮询是 CPU 主动问:

1
2
3
4
while (1) {
if (设备有事件)
处理事件;
}

中断是设备主动通知 CPU:

1
2
3
4
5
6
设备发生事件
-> 触发中断线
-> 中断控制器通知 CPU
-> CPU 跳转到 Linux 中断入口
-> Linux 找到对应 irq_desc
-> 调用驱动注册的 handler

中断的核心价值在于不需要 CPU 一直轮询,并且在此期间 CPU 可以做别的事情。中断适合按键、网卡、摄像头、DMA、VPU、NPU 这类异步事件。

但中断上下文有严格限制:

1
2
3
4
5
不能睡眠
不能调用 msleep()
不能拿 mutex
不能 copy_to_user()
不能直接做耗时操作

因为中断上下文没有对应的进程,调度器无法让它睡眠等待。

中断不是“用户态调用驱动”,而是硬件主动打断 CPU:

1
2
3
4
5
6
GPIO 电平变化
按键按下
摄像头 VSYNC
网卡收包
VPU/NPU 完成
DMA 传输完成

这些事件都可能通过中断通知 CPU。Linux 不能把每个中断都交给不同驱动随意处理,而是有一套统一框架:

1
2
3
4
5
硬件中断控制器
-> IRQ domain
-> Linux IRQ number / virq
-> irq_desc
-> request_irq 注册的 handler

二、中断子系统框架

从硬件到驱动,一次中断大致经过:

1
2
3
4
5
6
7
8
9
10
11
12
13
硬件设备
↓
中断控制器 / GPIO controller / GIC
↓
IRQ domain 映射
↓
Linux IRQ number: virq
↓
irq_desc[irq]
↓
irqaction / handler
↓
驱动的 irq_handler_t

驱动一般不直接操作 GIC 或 irq_desc,而是用标准 API:

1
2
3
4
5
request_irq()
devm_request_irq()
request_threaded_irq()
devm_request_threaded_irq()
free_irq()

三、上半部与下半部

一个真实中断往往包含两类事情:

1
2
3
4
5
6
7
8
9
10
11
12
必须马上做:
清中断状态
记录事件
唤醒等待者
调度后续处理

可以晚一点做:
拷贝数据
格式转换
投递队列
访问慢速设备
加 mutex

所以内核把中断处理拆成上半部和下半部:

部分 上下文 做什么
上半部 hardirq 中断上下文 最快速度确认事件、记数、清中断、调度下半部
下半部 softirq / 进程上下文 耗时处理、拷贝数据、投递队列、唤醒用户态

下半部常见机制:

机制 上下文 能否睡眠 适合
softirq 中断相关上下文 不能 网络等高吞吐场景
tasklet softirq 不能 旧的轻量延后处理
workqueue 进程上下文 能 需要睡眠、加锁、耗时的处理
threaded IRQ 内核线程 能 处理逻辑天然属于该中断

可以按下面的判断表选机制:

需求 推荐机制
只是为了不丢事件,做最少处理 hardirq 上半部
极短、不能睡眠、对延迟敏感 tasklet
需要睡眠、拿 mutex、做耗时事情 workqueue
处理逻辑天然属于这个中断,希望内核帮你管理线程 threaded IRQ
只是想等一个事件发生 waitqueue / completion
用户态同时等多个设备 poll / epoll + waitqueue

四、从设备树到中断号

设备树里常见的中断描述有两类。

3.1 直接写 interrupts

1
2
3
4
5
my-device@fe000000 {
compatible = "vendor,my-device";
reg = <0xfe000000 0x1000>;
interrupts = <GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>;
};

驱动里可以:

1
int irq = platform_get_irq(pdev, 0);

适合 SoC 内部外设,比如 DMA、VPU、I2C、SPI、UART。

3.2 用 GPIO 作为中断源

1
2
3
4
5
edge-irq {
compatible = "edge,irq-lab";
irq-gpios = <&gpio0 1 GPIO_ACTIVE_HIGH>;
status = "okay";
};

驱动里可以:

1
2
priv->irq_gpio = devm_gpiod_get_optional(dev, "irq", GPIOD_IN);
priv->irq = gpiod_to_irq(priv->irq_gpio);

适合按键、红外、传感器、编码器、外部触发信号。

五、中断处理函数

中断处理函数的原型如下:

1
irqreturn_t (*irq_handler_t) (int, void *)

参数:

  • 第一个参数是要中断处理函数要相应的中断号
  • 第二个参数是一个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构

返回值:

  • 中断处理函数的返回值为 irqreturn_t 类型,定义如下:

    1
    2
    3
    4
    5
    6
    enum irqreturn {
    IRQ_NONE = (0 << 0), // 该中断对应的设备并不是在注册处理函数期间指定的产生源时
    IRQ_HANDLED = (1 << 0), // 中断处理程序被正确调用且确实是它对应的设备产生了中断
    IRQ_WAKE_THREAD = (1 << 1), // 需要唤醒 threaded handler
    };
    typedef enum irqreturn irqreturn_t;

    一般中断服务函数返回值使用这种形式:return IRQ_RETVAL(IRQ_HANDLED)

用户需要自己来实现中断处理函数,例如:

1
2
3
4
static irqreturn_t my_handler(int irq, void *data)
{
return IRQ_HANDLED;
}

注意,中断处理函数要保证快进快出,可以做一些比如:记录计数、唤醒等待者、调度 workqueue 等工作;不要做 sleep、打印日志等耗时任务

六、注册中断

典型写法:

1
2
3
4
5
ret = devm_request_irq(dev, irq, handler,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"irq_lab", priv);
if (ret)
return dev_err_probe(dev, ret, "failed to request irq %d\n", irq);

参数:

参数 含义
dev 设备指针,用于 devm 自动释放
irq 中断号
handler 中断处理函数
flags 触发方式、共享标志等
name /proc/interrupts 中显示的名字
priv 传给 handler 的 data

常见触发方式:

flags 含义
IRQF_TRIGGER_RISING 上升沿触发
IRQF_TRIGGER_FALLING 下降沿触发
IRQF_TRIGGER_HIGH 高电平触发
IRQF_TRIGGER_LOW 低电平触发
IRQF_TRIGGER_RISING + IRQF_TRIGGER_FALLING 双边沿触发
IRQF_SHARED 共享中断

七、实例

irq_lab.c:

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// SPDX-License-Identifier: GPL-2.0

#include <linux/atomic.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/sysfs.h>

#define IRQ_LAB_NAME "irq_lab"

struct irq_lab {
struct gpio_desc *irq_gpio;
int irq;
atomic_t irq_count;
};

static irqreturn_t irq_lab_handler(int irq, void *data)
{
struct irq_lab *priv = data;

atomic_inc(&priv->irq_count);
return IRQ_HANDLED;
}

static ssize_t irq_count_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct irq_lab *priv = dev_get_drvdata(dev);

return sysfs_emit(buf, "%d\n", atomic_read(&priv->irq_count));
}
static DEVICE_ATTR_RO(irq_count);

static ssize_t trigger_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
struct irq_lab *priv = dev_get_drvdata(dev);

irq_lab_handler(priv->irq, priv);
return count;
}
static DEVICE_ATTR_WO(trigger);

static struct attribute *irq_lab_attrs[] = {
&dev_attr_irq_count.attr,
&dev_attr_trigger.attr,
NULL,
};
ATTRIBUTE_GROUPS(irq_lab);

static int irq_lab_probe(struct platform_device *pdev)
{
struct irq_lab *priv;
int ret;

priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv) {
return -ENOMEM;
}

atomic_set(&priv->irq_count, 0);
platform_set_drvdata(pdev, priv);

priv->irq_gpio = devm_gpiod_get_optional(&pdev->dev, "irq", GPIOD_IN);
if (IS_ERR(priv->irq_gpio)) {
return dev_err_probe(&pdev->dev, PTR_ERR(priv->irq_gpio),
"failed to get irq gpio\n");
}

if (!priv->irq_gpio) {
dev_warn(&pdev->dev, "no irq-gpios property; software trigger only\n");
priv->irq = -1;
return 0;
}

priv->irq = gpiod_to_irq(priv->irq_gpio);
if (priv->irq < 0) {
return dev_err_probe(&pdev->dev, priv->irq, "gpiod_to_irq failed\n");
}

ret = devm_request_irq(&pdev->dev, priv->irq, irq_lab_handler,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
IRQ_LAB_NAME, priv);
if (ret) {
return dev_err_probe(&pdev->dev, ret, "failed to request irq %d\n",
priv->irq);
}

dev_info(&pdev->dev, "probed: irq=%d\n", priv->irq);
return 0;
}

static int irq_lab_remove(struct platform_device *pdev)
{
struct irq_lab *priv = platform_get_drvdata(pdev);

dev_info(&pdev->dev, "removed: irq=%d count=%d\n",
priv->irq, atomic_read(&priv->irq_count));
return 0;
}

static const struct of_device_id irq_lab_of_match[] = {
{ .compatible = "edge,irq-lab" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, irq_lab_of_match);

static struct platform_driver irq_lab_driver = {
.probe = irq_lab_probe,
.remove = irq_lab_remove,
.driver = {
.name = IRQ_LAB_NAME,
.of_match_table = irq_lab_of_match,
.dev_groups = irq_lab_groups,
},
};

module_platform_driver(irq_lab_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("orange-pi-edge");
MODULE_DESCRIPTION("Minimal IRQ subsystem driver for Linux driver learning");
MODULE_VERSION("1.0");

irq_lab_testdev.c:

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
// SPDX-License-Identifier: GPL-2.0

#include <linux/gpio/machine.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/platform_device.h>

#define IRQ_LAB_NAME "irq_lab"

static struct platform_device *irq_lab_pdev;

static char *irq_chip = "gpio0";
static int irq_line = 1;
module_param(irq_chip, charp, 0444);
module_param(irq_line, int, 0444);
MODULE_PARM_DESC(irq_chip, "GPIO chip label for the interrupt input");
MODULE_PARM_DESC(irq_line, "GPIO line offset for the interrupt input");

static struct gpiod_lookup_table irq_lab_gpio_lookup = {
.dev_id = IRQ_LAB_NAME,
.table = {
GPIO_LOOKUP("", 0, "irq", GPIO_ACTIVE_HIGH),
{ }
},
};

static int __init irq_lab_testdev_init(void)
{
irq_lab_gpio_lookup.table[0].key = irq_chip;
irq_lab_gpio_lookup.table[0].chip_hwnum = irq_line;
gpiod_add_lookup_table(&irq_lab_gpio_lookup);

irq_lab_pdev = platform_device_register_simple(IRQ_LAB_NAME, -1, NULL, 0);
if (IS_ERR(irq_lab_pdev)) {
gpiod_remove_lookup_table(&irq_lab_gpio_lookup);
return PTR_ERR(irq_lab_pdev);
}

pr_info(IRQ_LAB_NAME "_testdev: registered %s with %s line %d\n",
IRQ_LAB_NAME, irq_chip, irq_line);
return 0;
}

static void __exit irq_lab_testdev_exit(void)
{
platform_device_unregister(irq_lab_pdev);
gpiod_remove_lookup_table(&irq_lab_gpio_lookup);
pr_info(IRQ_LAB_NAME "_testdev: unregistered\n");
}

module_init(irq_lab_testdev_init);
module_exit(irq_lab_testdev_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Test helper: platform_device + GPIO lookup for irq_lab");

Makefile:

1
2
3
4
5
6
7
8
9
10
11
12
obj-m += irq_lab.o
obj-m += irq_lab_testdev.o

KDIR ?= /lib/modules/$(shell uname -r)/build

all:
$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean

.PHONY: all clean

运行查看:

1
2
3
4
5
6
> make
> sudo insmod irq_lab.c
> sudo insmod irq_lab_testdev.c
> sudo dmesg | grep irq_lab | tail -n 8
[43219.998579] irq_lab_testdev: registered irq_lab with gpio0 line 1
[43220.062076] irq_lab irq_lab: probed: irq=144

说明:

1
2
gpio0 line 1 -> Linux IRQ number 144
devm_request_irq() 注册成功

再看:

1
2
> grep irq_lab /proc/interrupts
144: 0 0 0 0 0 0 0 0 rockchip_gpio_irq 1 Edge irq_lab
字段 含义
144 Linux IRQ number
多个 0 每个 CPU 上此中断计数
rockchip_gpio_irq GPIO 中断控制器
1 GPIO line offset
Edge 边沿触发
irq_lab request_irq 注册的名字

Linux驱动——中断子系统框架
http://example.com/2026/08/06/Linux驱动——中断子系统框架/
作者
Yu xin
发布于
2026年8月6日
许可协议