ESC
输入关键词搜索文章标题和内容

嵌入式Linux中断处理与并发控制:硬中断、下半部与锁选型

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

嵌入式Linux中断处理与并发控制:硬中断、下半部与锁选型

中断是驱动开发绕不开的机制,但中断上下文的严格约束让并发控制容易踩坑。本文拆解中断响应链路和上半部/下半部机制演进,对比自旋锁与互斥锁的选型策略,给出中断+Workqueue+Mutex的综合代码示例,帮你避开驱动开发中最常见的并发陷阱。


一、原理简析

中断机制的本质

中断是硬件外设主动通知CPU的机制。当外设需要CPU关注时,通过中断控制器(如GIC)向CPU发送电信号,CPU暂停当前任务,跳转到中断向量表执行对应的中断处理程序。

完整的中断响应链路:外设触发中断信号 → 中断控制器仲裁优先级 → CPU响应中断 → 保存上下文 → 执行中断处理程序 → 恢复上下文。

中断处理的原则只有四个字:快进快出。因为中断上下文不允许睡眠、不允许访问用户空间、不允许执行耗时操作。违反这些约束,轻则系统卡顿,重则死锁崩溃。

并发问题的根源

在SMP系统和内核抢占环境下,并发随时可能发生:硬中断打断进程、软中断打断进程、不同CPU同时访问共享数据、抢占调度导致临界区被重入。这些场景归根结底就一件事:多个执行流同时访问同一临界区,却没有正确的同步保护。

flowchart TB A(["外设触发中断"]) --> B["中断控制器仲裁"] B --> C["CPU响应中断"] C --> D["保存上下文"] D --> E["执行中断处理程序"] E --> F{"是否需要<br/>延迟处理?"} F -->|"否"| G["恢复上下文"] F -->|"是"| H["调度下半部"] H --> G G --> I(["继续执行"])

轮询 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),不会睡眠。适用于短临界区、中断上下文。

核心规则:持自旋锁期间绝对不能睡眠。

自旋锁有多种变体,选择依据是临界区数据被哪些执行上下文访问:

来自 linuxros.cn · linuxROS
变体 禁用对象 适用场景
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 否 是 是 否
适用上下文 任意 任意 仅进程 仅进程 任意
临界区长度 单条指令 极短 可较长 可较长 极短
开销 最低 低 中 中 低
典型场景 计数器 中断与进程共享 进程间互斥 资源计数 读多写少

五、核心流程图

中断处理完整流程

flowchart TB A(["硬件中断触发"]) --> B["中断控制器仲裁"] B --> C["CPU响应中断<br/>保存上下文"] C --> D["执行handler<br/>硬中断上下文"] D --> E{"中断来源<br/>是本设备?"} E -->|"否"| F["返回IRQ_NONE"] E -->|"是"| G["读取硬件状态<br/>清除中断标志"] G --> H{"需要延迟处理?"} H -->|"否"| I["返回IRQ_HANDLED"] H -->|"是"| J{"选择下半部机制"} J -->|"Workqueue"| K["schedule_work()"] J -->|"Threaded IRQ"| L["返回IRQ_WAKE_THREAD<br/>唤醒内核线程"] K --> M(["进程上下文执行<br/>可睡眠可调度"]) L --> N(["thread_fn执行<br/>可睡眠可调度"]) M --> O(["恢复上下文<br/>继续执行"]) N --> O I --> O F --> O style A fill:#E8F5E9 style E fill:#FFF8E1 style H fill:#FFF8E1 style J fill:#FFF8E1 style F fill:#FFEBEE

并发控制选型决策树

flowchart TB A(["需要同步保护?"]) --> B{"临界区能否睡眠?"} B -->|"能"| C{"是否需要计数?"} B -->|"否"| D{"数据被谁访问?"} C -->|"否"| E["互斥锁 mutex"] C -->|"是"| F["信号量 semaphore"] D -->|"仅进程上下文"| G["spin_lock()"] D -->|"进程+软中断"| H["spin_lock_bh()"] D -->|"进程+硬中断"| I["spin_lock_irqsave()"] D -->|"仅简单计数"| J["原子操作 atomic_t"] D -->|"读多写少"| K["读写锁 rwlock_t"] style A fill:#E3F2FD style B fill:#FFF8E1 style C fill:#FFF8E1 style D fill:#FFF8E1 style E fill:#E8F5E9 style F fill:#E8F5E9 style G fill:#E8F5E9 style H fill:#E8F5E9 style I fill:#E8F5E9 style J fill:#E8F5E9 style K fill:#E8F5E9

六、完整代码示例

以下示例展示一个完整的按键中断驱动,综合使用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_ 系列。


八、总结

中断处理与并发控制是驱动开发的基本功。要点回顾:

  1. 中断上下文不能睡眠——这是铁律,违反必出问题
  2. 上半部快进快出——耗时操作延迟到下半部
  3. 下半部选型——新驱动用Threaded IRQ或Workqueue,不用已废弃的tasklet
  4. 自旋锁选型——根据竞争上下文选择变体,不确定就用 spin_lock_irqsave()
  5. 互斥锁优先用可中断版本——mutex_lock_interruptible() 比 mutex_lock() 更安全
  6. IRQF_ONESHOT——电平触发中断的必需标志

下期聊DMA传输与内存映射。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页