嵌入式Linux中断处理与并发控制:硬中断、下半部与锁选型
中断是驱动开发绕不开的机制,但中断上下文的严格约束让并发控制容易踩坑。本文拆解中断响应链路和上半部/下半部机制演进,对比自旋锁与互斥锁的选型策略,给出中断+Workqueue+Mutex的综合代码示例,帮你避开驱动开发中最常见的并发陷阱。
一、原理简析
中断机制的本质
中断是硬件外设主动通知CPU的机制。当外设需要CPU关注时,通过中断控制器(如GIC)向CPU发送电信号,CPU暂停当前任务,跳转到中断向量表执行对应的中断处理程序。
完整的中断响应链路:外设触发中断信号 → 中断控制器仲裁优先级 → CPU响应中断 → 保存上下文 → 执行中断处理程序 → 恢复上下文。
中断处理的原则只有四个字:快进快出。因为中断上下文不允许睡眠、不允许访问用户空间、不允许执行耗时操作。违反这些约束,轻则系统卡顿,重则死锁崩溃。
并发问题的根源
在SMP系统和内核抢占环境下,并发随时可能发生:硬中断打断进程、软中断打断进程、不同CPU同时访问共享数据、抢占调度导致临界区被重入。这些场景归根结底就一件事:多个执行流同时访问同一临界区,却没有正确的同步保护。
轮询 vs 中断
| 对比维度 | 轮询方式 | 中断方式 |
|---|---|---|
| CPU占用 | 持续占用,空转浪费 | 事件驱动,空闲时CPU可做其他事 |
| 响应延迟 | 取决于轮询周期 | 几乎即时响应 |
| 实现复杂度 | 简单 | 需处理并发和同步 |
| 适用场景 | 高频事件、轮询开销小于中断开销 | 低频事件、需要低延迟响应 |
| 功耗 | 高 | 低 |
二、中断处理开发
上半部:硬中断处理
上半部运行在中断上下文,执行最紧急的操作:读取硬件状态、清除中断标志、拷贝数据到缓冲区、调度下半部。
注册中断的核心API:
#include <linux/interrupt.h>
/* 基本注册 */
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev_id);
/* 线程化注册(推荐) */
int request_threaded_irq(unsigned int irq,
irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags,
const char *name, void *dev_id);
/* 释放中断 */
void free_irq(unsigned int irq, void *dev_id);
request_irq 本质上是 request_threaded_irq(irq, handler, NULL, flags, name, dev_id) 的封装——thread_fn 为NULL时不创建内核线程。
中断标志位
| 标志位 | 说明 |
|---|---|
IRQF_TRIGGER_RISING |
上升沿触发 |
IRQF_TRIGGER_FALLING |
下降沿触发 |
IRQF_TRIGGER_HIGH |
高电平触发 |
IRQF_TRIGGER_LOW |
低电平触发 |
IRQF_SHARED |
共享中断,多个设备共用同一中断号 |
IRQF_ONESHOT |
线程化中断中,thread_fn执行完才重新使能中断线 |
IRQF_NO_SUSPEND |
系统休眠时不关闭该中断 |
注意:使用
IRQF_SHARED时,dev_id不能为NULL,因为free_irq需要通过它区分共享同一中断号的多个处理程序。
中断返回值
irqreturn_t my_handler(int irq, void *dev_id)
{
if (!is_my_device_interrupt())
return IRQ_NONE; /* 不是本设备中断 */
handle_urgent_work();
if (need_thread_processing)
return IRQ_WAKE_THREAD; /* 唤醒thread_fn线程 */
return IRQ_HANDLED; /* 已处理完成 */
}
| 返回值 | 含义 |
|---|---|
IRQ_HANDLED |
中断已处理,无需唤醒线程 |
IRQ_NONE |
不是本设备的中断(共享中断时用于轮询判断) |
IRQ_WAKE_THREAD |
中断已初步处理,唤醒 thread_fn 继续处理 |
下半部机制
上半部必须快进快出,耗时操作必须延迟到下半部执行。Linux提供了四种下半部机制,各有适用场景。
SoftIRQ
最底层的延迟机制,运行在软中断上下文,不能睡眠。仅网络子系统和块设备等核心路径使用,驱动开发者一般不直接使用。
Tasklet(已废弃)
基于SoftIRQ构建,保证同一个tasklet不会在多个CPU上并发执行。但从Linux 5.14起,tasklet已被明确标记为deprecated,新代码不应再使用。
旧版API(已废弃):
/* 旧API:callback接收data参数(已废弃) */
DECLARE_TASKLET_OLD(name, func, data);
新版API(5.9+,但仍标记deprecated):
/* 新API:callback接收tasklet_struct指针 */
static void my_tasklet_func(struct tasklet_struct *t);
DECLARE_TASKLET(my_tasklet, my_tasklet_func);
/* 动态初始化 */
tasklet_setup(&dev->tasklet, my_tasklet_func);
/* 获取包含结构体 */
struct my_device *dev = from_tasklet(dev, t, tasklet);
结论:新驱动应使用 Workqueue 或 Threaded IRQ 替代tasklet。
Workqueue
运行在进程上下文,可以睡眠、可以调度、可以执行耗时操作。是驱动开发中最常用的下半部机制。
/* 静态定义 */
static DECLARE_WORK(my_work, my_work_func);
/* 动态初始化 */
INIT_WORK(&dev->work, my_work_func);
/* 调度执行 */
schedule_work(&dev->work);
/* 获取包含结构体 */
struct my_device *dev = container_of(work, struct my_device, work);
Threaded IRQ
将中断处理拆分为硬中断handler和内核线程thread_fn两阶段。handler返回 IRQ_WAKE_THREAD 后,内核唤醒专用线程执行thread_fn。这是现代驱动的推荐方案。
static irqreturn_t my_handler(int irq, void *dev_id)
{
/* 硬中断上下文:只做最紧急的操作 */
disable_device_irq(dev);
return IRQ_WAKE_THREAD;
}
static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
/* 进程上下文:可以睡眠、可以执行耗时操作 */
process_data(dev);
enable_device_irq(dev);
return IRQ_HANDLED;
}
/* 注册线程化中断 */
ret = request_threaded_irq(irq, my_handler, my_thread_fn,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"my_device", dev);
关键点:
IRQF_ONESHOT标志确保在thread_fn执行完毕前,中断线保持屏蔽状态。对于电平触发中断,这个标志是必需的,否则中断线刚解除屏蔽就会再次触发,造成中断风暴。
三、并发控制机制
原子操作
最轻量的同步手段,适用于简单的计数器操作:
#include <linux/atomic.h>
atomic_t counter = ATOMIC_INIT(0);
atomic_inc(&counter); /* 原子加1 */
atomic_dec(&counter); /* 原子减1 */
atomic_add(5, &counter); /* 原子加5 */
int val = atomic_read(&counter); /* 原子读 */
int old = atomic_xchg(&counter, 10); /* 原子交换 */
自旋锁
自旋锁在获取失败时忙等(spin),不会睡眠。适用于短临界区、中断上下文。
核心规则:持自旋锁期间绝对不能睡眠。
自旋锁有多种变体,选择依据是临界区数据被哪些执行上下文访问:
| 变体 | 禁用对象 | 适用场景 |
|---|---|---|
spin_lock() |
仅禁用抢占 | 数据仅在进程上下文间共享 |
spin_lock_bh() |
禁用软中断 | 数据在进程与软中断/tasklet间共享 |
spin_lock_irq() |
禁用硬中断 | 数据在进程与硬中断间共享,且确信中断已开启 |
spin_lock_irqsave() |
保存并禁用中断状态 | 数据在进程与硬中断间共享(通用方案) |
/* 进程上下文 + 硬中断共享数据:最常用的安全方案 */
unsigned long flags;
spin_lock_irqsave(&dev->lock, flags);
/* 临界区 */
spin_unlock_irqrestore(&dev->lock, flags);
/* 进程上下文 + 软中断共享数据 */
spin_lock_bh(&dev->lock);
/* 临界区 */
spin_unlock_bh(&dev->lock);
/* 已在硬中断上下文中:只需基本自旋锁 */
spin_lock(&dev->lock);
/* 临界区 */
spin_unlock(&dev->lock);
选型原则:不确定用哪个?用
spin_lock_irqsave(),它是最安全的通用方案。如果明确知道只有软中断竞争,用spin_lock_bh()开销更小。
互斥锁
互斥锁在获取失败时睡眠等待,适用于进程上下文中的长临界区。
/* 静态初始化 */
DEFINE_MUTEX(my_mutex);
/* 动态初始化 */
struct mutex my_mutex;
mutex_init(&my_mutex);
/* 可中断加锁(推荐) */
if (mutex_lock_interruptible(&my_mutex))
return -ERESTARTSYS; /* 被信号中断,返回用户空间重启系统调用 */
/* 临界区 */
mutex_unlock(&my_mutex);
关键:
mutex_lock_interruptible()返回0表示成功,返回-EINTR表示被信号中断。驱动中应优先使用此接口而非mutex_lock(),后者不可中断,会导致进程无法被kill。
互斥锁的严格约束:
- 只有持锁者才能释放锁
- 不能递归加锁
- 不能在中断上下文使用
- 持锁期间不能退出任务
信号量
信号量是计数锁,允许指定数量的并发持有者。当计数为1时等价于互斥锁,但新代码应优先使用mutex。
struct semaphore sem;
sema_init(&sem, 1); /* 计数1,等价于互斥锁 */
if (down_interruptible(&sem))
return -ERESTARTSYS;
/* 临界区 */
up(&sem);
读写锁
读多写少场景的优化方案,允许多个读者并发,写者独占。
/* 读写自旋锁 */
rwlock_t rw_lock;
rwlock_init(&rw_lock);
read_lock(&rw_lock); /* 读者加锁 */
read_unlock(&rw_lock);
write_lock_irqsave(&rw_lock, flags); /* 写者加锁 */
write_unlock_irqrestore(&rw_lock, flags);
注意:读写自旋锁可能造成写者饥饿。读多写少场景优先考虑RCU机制。
四、对比表格
下半部机制对比
| 特性 | SoftIRQ | Tasklet | Workqueue | Threaded IRQ |
|---|---|---|---|---|
| 执行上下文 | 软中断 | 软中断 | 进程 | 进程(专用线程) |
| 可否睡眠 | 否 | 否 | 是 | 是 |
| 并发性 | 同类型可多CPU并发 | 同一tasklet串行 | 可配置并发 | 独立线程 |
| 优先级控制 | 无 | 固定两级 | 无 | 可设置线程优先级 |
| 新驱动推荐 | 否(内核内部用) | 否(5.14+已废弃) | 是 | 是 |
| 典型用途 | 网络收发、块设备 | 旧驱动兼容 | 通用延迟处理 | 中断线程化 |
并发控制机制对比
| 特性 | 原子操作 | 自旋锁 | 互斥锁 | 信号量 | 读写自旋锁 |
|---|---|---|---|---|---|
| 等待方式 | 无等待 | 忙等 | 睡眠 | 睡眠 | 忙等 |
| 可否睡眠 | N/A | 否 | 是 | 是 | 否 |
| 适用上下文 | 任意 | 任意 | 仅进程 | 仅进程 | 任意 |
| 临界区长度 | 单条指令 | 极短 | 可较长 | 可较长 | 极短 |
| 开销 | 最低 | 低 | 中 | 中 | 低 |
| 典型场景 | 计数器 | 中断与进程共享 | 进程间互斥 | 资源计数 | 读多写少 |
五、核心流程图
中断处理完整流程
并发控制选型决策树
六、完整代码示例
以下示例展示一个完整的按键中断驱动,综合使用Threaded IRQ + Workqueue + Mutex + Wait Queue:
/* SPDX-License-Identifier: GPL-2.0 */
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/mutex.h>
#include <linux/wait.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/of.h>
#define BUF_SIZE 256
struct btn_dev {
struct device *dev;
int irq;
void __iomem *regs;
/* 并发控制 */
struct mutex buf_mutex; /* 保护数据缓冲区 */
spinlock_t irq_lock; /* 保护中断状态 */
/* 数据缓冲区 */
char rx_buf[BUF_SIZE];
int rx_len;
/* 等待队列 */
wait_queue_head_t read_wait;
/* Workqueue */
struct work_struct rx_work;
/* 设备节点 */
dev_t devt;
struct cdev cdev;
};
/* Workqueue处理函数 - 进程上下文,可睡眠 */
static void btn_rx_work(struct work_struct *work)
{
struct btn_dev *bdev = container_of(work, struct btn_dev, rx_work);
/* 互斥锁保护缓冲区,使用可中断版本 */
if (mutex_lock_interruptible(&bdev->buf_mutex)) {
dev_warn(bdev->dev, "work interrupted\n");
return;
}
/* 处理数据:这里可以执行耗时操作 */
dev_info(bdev->dev, "processed %d bytes\n", bdev->rx_len);
mutex_unlock(&bdev->buf_mutex);
/* 唤醒阻塞在读操作上的进程 */
wake_up_interruptible(&bdev->read_wait);
}
/* 线程化中断的thread_fn - 进程上下文,可睡眠 */
static irqreturn_t btn_thread_fn(int irq, void *dev_id)
{
struct btn_dev *bdev = dev_id;
/* 在进程上下文中安全地获取互斥锁 */
mutex_lock(&bdev->buf_mutex);
/* 读取并处理数据 */
bdev->rx_len = readl(bdev->regs + 0x10);
if (bdev->rx_len > 0 && bdev->rx_len <= BUF_SIZE) {
memcpy_fromio(bdev->rx_buf, bdev->regs + 0x20, bdev->rx_len);
}
mutex_unlock(&bdev->buf_mutex);
/* 调度workqueue做进一步处理 */
schedule_work(&bdev->rx_work);
return IRQ_HANDLED;
}
/* 硬中断handler - 中断上下文,不可睡眠 */
static irqreturn_t btn_handler(int irq, void *dev_id)
{
struct btn_dev *bdev = dev_id;
u32 status;
status = readl(bdev->regs + 0x00);
if (!(status & 0x01))
return IRQ_NONE; /* 不是本设备中断 */
/* 清除中断标志 */
writel(0x01, bdev->regs + 0x04);
/* 唤醒线程处理 */
return IRQ_WAKE_THREAD;
}
/* 用户空间读接口 */
static ssize_t btn_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
struct btn_dev *bdev = filp->private_data;
int ret, to_copy;
/* 等待数据可用 */
ret = wait_event_interruptible(bdev->read_wait,
bdev->rx_len > 0);
if (ret)
return -ERESTARTSYS;
/* 互斥锁保护缓冲区 */
if (mutex_lock_interruptible(&bdev->buf_mutex))
return -ERESTARTSYS;
to_copy = min(count, (size_t)bdev->rx_len);
if (copy_to_user(buf, bdev->rx_buf, to_copy)) {
mutex_unlock(&bdev->buf_mutex);
return -EFAULT;
}
bdev->rx_len = 0; /* 标记数据已读 */
mutex_unlock(&bdev->buf_mutex);
return to_copy;
}
static int btn_open(struct inode *inode, struct file *filp)
{
struct btn_dev *bdev = container_of(inode->i_cdev,
struct btn_dev, cdev);
filp->private_data = bdev;
return 0;
}
static const struct file_operations btn_fops = {
.owner = THIS_MODULE,
.open = btn_open,
.read = btn_read,
};
static int btn_probe(struct platform_device *pdev)
{
struct btn_dev *bdev;
int ret;
bdev = devm_kzalloc(&pdev->dev, sizeof(*bdev), GFP_KERNEL);
if (!bdev)
return -ENOMEM;
bdev->dev = &pdev->dev;
/* 初始化同步原语 */
mutex_init(&bdev->buf_mutex);
spin_lock_init(&bdev->irq_lock);
init_waitqueue_head(&bdev->read_wait);
INIT_WORK(&bdev->rx_work, btn_rx_work);
/* 映射寄存器 */
bdev->regs = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(bdev->regs))
return PTR_ERR(bdev->regs);
/* 获取中断号 */
bdev->irq = platform_get_irq(pdev, 0);
if (bdev->irq < 0)
return bdev->irq;
/* 注册线程化中断 */
ret = devm_request_threaded_irq(&pdev->dev, bdev->irq,
btn_handler, btn_thread_fn,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"btn_irq", bdev);
if (ret) {
dev_err(&pdev->dev, "failed to request irq: %d\n", ret);
return ret;
}
platform_set_drvdata(pdev, bdev);
dev_info(&pdev->dev, "button driver probed\n");
return 0;
}
static int btn_remove(struct platform_device *pdev)
{
struct btn_dev *bdev = platform_get_drvdata(pdev);
/* 确保work完成 */
cancel_work_sync(&bdev->rx_work);
/* devm自动释放irq和内存 */
dev_info(&pdev->dev, "button driver removed\n");
return 0;
}
static const struct of_device_id btn_of_match[] = {
{ .compatible = "myvendor,btn" },
{ }
};
MODULE_DEVICE_TABLE(of, btn_of_match);
static struct platform_driver btn_driver = {
.probe = btn_probe,
.remove = btn_remove,
.driver = {
.name = "btn_driver",
.of_match_table = btn_of_match,
},
};
module_platform_driver(btn_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Driver");
MODULE_DESCRIPTION("Button IRQ + Workqueue + Mutex Demo");
对应的设备树节点:
btn_device {
compatible = "myvendor,btn";
reg = <0x1000 0x100>;
interrupts = <25 IRQ_TYPE_EDGE_FALLING>;
};
七、常见问题解决
Q1:中断处理中调用了 kmalloc(GFP_KERNEL) 导致崩溃?
中断上下文不能睡眠,GFP_KERNEL 可能睡眠。必须使用 GFP_ATOMIC。同理,mutex_lock()、copy_to_user() 等可能睡眠的函数也不能在中断上下文调用。
Q2:spin_lock_irq() 和 spin_lock_irqsave() 怎么选?
spin_lock_irq() 在解锁时无条件开启中断。如果加锁前中断本来就是关闭的,解锁后会错误地打开中断。spin_lock_irqsave() 保存中断状态,解锁时恢复原状。不确定时一律用 spin_lock_irqsave()。
Q3:mutex_lock_interruptible() 返回非零值怎么处理?
返回 -EINTR 表示被信号中断。在驱动的 read/write 文件操作中应返回 -ERESTARTSYS,让VFS层自动重启系统调用或返回用户空间处理。
Q4:电平触发中断不停触发怎么办?
使用 IRQF_ONESHOT 标志。它保证 thread_fn 执行完毕后才重新使能中断线,避免中断风暴。或者在中断handler中先禁用设备中断,处理完后再重新使能。
Q5:新驱动该用tasklet还是workqueue?
不用tasklet。Linux 5.14起tasklet已标记deprecated。轻量级场景用Threaded IRQ,需要灵活调度的场景用Workqueue。
Q6:devm_request_threaded_irq() 和 request_threaded_irq() 的区别?
devm_ 版本是设备资源管理版本,驱动移除时自动释放中断,无需手动调用 free_irq()。新驱动推荐使用 devm_ 系列。
八、总结
中断处理与并发控制是驱动开发的基本功。要点回顾:
- 中断上下文不能睡眠——这是铁律,违反必出问题
- 上半部快进快出——耗时操作延迟到下半部
- 下半部选型——新驱动用Threaded IRQ或Workqueue,不用已废弃的tasklet
- 自旋锁选型——根据竞争上下文选择变体,不确定就用
spin_lock_irqsave() - 互斥锁优先用可中断版本——
mutex_lock_interruptible()比mutex_lock()更安全 IRQF_ONESHOT——电平触发中断的必需标志
下期聊DMA传输与内存映射。