Hasen的linux设备驱动开发学习之旅--中断

/**
 * Author:hasen
 * 参考 :《linux设备驱动开发详解》
 * 简介:android小菜鸟的linux
 * 	         设备驱动开发学习之旅
 * 主题:中断
 * Date:2014-11-13
 */

一、中断和定时器
           所谓中断是指CPU在执行程序的过程中,出现了某些突发事件急待处理,CPU必须暂停执行当前的程序,
转而去处理突发事件,处理完毕后CPU又返回原程序被中断的位置并继续执行。
           下图是中断的分类

           嵌入式系统以及X86 PC中大多包含可编程中断控制器(PIC),许多MCU内部就集成了PIC。如在80386中,
PIC是两片i8259A芯片的级联。通过读写PIC的寄存器,程序员可以屏蔽/使能某中断及获得中断状态,前者一般
通过中断MASK寄存器完成,后者一般通过中断PEND寄存器完成。
           定时器在硬件上也是依赖中断实现的。
二、Linux中断处理架构
           设备的中断会打断内核的正常调度和运行,系统对更高吞吐量的追求势必要求中断服务程序尽可能的
短小精悍。大多数系统中,中断到来时,工作往往不是短小的,它可能要进行大量的耗时操作。
           下图描述了Linux的内核中断机制。为了在中断时间尽可能短和中断处理需完成大工作量之间找到平衡
点,Linux将中断处理程序分为两部分:顶半部(top half)和底半部(bottom half) 。

           顶半部完成可能少的比较紧急的功能,往往只是:
           (1)简单地读取寄存器中的中断状态并清除中断标志
           (2)进行“登记中断的”工作,这意味着将底半部处理程序挂到该设备的底半部执行队列中去。
这样,顶半部执行速度回很快,可以服务更多的中断请求。
           中断工作的重心落在底半部,
           (1)它来完成中断事件的绝大多数任务。
           (2)可以被新的中断打断,这是和顶半部最大不同,顶半部往往不可中断。
           (3)底半部相对来说不是非常紧急的,比较耗时,不在硬件中断服务程序中执行。
           如果中断要处理的工作本身很少,完全可以在顶半部完成。
           在linux中,查看/proc/interrupts文件可以获得系统中中断的统计信息。
三、Linux中断编程
1、申请和释放中断
           在linux设备驱动中,使用中断的设备需要申请和释放相应的中断,分别使用内核提供的request_irq()
和free_irq()函数。
           申请IRQ

int request_irq(unsigned int irq,irq_handler_t handler,
		unsigned long irqflags,const char *devname,void *dev_id)
           ==>irq是要申请的中断号。
           ==>handler是系统登记的中断处理函数(顶半部),是一个回调函数,中断发生时,系统调用这个函数,
dev_id参数将被传递给它。
           ==>irqfags是中断处理的属性,可以指定中断的触发方式以及处理方式。在触发方面,可以是
IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW等。
在处理方式方面,若设置了IRQF_DISABLED,表明中断处理程序是快速处理程序,快速处理程序被调用时屏蔽
所有中断,慢速处理程序则不会屏蔽其他设备的驱动;若设置了IRQF_SHARED,则表示多个设备共享中断,
dev_id在中断共享时会用到,一般设置为这个设备的设备结构体或者NULL。
           request_irq()返回0表示成功,返回-EINVAL表示中断号无效或者处理函数指针为NULL,返回-EBUSY表示
中断已经被占用且不能共享。
           顶半部handler的类型irq_handler_t定义为:
typedef irqreturn_t (*irq_handler_t)(int,void *) ;
typedef int irqreturn_t ;
           释放IRQ
           与request_irq()相对应的函数是free_irq(),free_irq()的原型是:
void free_irq(unsigned int irq,void *dev_id) ;
           free_irq()中的参数的定义域request_irq()函数相同。 
2、使能和屏蔽中断
           下列3个函数用于屏蔽或者使能一个中断源:
void disable_irq(int irq) ;
void disable_irq_nosync(int irq) ;
void enable_irq(int irq) ;
           disable_irq_nosync()和disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完成。由
于disable_irq()会引起等待指定的中断被处理完,因此如果在n号中断的顶半部调用disable_irq(n),会引起系统
的死锁,这种情况下,只能调用disable_irq_nosync(n) 。
           下列两个函数(或宏,具体实现依赖于CPU体系结构)将屏蔽本CPU内的所有中断:
#define local_irq_save(flags)...
void local_irq_disable(void) ;
           前者会将目前的中断状态保留在flags中(注意flags为unsigned long类型,被直接传递,而不是通过指针
),后者直接禁止中断而不保存状态。
           与上述两个禁止中断对应的恢复中断的函数(或宏)是:
#define local_irq_restore(flags) ...
void local_irq_enable(void) ;
           以上个local开头的方法的作用范围是本CPU内。
3、底半部机制
           Linux实现底半部的机制主要有tasklet、工作队列和软中断。
           (1)tasklet
           tasklet的使用较简单,只需要定义tasklet及其处理函数并将两者关联
void my_tasklet_func(unsigned long) ;/*定义一个处理函数*/
DECLARE_TASKLET(my_tasklet,my_tasklet_func,data) ;
/*定义一个tasklet结构my_tasklet,与my_tasklet_func(data)函数相关联*/
           在需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度运行:
tasklet_schedule(&my_tasklet) ;
示例:使用tasklet作为底半部处理中断的设备驱动程序模板代码
/*定义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) ;/*调度定义的tasklet函数xxx_do_tasklet适当时候执行*/
	...
}
/*设备驱动模块加载函数*/
int __init xxx_init(void)
{
	...
	/*申请中断*/
	result = request_irq(xxx_irq,xxx_interrupt,IRQF_DISABLE,"xxx",NULL) ;
	...
	return IRQ_HANDLED ;
}
/*设备驱动模块卸载函数*/
void __exit xxx_exit()
{
	...
	free_irq(xxx_irq,xxx_interrupt) ;
	...
}
           (2)工作队列
           工作队列的使用方式和tasklet非常相似,下面的代码用于定义一个工作队列和一个底半部执行函数:
struct work_struct my_wq ;/*定义一个工作队列*/
void my_wq_func(unsigned long) ;/*定义一个处理函数*/
           通过INIT_WORK()可以初始化这个工作队列并将工作队列与处理函数绑定:
INIT_WORK(&my_wq,(void (*)(void *))my_wq_func ,NULL) ;
/*初始化工作队列并将其与处理函数绑定*/
           与tasklet_schedule()对应的用于调度工作队列执行的函数为schedule_work(),如:
schedule_work(&mt_wq) ;/*调度工作队列执行*/
示例:使用工作队列处理中断底半部的设备驱动程序模板
/*定义工作队列和关联函数*/
struct work_struct xxx_wq ;
void xxx_do_work(struct long) ;

/*中断处理底半部*/
void xxx_do_work(unsigned long)
{
	...
}

/*中断处理顶半部*/
irqreturn_t xxx_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{
	...
	schedule_work(&xxx_wq) ;
	...
	return IRQ_HANDLED ;
}

/*设备驱动模块加载函数*/
int xxx_init(void)
{
	...
	/*申请中断*/
	result = request_irq(xxx_irq,xxx_interrupt,IRQF_DISABLED,"xxx",NULL) ;
	...
	/*初始化工作队列*/
	INIT_WORK(&xxx_wq,(void (*)(void *))xxx_do_work,NULL) ;
	...
}

/*设备驱动模块卸载函数*/
void xxx_exit(void)
{
	...
	/*释放中断*/
	free_irq(xxx_irq,xxx_interrupt) ;
	...
}
           (3)软中断
           软中断是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,tasklet是基于软中断
实现的,因此也运行于软中断上下文。
           Linux内核中,用softirq_action结构体表征一个软中断,这个结构体包含软中断处理函数指针和传递
给该函数的参数。使用open_softirq()函数可以注册中断对应的处理函数,而raise_softirq()函数可以触发一个
软中断。
           软中断和tasklet运行于软中断上下文,仍然属于原子上下文的一种,而工作队列则运行于进程上下文。
因此,软中断和tasklet处理函数中不能睡眠,而工作队列处理函数中允许睡眠。
           local_bh_disable()和local_bh_enable()是内核中用于禁止和使能中断和tasklet底半部机制的函数。
           内核中采用softirq的地方包括JI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、
NET_RX_SOFTIRQ、SCSI_SOFTIRQ、TASKLET_SOFTIRQ等,一般来说,驱动工程师不宜直接使用softirq。
4、中断共享
           多个设备共享一根硬件中断线在实际硬件系统中广泛存在。下面是共享中断的使用方法:
           (1)共享中断的多个设备在申请中断时,都应该使用IRQF_SHARED标志,而且一个设备以IRQF_SHARED
申请某中断成功的前提是该中断未被申请,或该中断虽然已经被申请了,但是之前申请该中断的多有设备也都以
IRQF_SHARED标志申请该中断。
           (2)尽管内核模块可访问的全局地址都可以作为request_irq(...,void* dev_id)的最后一个参数,但是
设备结构体指针显然是可传入的最佳参数。
           (3)在中断到来时,会遍历执行此中断处理程序,直到某一个函数返回IRQ_HANDLED。中断处理程序后半
部中,应迅速地根据硬件将寄存器中的信息比照传入的dev_id参数判断是否是本设备的中断,若不是,应迅速返
回IRQ_NONE。

示例:共享中断编程模板
/*中断处理顶半部*/
irqreturn_t xxx_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{
	...
	int status = read_int_status() ;/*获知中断源*/
	if(!is_myint(dev_id,status)) /*判断是否是本设备中断*/
		return IRQ_NONE ; /*不是本设备中断,立即返回*/
	
	/*是本设备中断,进行处理*/
	...
	return IRQ_HANDLED ;/*返回IRQ_HANDLED表明中断已被处理*/
}
/*设备驱动模块加载函数*/
int xxx_init(void)
{
	...
	/*申请共享中断*/
	result = request_irq(sh_irq,xxx_interrupt,IRQF_SHARED,"xxx",xxx_dev) ;
	...
}
/*设备驱动模块卸载函数*/
void xxx_exit(void)
{
	...
	/*释放中断*/
	free_irq(xxx_irq,xxx_interrupt) ;
	...
}
实例:S3C6410时钟中断
           S3C6410处理器内部集成了实时钟(RTC)模块,该模块能够在系统断电的情况下由后备电池继续工作,其
主要功能相当于一个时钟,记录年、月、日、时、分、秒等。S3C6410的RTC可产生两种中断,周期节拍(tick)
和报警(alarm)中断,前者相当于定时器,后者相当于一个“闹钟”,它在预先预定的时间到来时产生中断。
           S3C6410实时钟设备驱动的open()函数中,会申请它将要使用的中断。
static int s3c_rtc_open(struct device *dev)
{
	struct platform_device *pdev = to_platform_device(dev) ;
	struct rtc_device *rtc_dev = platform_get_drvdata(pdev) ;
	int ret ; 
	/*申请alarm中断*/
	ret = request_irq(s3c_rtc_alarmno,s3c_rtc_alarmirq,
			IRQF_DISABLE,"s3c2410-rtc alarm",rtc_dev) ;
	if(ret){
		dev_err(dev,"IRQ%d error %d\n",s3c_rtc_alarmno,ret);
		return ret ;
	}
	/*申请tick中断*/
	ret = request_irq(s3c_rtc_tickno,s3c_rtc_tickirq,
			IRQF_DISABLE,"s3c2410-rtc tick",rtc_dev) ;
	if(ret){
		dev_err(dev,"IRQ%d error %d\n",s3c_rtc_tickno,ret);
		goto tick_err ;
	}
	return ret ;
tick_err :
	free_irq(s3c_rtc_alarmno,rtc_dev) ;
	return ret ;
}
           S3C6410实时钟设备驱动的release()函数中,会释放它将要使用的中断。
static void s3c_rtc_release(struct device *dev)
{
    struct platform_device *pdev = to_platform_device(dev) ;
    struct rtc_device *rtc_dev = platform_get_drvdata(pdev) ;
    
    s3c_rtc_setpie(dev,0) ;
    /*释放中断*/
    free_irq(s3c_rtc_alarmno,rtc_dev) ;
    free_irq(s3c_rtc_tickno,rtc_dev) ;
}
           S3C6410实时钟驱动的中断处理比较简单,没有明确地分为上下两个半部,只有顶半部。
static irqreturn_t s3c_rtc_alarmirq(int irq,void *id)
{
     struct rtc_device *rdev = id ;
     rtc_update_irq(rdev,1,RTC_AF|RTC_IRQF) ;
     s3c_rtc_set_bit_byte(s3c_rtc_base,S3C2410_INTP,S3C2410_INTP_ALM) ;
     return IRQ_HANDLED ;
}
           
static irqreturn_t s3c_rtc_tickirq(int irq,void *id)
{
     struct rtc_device *rdev = id ;
     rtc_update_irq(rdev,1,RTC_PF|RTC_IRQF) ;
     s3c_rtc_set_bit_byte(s3c_rtc_base,S3C2410_INTP,S3C2410_INTP_TIC) ;
     return IRQ_HANDLED ;
}
           代码中调用的rtc_update_irq()函数定义于drivers/rtc/interface.c文件中,被各种实时钟驱动共享。
void rtc_update_irq(struct rtc_device *rtc,unsigned long num,unsigned long events)
{
     spin_lock(&rtc->rtc_lock) ;
     rtc->irq_data = (rtc->irq_data + (num << 8)) | events ;
     spin_unlock(&rtc->irq_lock) ;
     
     spin_lock(&rtc->irq_task_lock) ;
     if(rtc->irq_task)
    	 rtc->irq_task->func(rtc->irq_task->private_data) ;
     spin_unlock(&rtc->irq_task_lock) ;
     
     wake_up_interuptible(&rtc->irq_queue) ;
     kill_fasync(&rtc->async_queue,SIGIO,POLL_IN) ;
}
           上述中断处理程序中没有底半部(没有严格意义上的tasklet,工作队列或者软中断底半部),实际上,它只
是唤醒一个等待队列(rtc->irq_queue)并发出一个SIGIO信号,而这个等待队列的唤醒也将导致一个阻塞的进程被
执行(这个阻塞的进程可以看做底半部)。等待队列可以作为中断处理程序顶半部和进程同步的一种良好机制。但
是,任何情况下,都不能在顶半部等待一个等待队列,而只能唤醒。


总结:
           Linux的中断分为两个半部,顶半部处理紧急的硬件操作,底半部处理不紧急的耗时操作,tasklet和工
作队列都是调度中断底半部的良好机制,tasklet基于软中断实现。内核定时器也依靠软中断实现。


郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。