<一> 中断处理流程如下:
1、发生中断时,CPU执行异常向量vector_irq的代码。
2、在vector_irq里面,最终会调用中断处理的总入口函数asm_do_IRQ。
3、asm_do_IRQ根据中断号调用irq_desc数组项中的handle_irq。
4、hadnle_irq会使用chip成员中的函数来设置硬件,比如清除中断、禁止中断、重新使能中断等。
5、handle_irq逐个调用用户在action链表中注册的处理函数。
<二>安装中断处理例程
注册/释放中断的函数:
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);
unsigned int irq:申请的中断号
irqreturn_t (*handler)(int, void *, struct pt_regs*):安装的中断处理函数指针
unsigned long flags:中断管理有关的位掩码选项,中断的触发方式,如SA_INTERRUPT、SA_SHIRQ等
const char *dev_name:传递给request_irq的字符串,中断名字
void *dev_id:该指针只用于共享的中断信号线。没有使用共享时,dev_id设置为NULL。
中断处理例程可以在驱动程序初始化时或设备第一次打开时安装。但考虑中断资源有限,调用request_irq的正确位置应该是在设备第一次打开、硬件被告知产生中断之前。调用free_irq的位置是最后一次关闭设备、硬件被告知不用再中断处理器之后。
<三>实现中断处理例程
中断处理例程其实也是一个C函数程序,但由于发生在中断时间内,所以它有以下特点:
1、处理例程不能向用户空间发送或者接收数据,因为它不是在任何进程的上下文中执行的。
2、处理例程不能做任何可能发生休眠的操作,例如调用wait_event,锁住一个信号量等。
3、处理例程不能调用schdule函数。
中断处理例程的功能就是将有关中断接收的信息反馈给设备,并根据正在服务的中断的不同含义对数据进行相应的读或写。它的一个典型任务就是:如果中断通知进程所等待的事件已经发生,比如新的数据到达,就会唤醒在该设备上休眠的进程。
无论是快速还是慢速处理例程,程序员都应该编写执行时间尽可能短的处理例程。如果需要执行一个长时间的计算任务,最好的方法是使用tasklet或者工作队列在更安全的时间内调度计算任务。
中断处理例程irqreturn_t (*handler)(int, void *, struct pt_regs*)的参数及返回值:
int irq:中断号
void *dev_id:一种客户数据类型即驱动程序可用的私有数据,传递给request_irq函数的void *参数会在中断发生时作为参数被传回处理例程。通常,我们会为dev_id传递一个指向自己设备的数据结构指针。例如:
static irqreturn_t sample_interrupt(int irq, void *dev_id, struct pt_regs) { struct sample_dev *dev = dev_id; …… } /*与该处理例程相关联的典型open代码如下所示:*/ static void sample_open(struct inode *inode, struct file *filp) { struct sample_dev *dev = hwinfo + MINOR(inode->i_rdev); request_irq(dev->irq, sample_interrupt, 0, "sample", (void *)dev); …… return 0; }
struct pt_reg *regs:很少使用。
返回值有三种:IRQ_HANDLED;IRQ_NONE;IRQ_RETVAL(handled);
禁用和启用中断:
有时设备驱动程序必须在一个时间内阻塞中断的发生。中断的禁用分为禁用单个中断和禁用所有中断。
禁用单个中断
#include <asm/irq.h>
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
禁用所有中断
#include <asm/irq.h>
void local_irq_save(unsigned long flags);
void local_irq_disable(void);
void local_irq_restore(unsigned long flags);
void local_irq_enable(void);
<四>顶半部和底半部
中断处理的一个主要问题是怎样在处理例程内完成耗时的任务。相应一次设备中断需要完成一定数量的工作,但是中断处理例程需要尽快结束而不能使终端阻塞的时间过长,这两个需求彼此冲突。Linux通过将中断处理例程分成两部分来解决这个问题。称为顶半部的部分,是实际响应中断的例程,也就是用request_irq注册的中断例程;而所谓的底半部是一个被顶半部调度,并在稍后更安全的时间内执行的例程。顶半部处理例程和底半部处理例程之间最大的不同,就是当底半部处理例程执行时,所有的中断都是打开的——这就是所谓的更安全时间内运行。
典型的情况是顶半部保存设备的数据到一个设备特定的缓冲区并调度它的底半部,然后退出,这个操作是非常快的。然后,底半部执行其他必要的工作,例如唤醒进程、启动另外的I/O操作等等。这种方式允许在底半部工作期间,顶半部还可以继续为新的中断服务。
Linux内核有两种不同机制可以用来实现底半部处理:tasklet和工作队列。tasklet通常是底半部处理的优选机制,因为它非常快,但是所有的tasklet代码必须是原子的。工作队列具有更高的延迟,但允许休眠。
tasklet:tasklet是一个可以由系统决定的安全时刻在软件中断上下文被调度运行的特殊函数。
使用宏DECLARE_TASKLET声明tasklet:DECLARE_TASKLET(name, function, data);其中的参数name是给tasklet起的名字,function是执行tasklet时调用的函数(它带有一个unsigned long型的参数并且返回void),data是一个用来传递给tasklet函数的unsigned long类型的值。
函数tasklet_schedule用来调度一个tasklet运行。示例如下:
void example_do_tasklet(unsigned long) { …… } DECLARE_TASKLET(example_tasklet, example_do_tasklet, 0); irqreturn_t example_interrupt(int irq, void *dev_id, struct pt_regs *regs) { …… tasklet_schedule(&example_tasklet); …… }
工作队列:工作队列会在将来的某个时间、在某个特定的工作者进程上下文中调用一个函数。因为工作队列函数运行在进程上下文中,因此可在必要时休眠。工作队列一般程序编写示例如下:
static struct work_struct example_workqueue; INIT_WORK(&example_workqueue, (void (*)(void *))example_do_tasklet, NULL); irqreturn_t example_interrupt(int irq, void *dev_id, struct pt_regs *regs) { …… schedule_work(&example_workqueue); …… }
<五>中断共享
安装共享的处理例程:就像普通非共享的中断一样,共享的中断也是通过request_irq安装的,但有两处不同:
1、请求中断时,必须指定flags参数中的SA_SHIRQ位。
2、dev_id参数必须是唯一的。任何指定模块地址空间的指针都可以使用,但dev_id不能设置成NULL。
当请求一个共享中断时,如果满足下面条件之一,那么request_irq就会成功:
1、中断信号线空闲
2、任何已经注册了该中断信号线的处理例程也标识了IRQ是共享的。
使用共享处理例程的驱动程序需要小心一件事情:不能使用enable_irq和disable_irq。