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

来源:互联网 发布:求婚大作战日剧知乎 编辑:程序博客网 时间:2024/06/03 09:45
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. /** 
  2.  * Author:hasen 
  3.  * 参考 :《linux设备驱动开发详解》 
  4.  * 简介:android小菜鸟的linux 
  5.  *           设备驱动开发学习之旅 
  6.  * 主题:中断 
  7.  * Date:2014-11-13 
  8.  */  

一、中断和定时器
           所谓中断是指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

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. int request_irq(unsigned int irq,irq_handler_t handler,  
  2.         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定义为:
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. typedef irqreturn_t (*irq_handler_t)(int,void *) ;  
  2. typedef int irqreturn_t ;  
           释放IRQ
           与request_irq()相对应的函数是free_irq(),free_irq()的原型是:
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. void free_irq(unsigned int irq,void *dev_id) ;  
           free_irq()中的参数的定义域request_irq()函数相同。 
2、使能和屏蔽中断
           下列3个函数用于屏蔽或者使能一个中断源:
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. void disable_irq(int irq) ;  
  2. void disable_irq_nosync(int irq) ;  
  3. void enable_irq(int irq) ;  
           disable_irq_nosync()和disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完成。由
于disable_irq()会引起等待指定的中断被处理完,因此如果在n号中断的顶半部调用disable_irq(n),会引起系统
的死锁,这种情况下,只能调用disable_irq_nosync(n) 。
           下列两个函数(或宏,具体实现依赖于CPU体系结构)将屏蔽本CPU内的所有中断:
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. #define local_irq_save(flags)...  
  2. void local_irq_disable(void) ;  
           前者会将目前的中断状态保留在flags中(注意flags为unsigned long类型,被直接传递,而不是通过指针
),后者直接禁止中断而不保存状态。
           与上述两个禁止中断对应的恢复中断的函数(或宏)是:
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. #define local_irq_restore(flags) ...  
  2. void local_irq_enable(void) ;  
           以上个local开头的方法的作用范围是本CPU内。
3、底半部机制
           Linux实现底半部的机制主要有tasklet、工作队列和软中断。
           (1)tasklet
           tasklet的使用较简单,只需要定义tasklet及其处理函数并将两者关联
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. void my_tasklet_func(unsigned long) ;/*定义一个处理函数*/  
  2. DECLARE_TASKLET(my_tasklet,my_tasklet_func,data) ;  
  3. /*定义一个tasklet结构my_tasklet,与my_tasklet_func(data)函数相关联*/  
           在需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度运行:
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. tasklet_schedule(&my_tasklet) ;  
示例:使用tasklet作为底半部处理中断的设备驱动程序模板代码
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. /*定义tasklet和底半部函数相关联*/  
  2. void xxx_do_tasklet(unsigned long) ;  
  3. DECLARE_TASKLET(xxx_tasklet,xxx_do_tasklet,0) ;  
  4.   
  5. /*中断处理底半部*/  
  6. void xxx_do_tasklet(unsigned long)  
  7. {  
  8.     ...  
  9. }  
  10. /*中断处理顶半部*/  
  11. irqreturn_t xxx_interrupt(int irq,void* dev_id)  
  12. {  
  13.     ...  
  14.     tasklet_schedule(&xxx_tasklet) ;/*调度定义的tasklet函数xxx_do_tasklet适当时候执行*/  
  15.     ...  
  16. }  
  17. /*设备驱动模块加载函数*/  
  18. int __init xxx_init(void)  
  19. {  
  20.     ...  
  21.     /*申请中断*/  
  22.     result = request_irq(xxx_irq,xxx_interrupt,IRQF_DISABLE,"xxx",NULL) ;  
  23.     ...  
  24.     return IRQ_HANDLED ;  
  25. }  
  26. /*设备驱动模块卸载函数*/  
  27. void __exit xxx_exit()  
  28. {  
  29.     ...  
  30.     free_irq(xxx_irq,xxx_interrupt) ;  
  31.     ...  
  32. }  
           (2)工作队列
           工作队列的使用方式和tasklet非常相似,下面的代码用于定义一个工作队列和一个底半部执行函数:
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. struct work_struct my_wq ;/*定义一个工作队列*/  
  2. void my_wq_func(unsigned long) ;/*定义一个处理函数*/  
           通过INIT_WORK()可以初始化这个工作队列并将工作队列与处理函数绑定:
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. INIT_WORK(&my_wq,(void (*)(void *))my_wq_func ,NULL) ;  
  2. /*初始化工作队列并将其与处理函数绑定*/  
           与tasklet_schedule()对应的用于调度工作队列执行的函数为schedule_work(),如:
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. schedule_work(&mt_wq) ;/*调度工作队列执行*/  
示例:使用工作队列处理中断底半部的设备驱动程序模板
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. /*定义工作队列和关联函数*/  
  2. struct work_struct xxx_wq ;  
  3. void xxx_do_work(struct long) ;  
  4.   
  5. /*中断处理底半部*/  
  6. void xxx_do_work(unsigned long)  
  7. {  
  8.     ...  
  9. }  
  10.   
  11. /*中断处理顶半部*/  
  12. irqreturn_t xxx_interrupt(int irq,void *dev_id,struct pt_regs *regs)  
  13. {  
  14.     ...  
  15.     schedule_work(&xxx_wq) ;  
  16.     ...  
  17.     return IRQ_HANDLED ;  
  18. }  
  19.   
  20. /*设备驱动模块加载函数*/  
  21. int xxx_init(void)  
  22. {  
  23.     ...  
  24.     /*申请中断*/  
  25.     result = request_irq(xxx_irq,xxx_interrupt,IRQF_DISABLED,"xxx",NULL) ;  
  26.     ...  
  27.     /*初始化工作队列*/  
  28.     INIT_WORK(&xxx_wq,(void (*)(void *))xxx_do_work,NULL) ;  
  29.     ...  
  30. }  
  31.   
  32. /*设备驱动模块卸载函数*/  
  33. void xxx_exit(void)  
  34. {  
  35.     ...  
  36.     /*释放中断*/  
  37.     free_irq(xxx_irq,xxx_interrupt) ;  
  38.     ...  
  39. }  
           (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。

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


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

0 0
原创粉丝点击