前言
中断在驱动中是非常常用的,无论是外部的GPIO中断,还是SPI,I2C等发送或接收中断,都是必不可少的。所以今天来看看Linux中的中断处理。
中断分类
上面我们根据中断来源,屏蔽方式和中断入口对中断进行了简单的分类。
中断控制器
PIC: 可编程中断控制器
GIC: Generic Interrupt Controller, 通用中断控制器。(常用)
GIC是目前最常见的一种中断控制器,它在多核CPU中特别常见。它对中断做了细分:
- SGI: Software Generated Interrupt, 软件产生的中断,可以用于多核的核间通信。一个CPU可以通过写GIC的寄存器给另外一个CPU产生中断。(中断号0~15)
- PPI: Private Peripheral Interrupt, 某个CPU私有外设的中断,这类外设的中断只能发给绑定的那个CPU。(中断号16~31)
- SPI: Shared Peripheral Interrupt, 共享外设的中断,这类中断可以路由到任何一个CPU。(中断号32~1019)
这些更详细的内容可以到内核源码中的Documentation中找到更详细的介绍。
中断处理框架
为了在中断执行时间尽量短和中断处理需完成的工作尽量大之间找到平衡点,Linux将中断处理程序分解为两个半部: 顶半部和底半部。
顶半部用于完成尽量少的比较紧急的功能。底半部完成耗时操作。顶半部不会被中断打断,底半部可以。
如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。
驱动中断编程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | //申请中断 /* 参数说明: irq:要申请的硬件中断号 handler:中断处理函数,顶半部 flags:中断触发方式 name:中断名称 dev:传递给中断处理函数的私有数据 */ request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev) devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id) //释放中断 const void *free_irq(unsigned int irq, void *dev_id) //使能和屏蔽某个中断 void disable_irq(unsigned int irq) //需要等待目前中断处理完成 void disable_irq_nosync(unsigned int irq) //不需要等待目前中断处理完成 void enable_irq(unsigned int irq) //使能和屏蔽所有中断 #define local_irq_enable() do { raw_local_irq_enable(); } while (0) #define local_irq_disable() do { raw_local_irq_disable(); } while (0) |
上面是Linux提供的一些接口函数,比较简单。
底半部机制
Linux实现底半部的机制主要有tasklet, 工作队列, 软中断和线程irq。
1. tasklet
要使用tasklet只要使用下面的宏将tasklet和其处理函数关联在一起即可。
1 2 | #define DECLARE_TASKLET(name, func, data) \ struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data } |
然后在需要调用tasklet的时候调用tasklet_schedule函数就能使系统在适当的时候进行调度运行。
1 | static inline void tasklet_schedule(struct tasklet_struct *t) |
tasklet程序模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //定义tasklet和底半部函数并关联 void xxx_do_tasklet(unsigned long); DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0); //中断处理底半部 void xxx_do_tasklet(unsigned long) { ...... } //中断处理顶半部 irqreturn_t xxx_interrupt(int irq, void *dev_id) { ...... tasklet_schedule(&xxx_tasklet); ...... } //设备驱动模块加载函数 int __init xxx_init(void) { //申请中断 result = request_irq(xxx_irq, xxx_interrupt, IRQF_DISABLED, "xxx", NULL); ...... return IRQ_HANDLED; } //设备驱动模块卸载函数 void __exit xxx_exit(void) { ...... //释放中断 free_irq(xxx_irq, xxx_interrupt); } |
2. 工作队列
工作队列的使用方法和tasklet基本相似。
(1) 定义一个工作队列和底半部处理函数
1 2 | struct work_struct my_wq; //工作队列 void my_wq_func(struct work_struct *work);//处理函数 |
(2) 初始化工作队列
1 | INIT_WORK(&my_wq, my_wq_func); |
(3) 调用工作队列执行函数
1 | schedule_work(&my_wq); //调度工作队列执行 |
工作队列程序模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | //定义工作队列和处理函数 struct work_struct xxx_wq; void xxx_do_work(struct work_struct *work); //中断处理底半部 void xxx_do_work(struct work_struct *work) { ....... } //中断处理顶半部 irqreturn_t xxx_interrupt(int irq, void *dev_id) { ....... schedule_work(&xxx_wq); ....... return IRQ_HANDLED; } //设备驱动模块加载函数 int xxx_init(void) { ...... //申请中断 result = request_irq(xxx_irq, xxx_interrupt, 0, "XXX", NULL); //初始化工作队列 INIT_WORK(&xxx_wq, xxx_do_work); ...... } //设备驱动模块卸载函数 void xxx_exit(void) { ....... //释放中断 free_irq(xxx_irq, xxx_interrupt); ....... } |
3. 软中断
这个我们一般不直接使用,tasklet就是基于软中断实现的。
4. 线程irq
在内核中除了使用request_irq()、devm_request_irq()申请中断外,还可以使用 request_threaded_irq() 和 devm_request_threaded_irq()来申请。
1 2 3 4 5 6 7 8 9 | int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id) int devm_request_threaded_irq(struct device *dev, unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id) |
上面两个函数多了个thread_fn, 用这两个API申请中断的时候,内核会为相应的中断号分配一个对应的内核线程。
参数handler对应的函数执行于中断上下文,thread_fn参数对应的函数执行于内核线程。如果handler结束时返回值为IRQ_WAKE_THREAD, 内核会调度对应线程执行thread_fn对应的函数。