4、中断处理机制:中断注册request_irq、上半部与下半部(tasklet/workqueue)、中断共享与触发类型
中断,是嵌入式系统里最核心的异步事件处理方式。说白了,就是硬件告诉CPU:“我有事,你快来处理一下。” 我刚开始做驱动时,总觉得中断很简单——注册个函数不就完了?后来踩的坑多了才发现,中断处理的设计好坏,直接决定了系统的实时性和稳定性。
这一章,我们就来把中断的注册、上下半部机制、共享中断和触发类型这些关键点,一个一个说清楚。
4.1 中断注册:request_irq 的完整用法
在Linux内核里,注册一个中断处理函数,最核心的API就是 request_irq。它的原型长这样:
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev_id);
每个参数都有讲究,我一个个说:
| 参数 | 含义 | 注意事项 |
|---|---|---|
irq |
中断号 | 通常从设备树或平台数据获取,不要写死 |
handler |
中断处理函数指针 | 返回类型为 irqreturn_t |
flags |
中断标志位 | 决定触发类型、共享属性等 |
name |
设备名称 | 用于 /proc/interrupts 显示 |
dev_id |
设备标识 | 共享中断时必须唯一,用于注销时区分 |
dev_id 我通常传设备结构体指针。这样在中断处理函数里,可以直接通过 dev_id 拿到所有设备上下文,省得用全局变量。
一个典型的中断注册示例:
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = (struct my_device *)dev_id;
// 处理中断...
return IRQ_HANDLED;
}
static int my_probe(struct platform_device *pdev)
{
struct my_device *dev;
int irq, ret;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
ret = request_irq(irq, my_irq_handler,
IRQF_TRIGGER_RISING,
"my_device", dev);
if (ret) {
dev_err(&pdev->dev, "request_irq failed\n");
return ret;
}
dev->irq = irq;
platform_set_drvdata(pdev, dev);
return 0;
}
printk 或 dev_err 输出调试信息。在中断频繁触发时,这会导致系统响应变慢,甚至触发看门狗复位。中断处理函数里,能不做的事尽量别做。
4.2 上半部与下半部:为什么需要拆分?
中断处理函数执行时,CPU是关中断的(至少是当前中断线被屏蔽)。这意味着,你在中断处理函数里待得越久,系统响应其他中断的能力就越差。你想想看,如果网卡中断来了,你在里面处理了5毫秒,那串口数据可能就丢了。
所以,Linux内核把中断处理拆成了两部分:
- 上半部(Top Half): 执行时间极短的操作,比如清除中断标志、拷贝硬件寄存器数据到内存。这部分在关中断环境下执行。
- 下半部(Bottom Half): 耗时的处理逻辑,比如数据解析、协议栈递交、唤醒等待队列。这部分在开中断环境下执行。
判断标准很简单:如果这个操作可以在几十微秒内完成,就放上半部;否则,必须推给下半部。
4.3 下半部的两种主流实现:tasklet 与 workqueue
Linux内核提供了多种下半部机制,但实际项目中最常用的就两个:tasklet 和 workqueue。
4.3.1 tasklet:软中断上下文,不可睡眠
tasklet 运行在软中断上下文,说白了就是不能睡眠、不能阻塞、不能调用可能引起调度的函数(比如 copy_to_user、kmalloc(GFP_KERNEL))。它的优点是开销小,执行快。
使用步骤:
- 定义并初始化 tasklet
- 在上半部中调度 tasklet
- 实现 tasklet 处理函数
// 定义 tasklet 及其处理函数
void my_tasklet_handler(unsigned long data)
{
struct my_device *dev = (struct my_device *)data;
// 处理数据,注意不能睡眠
}
DECLARE_TASKLET(my_tasklet, my_tasklet_handler,
(unsigned long)dev);
// 上半部中调度
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = (struct my_device *)dev_id;
// 保存硬件数据到内存
dev->hw_data = readl(dev->regs + DATA_REG);
// 调度 tasklet
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
4.3.2 workqueue:进程上下文,可以睡眠
workqueue 运行在进程上下文,可以睡眠、可以调用 copy_to_user、可以申请大块内存。代价是调度开销比 tasklet 大,实时性稍差。
使用步骤:
- 定义 work_struct 并初始化
- 在上半部中调度 work
- 实现 work 处理函数
// 定义 work 及其处理函数
void my_work_handler(struct work_struct *work)
{
struct my_device *dev = container_of(work,
struct my_device, my_work);
// 可以睡眠,可以调用 copy_to_user
copy_to_user(dev->user_buf, dev->data, dev->data_len);
wake_up_interruptible(&dev->waitq);
}
// 在 probe 中初始化
INIT_WORK(&dev->my_work, my_work_handler);
// 上半部中调度
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = (struct my_device *)dev_id;
// 保存数据
dev->data_len = readl(dev->regs + LEN_REG);
memcpy_fromio(dev->data, dev->regs + DATA_REG, dev->data_len);
// 调度 work
schedule_work(&dev->my_work);
return IRQ_HANDLED;
}
4.4 中断共享:多个设备共用一根中断线
在嵌入式系统里,中断线是稀缺资源。多个设备共用一根中断线的情况很常见。共享中断的注册,靠的是 IRQF_SHARED 标志。
共享中断的几个硬性要求:
- 所有共享该中断的驱动,注册时都必须指定
IRQF_SHARED - 每个驱动的
dev_id必须唯一(通常传设备结构体指针) - 中断处理函数必须检查中断是否来自自己的设备
static irqreturn_t my_shared_handler(int irq, void *dev_id)
{
struct my_device *dev = (struct my_device *)dev_id;
// 检查中断状态寄存器,判断是否是自己设备的中断
if (!(readl(dev->regs + INT_STAT) & dev->int_mask))
return IRQ_NONE; // 不是我的中断
// 处理中断
writel(dev->int_mask, dev->regs + INT_CLR);
tasklet_schedule(&dev->tasklet);
return IRQ_HANDLED;
}
// 注册时指定 IRQF_SHARED
ret = request_irq(irq, my_shared_handler,
IRQF_TRIGGER_LOW | IRQF_SHARED,
"my_device", dev);
IRQ_HANDLED,但实际上并没有处理中断(因为没检查状态寄存器)。导致其他设备的中断被“吃掉”,系统出现随机性故障。排查了整整两天才找到原因。记住:共享中断的处理函数,一定要先检查中断是否来自自己的设备。
4.5 触发类型:边缘触发 vs 电平触发
中断触发类型,是硬件设计时就定好的。驱动必须和硬件匹配,否则中断要么不触发,要么疯狂触发。
| 触发类型 | 标志位 | 特点 | 典型场景 |
|---|---|---|---|
| 上升沿触发 | IRQF_TRIGGER_RISING |
信号从低到高跳变时触发 | 按键中断、GPIO中断 |
| 下降沿触发 | IRQF_TRIGGER_FALLING |
信号从高到低跳变时触发 | 边沿敏感的通信协议 |
| 高电平触发 | IRQF_TRIGGER_HIGH |
信号为高电平时持续触发 | 共享中断、电平敏感设备 |
| 低电平触发 | IRQF_TRIGGER_LOW |
信号为低电平时持续触发 | 共享中断、唤醒源 |
电平触发和边缘触发有个重要区别:电平触发如果不在中断处理函数里清除中断源,中断会一直触发,导致系统卡死。边缘触发如果丢失了边沿,中断就永远丢了。
4.6 知识结构总览
下面这张图,把中断处理的整个流程和关键决策点串起来了。你可以把它当作调试时的检查清单。
4.7 调试与故障定位实用技巧
中断相关的故障,往往是最难排查的。我分享几个实战中积累的经验:
- 查看中断统计:
cat /proc/interrupts可以看到每个中断的触发次数。如果某个中断次数不增长,说明中断没触发;如果疯狂增长,说明中断没被正确清除。 - 测量中断延迟: 用
ftrace的irqsoff追踪器,可以测量关中断的时间。如果关中断时间过长,说明上半部处理太慢。 - 共享中断排查: 先确认所有共享驱动都正确检查了中断状态寄存器。可以用
printk临时打印每个驱动的IRQ_NONE返回情况。 - 触发类型不匹配: 用示波器抓中断引脚的波形,确认实际触发边沿/电平与驱动配置一致。这是最直接的方法。
request_irq 之后用 cat /proc/interrupts | grep my_device 确认中断号、触发次数和设备名称是否正确。这个命令我几乎每天都会用。
中断处理是驱动开发的核心能力。理解上半部和下半部的分工,掌握 tasklet 和 workqueue 的适用场景,处理好共享中断的细节,你就能应对绝大多数中断相关的开发任务了。