版权信息
warning
本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
在 Linux 内核开发中,中断(Interrupt) 是 CPU 与硬件通信的“急救热线”。如果让 CPU 不断轮询(Polling)硬件状态,效率极低;而中断机制允许硬件在需要时“打断” CPU,极大提升了系统的并发处理能力。
本文将帮助您理解 Linux 中断处理的顶半部/底半部机制,掌握核心 API。
1. “顶半部”和“底半部”
Linux 中断处理的一个黄金法则 是:快进快出
当硬件触发中断时,CPU 会暂停当前任务,跳转到中断处理函数。如果这个函数执行时间过长(例如读取大量数据或进行复杂计算),系统就会卡顿,甚至丢失其他重要的中断信号。
为了解决这个问题,Linux 将中断处理分为两个部分:
1.1. 顶半部 (Top Half / Hard IRQ)
- 角色: “前台接待员”。
- 触发: 硬件直接触发。
- 特点:
- 极快:只做最紧急的事(如读取寄存器状态、清除中断标志)。
- 屏蔽中断:执行期间通常会屏蔽当前CPU的其他中断。
- 不可睡眠:严禁调用任何可能导致睡眠的函数(如
ssleep,mutex_lock,copy_from_user)。
- 任务: 登记任务,将耗时操作推迟给底半部,然后立即返回。
1.2. 底半部 (Bottom Half)
- 角色: “后台处理专员”。
- 触发: 由顶半部调度,在稍后的安全时间执行。
- 特点:
- 开中断:执行时允许响应其他中断。
- 灵活:根据机制不同,有些可以睡眠(Workqueue),有些不行(Softirq/Tasklet)。
- 常见场景: 按键消抖、处理数据包、执行长时间计算
1.2.1. 常见的底半部机制
选择哪种底半部机制取决于您的具体需求:
| 机制 | 运行上下文 | 是否允许睡眠 | 适用场景 |
|---|---|---|---|
| Softirq (软中断) | 中断上下文 | 否 | 极高并发需求(如网卡驱动)。一般驱动很少直接用。 |
| Tasklet | 中断上下文 | 否 | (旧) 轻量级任务。但在新内核中逐渐被 Workqueue 取代。 |
| Workqueue (工作队列) | 进程上下文 | 是 | (推荐) 需要耗时操作、需要睡眠、需要申请大量内存时。 |
| Threaded IRQ | 进程上下文 | 是 | (推荐) 现代驱动的首选,将中断处理直接线程化。 |
2. IRQ 资源编号与设备树
在现代 Linux 内核中,绝大多数 SoC 的中断都不再通过固定的“linux 内部 IRQ 号”访问,而是通过 设备树(Device Tree)描述硬件 → 内核解析 → 统一抽象(irq_domain) → 转换成 Linux IRQ 号 的方式使用。
这一层抽象非常重要,因为:
- SoC 的硬件 IRQ 号不是从 0 开始,也不是固定长度
- 不同中断控制器(GIC, NVIC, RISC-V PLIC …)的中断号格式完全不同
- DTS 可以统一描述,不需要驱动写死中断号
2.1. 设备树中的中断描述
在 DTS 中,中断通常由两个属性提供:
- interrupt-parent:指定使用哪个中断控制器
- 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
解析过程:
-
解析 interrupt-parent 属性
内核找到你的设备是由哪个中断控制器管理。 -
将 interrupts 中的原生硬件 IRQ 参数交给 irq_domain
中断控制器在初始化时,会创建对应的 irq_domain 对象,用于映射:
硬件中断号 (hwirq)
↓
Linux 虚拟 IRQ(virq)
-
返回驱动可用的 Linux IRQ
这个 IRQ 是你在 request_irq() 中使用的。 -
你在驱动中拿到的 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;
}
注意:
- 不能睡眠
- 不能使用可能阻塞的 API(如 msleep)
返回值:
-
IRQ_HANDLED
表示:- 中断被这个 handler 正常处理了
- 内核会认为这个中断“属于你”
-
IRQ_NONE
表示:- 这个中断不属于你
- 通常用于共享中断线(例如 PCI)
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 禁止使用:
- msleep
- mutex_lock(可能睡眠)
- schedule()
5.2. 尽可能短
- 清状态寄存器
- 准备数据
- 调度底半部
5.3. 触发方式匹配硬件
常见 flags:
- IRQF_TRIGGER_RISING
- IRQF_TRIGGER_FALLING
- IRQF_TRIGGER_HIGH
- IRQF_TRIGGER_LOW
- IRQF_ONESHOT(配合 threaded irq)
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
可以看到:
- 中断号
- 对应 CPU
- 触发次数
- 对应驱动名称
例如:
33: 1023 GIC edge mydev_irq