设备与处理器之间的工作通常来说是异步,设备数据要传递给处理器通常来说有以下几种方法:轮询、等待和中断。
- 轮询让CPU以固定的频率读取设备,看看数据是否准备好,准备好就读取;
- 如果确定知道设备数据会在很短的未来准备好,也可以让CPU等待一段时间,之后读取数据;
- 设备准备好数据后,拉中断信号,CPU在中断线程中进行读取操作;
让CPU进行轮询等待总是不能让人满意,所以通常都采用中断的形式,让设备来通知CPU读取数据。
中断处理相关的函数
2.6内核的函数参数与现在的参数有所区别,这里都主要介绍概念,具体实现方法需要结合具体的内核版本。
int request_irq(unsigned int irq, irqreturn_t (*handler) (int, void *, struct pt_regs *), unsigned long flags, const char *dev_name, void *dev_id); void free_irq(unsigned int IRQ, void *dev_id);
request_irq函数申请中断,返回0表示申请成功,其他返回值表示申请失败,其具体参数解释如下:
- irq:申请的中断号,在第2小节介绍
- handler:中断处理函数,第3小节介绍
- flags:中断管理掩码,见后面
- dev_name:中断名称,可以显示在/proc/interrupts中
- dev_id:共享中断下必须指定,独占模式可以设为NULL,好的思路是用来指向设备的数据结构
flags 掩码可以使用以下几个:
掩码宏 | 解释 |
SA_INTERRUPT | 当该位被设置时,表示是一个快速中断处理例程 |
SA_SHIRQ | 表示中断在设备之间共享 |
SA_SAMPLE_RANDOM | 产生的中断对/dev/random和/dev/urandom设备熵池做贡献,可以理解为增强随机性 |
快速和慢速处理例程:现代内核中基本没有这两个概念了,使用SA_INTERRUPT位后,当中断被执行时,当前处理器的其他中断都将被禁止。通常不要使用SA_INTERRUPT标志位,除非自己明确知道会发生什么。
共享中断:使用共享中断时,一方面要使用SA_SHIRQ位,另一个是request_irq中的dev_id必须是唯一的,不能为NULL。这个限制的原因是:内核为每个中断维护了一个共享处理例程的列表,例程中的dev_id各不相同,就像设备签名。如果dev_id相同,在卸载的时候引起混淆(卸载了另一个中断),当中断到达时会产生内核OOP消息。
共享中断需要满足以下一个条件才能申请成功:
- 中断信号线空闲
- 已经注册该中断信号线的处理例程都标识为共享中断
当不需要使用该中断时,需要使用free_irq释放中断。
通常我们会在模块加载的时候申请安装中断处理例程,但书中建议:在设备第一次打开的时候安装,在设备最后一次关闭的时候卸载。
如果要查看中断触发的次数,可以查看 /proc/interrupts 和 /proc/stat。
irq中断号获取
书中讲述了如何自动检测中断号,在嵌入式开发中通常都是查看原理图和datasheet来直接确定。
自动检测的原理如下:驱动程序通知设备产生中断,然后查看哪些中断信号线被触发了。Linux提供了以下方法来进行探测:
unsigned long probe_irq_on(void); int probe_irq_off(unsigned long);
- prob_irq_on返回一个未分配中断的位掩码,该掩码需要传递给prob_irq_off。调用完probe_irq_on后就可以通知驱动设备产生中断(至少一次)。
- probe_irq_off 会返回probe_irq_on之后发生了中断的中断编号(即我们所需要的irq中断号)。如果没有检测到发生了中断,则返回0(因此IRQ 0不能用),如果有多个中断编号发生了变化,则返回负值。
探测工作耗时较长,建议在模块加载的时候做。
中断处理函数
中断处理函数和普通函数其实差不多,唯一的区别是其运行的中断上下文中,在这个上下文中有以下注意事项:
- 不能向用户空间发送或接收数据
- 不能做任何会产生休眠的操作
- 不能调用schedule函数
中断处理函数典型用法如下:
static irqreturn_t sample_interrut(int irq, void *dev_id, struct pt_regs *regs) { .... return IRQ_HANDLED; }
中断处理函数的参数和返回值含义如下:
- irq:前面说到的中断号,可以用来区分当前中断线程
- dev_id:申请是传递的参数dev_id
- regs:保存了处理器进入中断代码前的处理器上下文快照,主要用于监控和调试,对驱动通常来说没什么作用。
返回值主要有两个:IRQ_NONE和IRQ_HANDLED。
- 如果当前中断确实是自己需要处理的中断,则返回IRQ_HANDLED
- 如果不是自己的中断,则返回IRQ_NONE
4. 中断的启用与禁用
对于中断我们是可以进行开启和关闭的,Linux中提供了以下函数操作单个中断的开关:
#include <asm; void disable_irq(int irq); void disable_irq_nosync(int irq); void enable_irq(int irq);
该方法可以在所有处理器上禁止或启用中断。
需要注意的是:
- 上面的函数调用是可以嵌套的:就是调用了几次disable_irq则需要调用对应次数的enable_irq才能开启
- disable_irq禁止中断时,如果当前中断正在执行,则会等待其执行完成,所以如果禁止的线程拥有中断线程所需的资源,则会造成思索。
如果要关闭当前处理器上所有的中断,则可以调用以下方法:
#include <asm; void local_irq_save(unsigned long flags); void local_irq_disable(void); void local_irq_restore(unsigned long flags); void local_irq_enable(void);
local_irq_save 会将中断状态保持到flags中,然后禁用处理器上的中断;如果明确知道中断没有在其他地方被禁用,则可以使用local_irq_disable,否则请使用local_irq_save。
locat_irq_restore 会根据上面获取到flags来恢复中断;local_irq_enable 会无条件打开所有中断。
5. 顶半部和底半部概念
在中断中需要做一些工作,如果工作内容太多,必然导致中断处理所需的时间过长;而中断处理又要求能够尽快完成,这样才不会影响正常的系统调度,这两个之间就产生了矛盾。
现在很多操作系统将中断分为两个部分来处理上面的矛盾:顶半部和底半部。
顶半部就是我们用request_irq来注册的中断处理函数,这个函数要求能够尽快结束,同时在其中调度底半部,让底半部在之后来进行后续的耗时工作。
顶半部就不再说明了,就是上面的中断处理函数,只是要求能够尽快处理完成并返回,不要处理耗时工作。
底半部通常使用tasklet或者工作队列来实现。
tasklet的特点和注意事项:
- 运行在软件中断上下文中,需要注意中断上下文的事项;
- 与调用它的函数运行在同一个CPU上;
工作队列的特点和注意事项:
- 运行在进程上下文中,可以进行休眠等操作;
- 工作者进程无法访问其他任何进程的地址空间;