Linux设备驱动程序学习(六)——中断处理

  中断是指 CPU 在执行程序的过程中,出现了某些突发事件时 CPU 必须暂停执行当前的程序,转去处理突发事件,处理完毕后 CPU 又返回原程序被中断的位置并继续执行。

中断的分类

根据中断来源分类

  根据中断的来源,中断可分为内部中断和外部中断

  • 内部中断的中断源来自 CPU内部(软件中断指令、溢出、除法错误等,例如,操作系统从用户态切换到内核态需借助 CPU 内部的软件中断);
  • 外部中断的中断源来自CPU外部,由外设提出请求。

根据是否可屏蔽中断分类

  根据是否可以屏蔽中断分为可屏蔽中断与不屏蔽中断(NMI)

  • 可屏蔽中断可以通过屏蔽字被屏蔽,屏蔽后,该中断不再得到响应;
  • 不可屏蔽中断不能被屏蔽。

根据中断入口跳转方式的不同分类

  根据中断入口跳转方法的不同,中断分为向量中断和非向量中断

  • 采用向量中断的 CPU 通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。不同中断号的中断有不同的入口地址。
  • 非向量中断的多个中断共享一个入口地址,进入该入口地址后再通过软件判断中断标志来识别具体是哪个中断。也就是说,向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址。

中断的实现

Linux内核中断处理机制

  为了在中断执行时间尽可能短和中断处理需完成大量工作之间找到一个平衡点,Linux 将中断处理程序分解为两个半部:顶半部(top half)和底半部(bottomhalf)

  • 顶半部:紧急的硬件操作;实际响应中断的例程(request_irq注册的那个例程);顶半部完成尽可能少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态并清除中断标志后就进行“登记中断”的工作。“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,可以服务更多的中断请求。
  • 底半部:延缓的延时操作;被顶半部调用并在稍后更安全的一个时间里执行的函数;中断处理的重心是底半部,它完成中断任务的绝大多数任务。底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断。这也是底半部和顶半部的最大不同:顶半部中断往往被设计成不可中断的。底半部则相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。

  注意:尽管顶半部、底半部的结合能够改善系统的响应能力,但是,僵化地认为 Linux设备驱动中的中断处理一定要分两个半部则是不对的。如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。

中断处理函数

  1. 申请和释放中断
    在 Linux 设备驱动中,使用中断的设备需要申请和释放对应的中断,分别使用内核提供的 request_irq()和 free_irq()函数。
    申请IRQ函数
 int request_irq(unsigned int irq, void (*handler)(int irq, void *dev_id, struct pt_regs *regs), 
                                                    unsigned long irqflags, 
                                                    const char * devname, 
                                                    void *dev_id);                                                  

   函数返回值: 0 表示成功,或返回一个负的错误码,如 -EBUSY 表示另一个驱动已经占用了你所请求的中断线。
  参数详解

  • unsigned int irq:要申请的中断号
  • irqreturn_t (*handler)(int,void *,struct pt_regs *): handler是向系统登记的中断处理函数,是一个回调函数,中断发生时,系统调用这个函数,dev_id 参数将被传递给它。
  • unsigned long flags: flags 是中断处理的属性,若设置了 SA_INTERRUPT,则表示中断处理程序是快速处理程序,快速处理程序被调用时屏蔽所有中断,慢速处理程序不屏蔽;若设置了 SA_SHIRQ,则表示多个设备共享中断,dev_id 在中断共享时会用到,一般设置为这个设备的设备结构体或者NULL。
  • const char *dev_name:传递给request_irq的字符串,用来在/proc/interrupts显示中断的拥有者。
  • void *dev_id:用于共享的中断信号线,它是唯一的标识,在中断线空闲时可以使用它,驱动程序也可以用它来指向自己的私有数据区(来标识哪个设备产生中断)。若中断没有被共享,dev_id 可以设置为 NULL,但推荐用它指向设备的数据结构。

  释放IRQ函数,request_irq()相对应的函数为 free_irq(),free_irq()的原型如下:
   void free_irq(unsigned int irq,void *dev_id);
  free_irq()中参数的定义与 request_irq()相同。

  1. 使能和屏蔽中断
    下列 3 个函数用于屏蔽一个中断源:
          void disable_irq(int irq); 
          void disable_irq_nosync(int irq); 
          void enable_irq(int irq); 

  disable_irq_nosync()与 disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完成。注意,这 3 个函数作用于可编程中断控制器,因此,对系统内的所有CPU 都生效。

  下列两个函数将屏蔽本 CPU 内的所有中断:

    void local_irq_save(unsigned long flags); 
    void local_irq_disable(void); 

  前者会将目前的中断状态保留在 flags 中,注意 flags 被直接传递,而不是通过指针传递。后者直接禁止中断。

  与上述两个禁止中断对应的恢复中断的方法如下:

void local_irq_restore(unsigned long flags); 
void local_irq_enable(void); 

  注意:以上各 local_开头的方法的作用范围是本 CPU 内。

  1. 自动检测中断号(IRQ)
    驱动程序初始化时,最迫切的问题之一就是如何决定设备要使用哪一条IRQ信号线,驱动程序需要这个信息以便正确的安装处理例程,尽管程序员可以在装载时指定中断号,但是在大部分时间用户都不知道这个中断号,因此,中断号的自动检测对于驱动程序可用性来说是一个基本要求。
    有时,自动检测依赖于一些设备拥有的默认特性,因此,驱动设备可以假定设备使用了这些默认值,这也就是short在检测并口的默认行为:
           if (short_irq < 0)          /* 依靠使并口的端口号,确定中断*/
              switch(short_base) {
                   case 0x378: short_irq = 7; break;
                   case 0x278: short_irq = 2; break;
                   case 0x3bc: short_irq = 5; break;
          }

  有些设备的设计更为先进,会简单的“申明”它们要使用的中断,这样,驱动程序就可以通过从设备的某个I/O端口或者PCI配置空间中读出一个状态值来获取中断号。当目标设备有能力告诉驱动程序他将使用的中断号时,自动检测中断号就只是一位着探测设备而不需要额外的工作来探测中断,虽然大部分硬件都是以这种方式工作,但是有些设备还是需要自动检测:驱动程序通知设备产生中断并观察会发生什么,如果一切正常,那么只有一条中短线被激活。

探测中断的两种方式:

内核定义的探测

  linux提供了一个底层设施来探测中断号。它只能在非共享中断的模式下工作,但是大多数硬件有能力工作在共享中断的模式下,并可提供更好的找到配置中断号的方法,内核提供的这一设施由两个函数组成,在头文件中声明:

<linux/interrupt.h>
unsigned long probe_irq_on(void);
/*返回一个未分配中断的位掩码。驱动必须保留返回的位掩码,并在后边传递给probe_irq_off,在调用它之后,
驱动程序应当至少安排它的设备产生一次中断*/
int probe_irq_off(unsigned long);
/*在请求设备产生一次中断后,驱动调用这个函数,并将probe_irq_on产生的位掩码作为参数传递给probe_irq_off,
probe_irq_off返回在probe_on之后发生的中断号。如果没有中断发生,返回0,如果产生了多次中断,返回一个负值。*/

  应当注意在调用 probe_irq_on 之后启用设备上的中断, 并在调用 probe_irq_off 前禁用。此外还必须记住在 probe_irq_off 之后服务设备中待处理的中断。
  以下是LDD3中的并口示例代码,(并口的管脚 9 和 10 连接在一起,探测五次失败后放弃):

intcount= 0;
do
{
        unsigned long mask;
        mask = probe_irq_on();
        outb_p(0x10,short_base+2);                /* enable reporting */
        outb_p(0x00,short_base);                  /* clear the bit */
        outb_p(0xFF,short_base);                   /* set the bit: interrupt! */
        outb_p(0x00,short_base+2);                 /* disable reporting */
        udelay(5);                                /* give it some time */
        short_irq = probe_irq_off(mask);

        if (short_irq== 0){/* none of them? */
                printk(KERN_INFO "short: no irq reported by probe\n");
                short_irq = -1;
        }
} while (short_irq < 0 && count++< 5);
if (short_irq< 0)
        printk("short: probe failed %i times, giving up\n",count);

  最好只在模块初始化时探测中断线一次,大部分体系定义了这两个函数( 即便是空的 )来简化设备驱动的移植。

DIY探测

  DIY探测与前面原理相同: 使能所有未使用的中断, 接着等待并观察发生什么。我们对设备的了解:通常一个设备能够使用3或4个IRQ 号中的一个来进行配置,只探测这些 IRQ 号使我们能不必测试所有可能的中断就探测到正确的IRQ 号。
  下面的LDD3中的代码通过测试所有"可能的"中断并且察看发生的事情来探测中断。 :

int trials[]= {3, 5, 7, 9, 0};  //trials 数组列出要尝试的中断, 以 0 作为结尾标志; 
int tried[]={0, 0, 0, 0, 0};  //tried 数组用来跟踪哪个中断号已经被这个驱动注册
int i, count = 0;
for (i = 0; trials[i]; i++)
        tried[i]= request_irq(trials[i], short_probing,
                              SA_INTERRUPT, "short probe", NULL);
        do
       {
                short_irq = 0;                        /* none got, yet */
                outb_p(0x10,short_base+2);            /* enable */
                outb_p(0x00,short_base);
                outb_p(0xFF,short_base);              /* toggle the bit */
                outb_p(0x00,short_base+2);            /* disable */
                udelay(5);                           /* give it some time */
         /* 等待中断,若在这段时间有中断产生,handler会改变 short_irq */
               if (short_irq== 0){                   
                       printk(KERN_INFO "short: no irq reported by probe\n");
        }
 } while (short_irq <=0&&count++< 5);

/*循环结束,卸载处理程序 */
for (i = 0; trials[i]; i++)
        if (tried[i]== 0)
                free_irq(trials[i],NULL);
if (short_irq< 0)
        printk("short: probe failed %i times, giving up\n",count);

  以下是handler的源码:

irqreturn_t short_probing(int irq,void*dev_id,struct pt_regs*regs)
{
        if (short_irq== 0) short_irq= irq;/* found */
        if (short_irq!= irq) short_irq=-irq;/* ambiguous */
        return IRQ_HANDLED;
}

  若事先不知道"可能的" IRQ ,就需要探测所有空闲的中断,所以不得不从 IRQ 0 探测到 IRQ NR_IRQS-1

底半部实现的三种方式

  Linux 内核有 3 个不同的机制可用来实现底半部处理:

  • tasklet (首选机制),它非常快, 但是所有的 tasklet 代码必须是原子的;

  • 工作队列,它可能有更高的延时,但允许休眠;

  • 软中断。

tasklet(小任务机制)

  tasklet 的使用较简单,我们只需要定义 tasklet 及其处理函数并将两者关联,例如:

void my_tasklet_func(unsigned long);     /*定义一个处理函数*/ 
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);   /*定义一个 tasklet 结构 my_tasklet,与 my_tasklet_func(data)函数相关联*/ 

  代码 DECLARE_TASKLET(my_tasklet,my_tasklet_func,data)实现了定义名称为my_tasklet 的 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, struct pt_regs *regs) 
 { 
... 
 tasklet_schedule(&xxx_tasklet); 
 ... 
 }  
 /*设备驱动模块加载函数*/ 
 int __init xxx_init(void) 
 { 
 ... 
 /*申请中断*/ 
 result = request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, "xxx", NULL); 
 ... 
}  
 /*设备驱动模块卸载函数*/ 
 void __exit xxx_exit(void) 
 { 
 ... 
 /*释放中断*/ 
free_irq(xxx_irq, xxx_interrupt); 
 ... 
} 

  上述程序在模块加载函数中申请中断,并在模块卸载函数中释放它。对应于 xxx_irq 的中断处理程序被设置为 xxx_interrupt()函数,在这个函数中,tasklet_schedule(&xxx_tasklet)调度的 tasklet 函数xxx_do_tasklet()在适当的时候得到执行。

工作队列

  工作队列的使用方法和 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(&my_wq); /*调度工作队列执行*/

  使用工作队列处理中断底半部的设备驱动程序模板:

 /*定义工作队列和关联函数*/ 
struct work_struct xxx_wq; 
void xxx_do_work(unsigned long);  
 /*中断处理底半部*/ 
 void xxx_do_work(unsigned long) 
{ 
 ... 
 } 
 /*中断处理顶半部*/ 
 irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs) 
{ 
 ... 
 schedule_work(&xxx_wq); 
 ... 
 } 
 /*设备驱动模块加载函数*/ 
 int xxx_init(void) 
 { 
 ... 
 /*申请中断*/ 
 result = request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, "xxx", NULL); 
 ... 
 /*初始化工作队列*/ 
 INIT_WORK(&xxx_wq, (void (*)(void *)) xxx_do_work, NULL); 
 ... 
 }  
 /*设备驱动模块卸载函数*/ 
void xxx_exit(void) 
{ 
 ... 
 /*释放中断*/ 
 free_irq(xxx_irq, xxx_interrupt); 
 ... 
 } 

  与tasklet 不同的是,上述程序在设计驱动模块加载函数中增加了初始化工作队列的代码。
  尽管 Linux 专家们多建议在设备第一次打开时才申请设备的中断并在最后一次关闭时释放中断以尽量减少中断被这个设备占用的时间,但是,大多数情况下,为求省事,大多数驱动工程师还是将中断申请和释放的工作放在了设备驱动的模块加载和卸载函数中。

软中断

  • 软中断是用软件方式模拟硬件中断的概念,实现宏观上的异步执行效果,tasklet也是基于软中断实现的
  • 硬中断是外部设备对 CPU 的中断,软中断通常是硬中断服务程序对内核的中断,而信号则是由内核(或其他进程)对某个进程的中断
    在 Linux 内核中,用 softirq_action 结构体表征一个软中断,这个结构体中包含软中断处理函数指针和传递给该函数的参数。使用 open_softirq()函数可以注册软中断对应的处理函数,而 raise_softirq()函数可以触发一个软中断。
    软中断和 tasklet 仍然运行于中断上下文,而工作队列则运行于进程上下文。因此,软中断和 tasklet 处理函数中不能睡眠,而工作队列处理函数中允许睡眠。
    local_bh_disable()和 local_bh_enable()是内核中用于禁止和使能软中断和 tasklet 底半部机制的函数。

中断共享

  通过 request_irq来安装共享中断与非共享中断有2点不同

  • 当request_irq时,flags中必须指定SA_SHIRQ位;
  • dev_id必须唯一。任何指向模块地址空间的指针都行,但dev_id绝不能设置为NULL。
    内核为每个中断维护一个中断共享处理例程列表,dev_id就是区别不同处理例程的签名。释放处理例程通过执行free_irq实现。 dev_id用来从这个中断的共享处理例程列表中选择正确的处理例程来释放,这就是为什么dev_id必须是唯一的。

  请求一个共享的中断时,如果满足下列条件之一,则request_irq成功:

  • 中断线空闲;
  • 所有已经注册该中断信号线的处理例程也标识了IRQ是共享。

注意:

  1. 一个共享的处理例程必须能够识别自己的中断,并且在自己的设备没有被中断时快速退出(返回IRQ_NONE)。
  2. 共享处理例程没有探测函数可用,但使用的中断信号线是空闲时标准的探测机制才有效。
  3. 一个使用共享处理例程的驱动需要小心:不能使用enable_irq或disable_irq,否则,对其他共享这条线的设备就无法正常工作了。即便短时间禁止中断,另一设备也可能产生延时而为设备和其用户带来问题。所以程序员必须记住:它的驱动并不是独占这个IRQ,它的行为应当比独占这个中断线更加"社会化"。

中断驱动I/O

   当与驱动程序管理的硬件间的数据传送可能因为某种原因而延迟,驱动编写者应当实现缓存。一个好的缓存机制需采用中断驱动的I/O,一个输入缓存在中断时被填充,并由读取设备的进程取走缓冲区的数据,一个输出缓存由写设备的进程填充,并在中断时送出数据。
  为正确进行中断驱动的数据传送,硬件应能够按照下列语义产生中断:

  • 输入:当新数据到达时并处理器准备好接受时,设备中断处理器。
  • 输出:当设备准备好接受新数据或确认一个成功的数据传送时,设备产生中断。

你可能感兴趣的:(Linux设备驱动程序,Linux设备驱动程序,linux,中断处理,设备驱动程序)