一、顶级架构一句话总结
硬件中断 → 中断控制器 → CPU响应 → 中断处理程序 → 唤醒进程/处理数据
中断机制解决了CPU与外设速度不匹配的问题,让外设能主动"通知"CPU处理任务,而不是CPU一直轮询等待。
二、为什么需要中断?
轮询 vs 中断
轮询方式:┌─────────────────────────────────────────┐│ while(1) { ││ if (device_ready) { ││ process_data(); ││ } ││ } │└─────────────────────────────────────────┘问题:CPU空转,资源浪费严重中断方式:┌─────────────────────────────────────────┐│ 设备就绪 → 触发中断信号 ││ ↓ ││ CPU暂停当前任务,执行中断处理程序 ││ ↓ ││ 处理完成,恢复原任务 │└─────────────────────────────────────────┘优势:CPU高效利用,响应及时
中断的价值
三、中断处理核心概念
1. 中断号(IRQ)
每个中断源都有唯一的中断号,内核通过中断号识别中断来源。
// 常见中断类型IRQ_TYPE_EDGE_RISING // 上升沿触发IRQ_TYPE_EDGE_FALLING // 下降沿触发IRQ_TYPE_EDGE_BOTH // 双边沿触发IRQ_TYPE_LEVEL_HIGH // 高电平触发IRQ_TYPE_LEVEL_LOW // 低电平触发
2. 中断处理流程
硬件中断发生 ↓中断控制器(GIC/APIC) ↓CPU响应中断 ↓保存当前上下文 ↓查找中断向量表 ↓执行中断处理函数(ISR) ↓恢复上下文 ↓返回被中断的程序
3. 中断处理原则
•快进快出:中断处理程序必须尽可能短•不能睡眠:中断上下文不能调用可能睡眠的函数•不能访问用户空间:中断上下文没有进程上下文
四、中断处理程序开发
1. 注册中断处理函数
#include<linux/interrupt.h>// 中断处理函数原型irqreturn_t (*irq_handler_t)(int irq, void *dev_id);// 注册中断intrequest_irq(unsignedint irq, // 中断号 irq_handler_t handler, // 处理函数 unsigned long flags, // 标志位 const char *name, // 设备名称 void *dev_id); // 设备ID// 释放中断voidfree_irq(unsignedint irq, void *dev_id);
2. 中断标志位
// 常用标志位IRQF_TRIGGER_RISING // 上升沿触发IRQF_TRIGGER_FALLING // 下降沿触发IRQF_TRIGGER_HIGH // 高电平触发IRQF_TRIGGER_LOW // 低电平触发IRQF_SHARED // 共享中断IRQF_DISABLED // 禁用其他中断(已废弃)IRQF_NOBALANCING // 禁止IRQ亲和性平衡
3. 中断处理函数返回值
// 返回值说明IRQ_HANDLED // 中断已处理IRQ_NONE // 中断未处理(用于共享中断判断)IRQ_WAKE_THREAD // 唤醒中断线程
4. 基本中断处理示例
#include<linux/module.h>#include<linux/interrupt.h>#include<linux/gpio.h>#define GPIO_IRQ_PIN 17staticirqreturn_tgpio_irq_handler(int irq, void *dev_id){ int gpio_value; // 读取GPIO状态 gpio_value = gpio_get_value(GPIO_IRQ_PIN); pr_info("GPIO interrupt triggered, value = %d\n", gpio_value); return IRQ_HANDLED;}static int irq_number;staticint __init my_init(void){ int ret; // 申请GPIO ret = gpio_request(GPIO_IRQ_PIN, "irq_gpio"); if (ret) { pr_err("Failed to request GPIO\n"); return ret; } // 设置GPIO为输入 gpio_direction_input(GPIO_IRQ_PIN); // 获取中断号 irq_number = gpio_to_irq(GPIO_IRQ_PIN); // 注册中断处理函数 ret = request_irq(irq_number, gpio_irq_handler, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "my_gpio_irq", NULL); if (ret) { pr_err("Failed to request IRQ\n"); gpio_free(GPIO_IRQ_PIN); return ret; } pr_info("Module loaded, IRQ %d registered\n", irq_number); return 0;}staticvoid __exit my_exit(void){ free_irq(irq_number, NULL); gpio_free(GPIO_IRQ_PIN); pr_info("Module unloaded\n");}module_init(my_init);module_exit(my_exit);MODULE_LICENSE("GPL");
五、中断下半部机制
为什么需要下半部?
中断处理程序运行在中断上下文,有以下限制:
•不能睡眠•不能执行耗时操作•不能访问用户空间
解决方案:将工作分为上半部(中断处理程序)和下半部(延迟处理)。
下半部机制对比
┌────────────────────────────────────────────────────────────┐│ 下半部机制对比 │├──────────────┬─────────────┬─────────────┬────────────────┤│ 机制 │ 执行上下文 │ 可睡眠? │ 适用场景 │├──────────────┼─────────────┼─────────────┼────────────────┤│ SoftIRQ │ 中断上下文 │ 否 │ 高性能网络 ││ Tasklet │ 中断上下文 │ 否 │ 一般设备驱动 ││ Workqueue │ 进程上下文 │ 是 │ 复杂耗时操作 ││ Threaded IRQ│ 进程上下文 │ 是 │ 需要睡眠的中断│└──────────────┴─────────────┴─────────────┴────────────────┘
1. Tasklet
#include<linux/interrupt.h>// 定义taskletDECLARE_TASKLET(my_tasklet, tasklet_handler, (unsigned long)data);// 或者动态创建struct tasklet_struct my_tasklet;voidtasklet_handler(unsignedlong data){ // 处理逻辑 pr_info("Tasklet executed with data: %lu\n", data);}// 初始化tasklet_init(&my_tasklet, tasklet_handler, (unsigned long)my_data);// 调度执行tasklet_schedule(&my_tasklet);// 禁用/启用tasklet_disable(&my_tasklet);tasklet_enable(&my_tasklet);// 杀死(确保不再执行)tasklet_kill(&my_tasklet);
2. Workqueue
#include<linux/workqueue.h>// 静态定义staticDECLARE_WORK(my_work, work_handler);// 动态创建struct work_struct my_work;voidwork_handler(struct work_struct *work){ // 可以睡眠! msleep(100); pr_info("Work executed\n");}// 初始化INIT_WORK(&my_work, work_handler);// 调度执行schedule_work(&my_work);// 延迟调度schedule_delayed_work(&my_delayed_work, msecs_to_jiffies(1000));// 等待工作完成flush_work(&my_work);flush_scheduled_work();// 取消工作cancel_work_sync(&my_work);
3. Threaded IRQ(推荐)
#include<linux/interrupt.h>// 上半部:快速响应irqreturn_tirq_thread_fn(int irq, void *dev_id){ struct my_device *dev = dev_id; // 可以睡眠! process_data(dev); return IRQ_HANDLED;}// 下半部:线程化处理irqreturn_tirq_handler(int irq, void *dev_id){ // 快速响应 disable_irq_nosync(irq); // 返回IRQ_WAKE_THREAD唤醒线程 return IRQ_WAKE_THREAD;}// 注册线程化中断ret = request_threaded_irq(irq_number, irq_handler, // 上半部 irq_thread_fn, // 下半部线程 IRQF_TRIGGER_RISING, "my_threaded_irq", dev_data);
六、并发控制机制
为什么需要并发控制?
在驱动开发中,多个执行流可能同时访问共享资源:
┌─────────────────────────────────────────────────────────┐│ 并发访问场景 │├─────────────────────────────────────────────────────────┤│ 进程A (read) 进程B (write) 中断处理程序 ││ ↓ ↓ ↓ ││ ┌─────────────────────────────────────────┐ ││ │ 共享数据(设备缓冲区) │ ││ └─────────────────────────────────────────┘ ││ ↑ 可能导致数据竞争! │└─────────────────────────────────────────────────────────┘
并发控制机制对比
1. 原子操作
#include<linux/atomic.h>// 原子变量定义atomic_t counter = ATOMIC_INIT(0);// 原子操作atomic_set(&counter, 10); // 设置值int val = atomic_read(&counter); // 读取值atomic_add(5, &counter); // 加法atomic_sub(3, &counter); // 减法atomic_inc(&counter); // 自增atomic_dec(&counter); // 自减// 带返回值的操作int new_val = atomic_add_return(5, &counter);int old_val = atomic_xchg(&counter, 20); // 交换// 条件操作if (atomic_cmpxchg(&counter, 10, 20) == 10) { // 原值是10,已替换为20}// 原子位操作unsigned long flags;set_bit(0, &flags); // 设置位clear_bit(0, &flags); // 清除位change_bit(0, &flags); // 翻转位test_bit(0, &flags); // 测试位test_and_set_bit(0, &flags); // 测试并设置
2. 自旋锁
#include<linux/spinlock.h>// 定义自旋锁spinlock_t my_lock;// 初始化spin_lock_init(&my_lock);// 基本使用spin_lock(&my_lock);// 临界区代码spin_unlock(&my_lock);// 在中断处理中使用spin_lock_irqsave(&my_lock, flags);// 临界区代码spin_unlock_irqrestore(&my_lock, flags);// 在底半部中使用spin_lock_bh(&my_lock);// 临界区代码spin_unlock_bh(&my_lock);// 尝试获取锁(非阻塞)if (spin_trylock(&my_lock)) { // 获取成功 spin_unlock(&my_lock);} else { // 获取失败}
3. 互斥锁
#include<linux/mutex.h>// 定义互斥锁DEFINE_MUTEX(my_mutex);// 或struct mutex my_mutex;mutex_init(&my_mutex);// 基本使用mutex_lock(&my_mutex);// 临界区代码(可以睡眠)mutex_unlock(&my_mutex);// 非阻塞获取if (mutex_trylock(&my_mutex)) { // 获取成功 mutex_unlock(&my_mutex);} else { // 获取失败}// 带中断的获取if (mutex_lock_interruptible(&my_mutex)) { // 被信号中断 return -ERESTARTSYS;}// 临界区代码mutex_unlock(&my_mutex);// 带超时的获取if (mutex_lock_killable(&my_mutex)) { // 被杀死信号中断 return -ERESTARTSYS;}
4. 信号量
#include<linux/semaphore.h>// 定义信号量DEFINE_SEMAPHORE(my_sem); // 初始值为1(互斥)// 或struct semaphore my_sem;sema_init(&my_sem, 1); // 初始值为1// 获取信号量(可睡眠)down(&my_sem);// 临界区代码up(&my_sem);// 可中断获取if (down_interruptible(&my_sem)) { return -ERESTARTSYS;}// 临界区代码up(&my_sem);// 非阻塞获取if (down_trylock(&my_sem)) { // 获取失败 return -EBUSY;}// 临界区代码up(&my_sem);
5. 读写锁
#include<linux/rwlock.h>// 定义读写锁rwlock_t my_rwlock;rwlock_init(&my_rwlock);// 读操作read_lock(&my_rwlock);// 读临界区read_unlock(&my_rwlock);// 写操作write_lock(&my_rwlock);// 写临界区write_unlock(&my_rwlock);// 带中断保护的版本read_lock_irqsave(&my_rwlock, flags);read_unlock_irqrestore(&my_rwlock, flags);write_lock_irqsave(&my_rwlock, flags);write_unlock_irqrestore(&my_rwlock, flags);
七、完整示例:中断+并发控制
#include<linux/module.h>#include<linux/interrupt.h>#include<linux/gpio.h>#include<linux/workqueue.h>#include<linux/mutex.h>#include<linux/cdev.h>#include<linux/fs.h>#include<linux/uaccess.h>#define DEVICE_NAME "irq_dev"#define GPIO_PIN 17#define BUFFER_SIZE 256struct my_device { struct cdev cdev; struct mutex lock; struct work_struct work; char buffer[BUFFER_SIZE]; int data_ready; int irq; wait_queue_head_t wait_queue;};static struct my_device *my_dev;static int major_number;// 工作队列处理函数staticvoidwork_handler(struct work_struct *work){ struct my_device *dev = container_of(work, struct my_device, work); mutex_lock(&dev->lock); // 模拟数据处理 snprintf(dev->buffer, BUFFER_SIZE, "IRQ processed at %lu", jiffies); dev->data_ready = 1; mutex_unlock(&dev->lock); // 唤醒等待的进程 wake_up_interruptible(&dev->wait_queue); pr_info("Work handler completed\n");}// 中断处理函数(上半部)staticirqreturn_tirq_handler(int irq, void *dev_id){ struct my_device *dev = dev_id; pr_info("IRQ triggered\n"); // 调度工作队列(下半部) schedule_work(&dev->work); return IRQ_HANDLED;}// 设备读取staticssize_tmy_read(struct file *file, char __user *buf, size_t count, loff_t *offset){ struct my_device *dev = file->private_data; ssize_t ret; // 等待数据就绪 if (wait_event_interruptible(dev->wait_queue, dev->data_ready)) return -ERESTARTSYS; mutex_lock(&dev->lock); if (count > strlen(dev->buffer)) count = strlen(dev->buffer); if (copy_to_user(buf, dev->buffer, count)) { ret = -EFAULT; goto out; } dev->data_ready = 0; ret = count;out: mutex_unlock(&dev->lock); return ret;}// 设备打开staticintmy_open(struct inode *inode, struct file *file){ struct my_device *dev = container_of(inode->i_cdev, struct my_device, cdev); file->private_data = dev; return 0;}static struct file_operations fops = { .owner = THIS_MODULE, .open = my_open, .read = my_read,};staticint __init my_init(void){ dev_t dev; int ret; // 分配设备结构 my_dev = kzalloc(sizeof(*my_dev), GFP_KERNEL); if (!my_dev) return -ENOMEM; // 初始化互斥锁 mutex_init(&my_dev->lock); // 初始化等待队列 init_waitqueue_head(&my_dev->wait_queue); // 初始化工作队列 INIT_WORK(&my_dev->work, work_handler); // 申请GPIO ret = gpio_request(GPIO_PIN, "irq_gpio"); if (ret) goto err_gpio; gpio_direction_input(GPIO_PIN); my_dev->irq = gpio_to_irq(GPIO_PIN); // 注册中断 ret = request_irq(my_dev->irq, irq_handler, IRQF_TRIGGER_RISING, "my_irq", my_dev); if (ret) goto err_irq; // 注册字符设备 ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME); if (ret) goto err_chrdev; major_number = MAJOR(dev); cdev_init(&my_dev->cdev, &fops); my_dev->cdev.owner = THIS_MODULE; ret = cdev_add(&my_dev->cdev, dev, 1); if (ret) goto err_cdev; pr_info("Module loaded, major = %d\n", major_number); return 0;err_cdev: unregister_chrdev_region(dev, 1);err_chrdev: free_irq(my_dev->irq, my_dev);err_irq: gpio_free(GPIO_PIN);err_gpio: kfree(my_dev); return ret;}staticvoid __exit my_exit(void){ dev_t dev = MKDEV(major_number, 0); cancel_work_sync(&my_dev->work); cdev_del(&my_dev->cdev); unregister_chrdev_region(dev, 1); free_irq(my_dev->irq, my_dev); gpio_free(GPIO_PIN); kfree(my_dev); pr_info("Module unloaded\n");}module_init(my_init);module_exit(my_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Driver Developer");MODULE_DESCRIPTION("Interrupt and Concurrency Demo");
八、选择指南
如何选择并发控制机制?
┌─────────────────────────────────────────────────────────────┐│ 并发控制选择决策树 │├─────────────────────────────────────────────────────────────┤│ ││ 临界区是否可能睡眠? ││ │ ││ ├── 否 → 使用自旋锁 ││ │ │ ││ │ ├── 在中断上下文?→ spin_lock_irqsave ││ │ └── 在进程上下文?→ spin_lock ││ │ ││ └── 是 → 使用互斥锁 ││ │ ││ ├── 需要资源计数?→ 信号量 ││ └── 只需互斥?→ mutex ││ ││ 只是简单计数?→ 原子操作 ││ ││ 读多写少?→ 读写锁/RCU ││ │└─────────────────────────────────────────────────────────────┘
如何选择下半部机制?
需要睡眠? → Workqueue / Threaded IRQ │ └── 否 → 性能要求极高? → SoftIRQ │ └── 否 → Tasklet(推荐)
九、终极总结
中断处理 + 并发控制 = Linux驱动开发的核心难题
•中断处理:上半部快速响应,下半部延迟处理•下半部机制:Tasklet(简单)、Workqueue(可睡眠)、Threaded IRQ(推荐)•并发控制:自旋锁(短临界区)、互斥锁(可睡眠)、原子操作(简单计数)•选择原则:根据上下文、是否睡眠、性能需求综合选择
掌握中断与并发控制,是成为高级驱动开发工程师的必经之路!