【Linux 驱动】第十章 中断处理

一,概念

       1)什么是中断?

            1>引入:

               我们知道,处理器的处理速度比硬件来说要快上N个数量级,那么由处理器向硬件发出请求并等待回应的办法显然是不可取的,在这期间处理器浪费了大量的时间。这些时间应该被用来处理其他的事务。轮询可能是解决办法之一,但显然这样的办法也会让处理器做大量的无用功。
           2>解决:

               最好的办法,就是让硬件在需要的时候才向内核发出信号,然后处理器去响应硬件的请求。这就是中断机制。一个“中断”仅是一个信号,当硬件需要获得处理器对它的关注时,就可以发送这个信号。Linux 处理中断的方式非常类似在用户空间处理信号的方式。 大多数情况下,一个驱动只需要为它的设备的中断注册一个处理例程,并当中断到来时进行正确的处理。本质上来讲,中断处理例程和其他的代码并行运行。因此,它们不可避免地引起并发问题,并竞争数据结构和硬件。 透彻地理解并发控制技术对中断来讲非常重要。    


            3>执行:

               当硬件需要和处理器通信时,会产生一个电信号(即中断信号),并发往处理器,处理器收到后,会告诉OS,然后由OS进行后续处理。每一种硬件设备都有其专门的中断值,OS得以通过这个来进行区分,到底是哪个设备发生了中断。这些中断值又被称为中断请求(IRQ)   当然有的IRQ是动态分配的。其实OS关注的并不是某个设备一定要产生某个特定的IRQ,而是一个特定的IRQ要和一个特定的设备有映射的关系,而且OS需要知道这种关系。这就表明,即便是IRQ在多个设备之间进行共享也是可以的,只要OS能够知道这些映射关系,并且能够有办法区分某一个时刻产生中断的设备是哪一个即可。
       

        2)中断处理程序

              中断处理程序,顾名思义,自然就是发生中断时,需要调用的处理函数。
              特点:1、不是每设备一个处理程序,而是每中断一个处理程序。
                         2、运行于特殊的上下文中:中断上下文
                         3、一般的中断处理程序,都会关中断,考虑到中断随时都有可能发生,处理程序应当尽可能的高效
                         4、一般中断处理程序都是要和硬件打交道的
        3)中断上下文
           1>进程上下文

               对内核而言,处于进程上下文表明内核处于这样一种模式:进程在执行(系统调用或者运行内核线程) 可以通过current宏关联当前进程。进程以进程上下文的形式关联内核,使得在进程上下文中可以睡眠,也可以调用调度程序。

          2>中断上下文

               中断上下文,则和进程上下文几乎完全相反:和进程无关、和current宏无关不能睡眠、不能调用会导致睡眠的函数。另外,处于中断上下文的代码,应当迅速、简洁,尽量把工作放到下半部中去完成。

               关于中断使用的堆栈:2.6的内核之前,中断没有自己的堆栈,而是与被中断的内核线程共享该线程的堆栈。2.6之后,内核增加了一个选项:每个内核线程只提供一页内存,这减轻了内存的压力,也同时促使中断被分离了出来:每处理器一页,称为中断栈
      

         4)中断的实现机制

              Linux中,中断的处理机制是依赖于体系结构的(处理器、中断控制器、体系结构的设计、机器本身)。下图是中断的路由

       
       5)关于中断的顶半部和底半部

             我们要把中断处理中需要做的工作区分开来:中断处理程序中,只处理那些有严格时间限制的工作,比如复位硬件,对中断进行应答等。而那些可以拖到后面做的,或者说有可能睡眠的处理,都应当放到下半部去处理这样做的目的很显然,就是让中断处理程序尽可能的简洁明快在适当的时机,下半部会开中断执行

            “顶半部”:是实际响应中断的例程(request_irq 注册的那个例程)。
            “底半部”:是被顶半部调度,并在稍后更安全的时间内执行的函数。




2中断控制

为什么要控制中断?控制中断的原因,说到底还是为了要进行同步。

通过禁止中断,可以确保该中断不会抢占当前运行的代码。禁止中断还可以禁止内核抢占。

需要注意的是,中断都是对每处理器而言(中断堆栈),也就是说,禁止中断并不能够保证自己使用的数据不会被其他处理器的并发进程所访问到。因此如果在使用某些全局的数据时,需要考虑对其进行加锁保护。

即:锁提供机制,防止来自其他处理器(当然也可以是本处理器)的并发访问,中断提供机制,防止来自其他中断处理程序的并发访问。
2.1禁止和激活中断

这里需要注意的是,内核提供两类接口:禁止/激活中断,保存/恢复中断状态。前者比较傻瓜,会无条件的禁止/激活中断,这需要使用者对当前的中断状态十分确定。而后者则相对更容易使用,免去了判断

另外关于cli()与sti():在2.5版本之前的内核,提供“禁止所有处理器上的中断”这样的功能,现在已经去掉了,需要开发人员用锁来避免并发。这么做的好处是,是的代码更加流线化,不会簇拥成团。而且使用粒度更细的锁,会比全局锁要效率高。
2.2禁止指定中断线

我们当然不用禁止全局的中断,有时候禁止某一条中断线就可以了,这是指:凡是在该中断线上产生的中断,都将不会报告给处理器。

一般对于有多个中断处理程序共享的中断线,并不建议使用这个功能,因为这会导致这条线上的其他的设备无法传递中断。
2.3中断系统的状态

有时候我们需要判断代码所处的状态:是否在中断上下文中。因为有些操作是只有在进程上下文中才能够进行的,比如睡眠。

系统提供接口,来判断中断是否被禁止、是否处于中断上下文、是否正在执行中断处理程序。
3下半部

一般的中断处理都会分为两个部分,前面讲到的中断处理程序只是所谓的上半部,这是系统处理中断不可或缺的一部分。但是由于中断处理程序本身的局限,仅靠上半部是无法高效的处理系统的所有中断的,这就需要下半部来提供支持。
3.1为什么需要下半部
3.1.1上半部的局限

    中断处理程序异步执行,且有可能打断其他代码,包括其他的中断处理程序

    中断处理程序执行时,会禁止该中断同级的其他中断,甚至禁止全局所有中断

    往往需要对硬件进行操作

    中断处理程序在中断上下文中运行,有很多限制

以上几点,就要求中断处理程序不仅要简洁、高效,而且对于阻塞这种行为也是不能支持的,这就导致上半部的限制很多。
3.1.2使用下半部

    处理中断时,工作推后到下半部的原因,就是为了突破上面提到的局限性,要尽可能的将那些可以推后执行的工作都放到下半部,提高系统的响应速度

    下半部何时被调用?这个跟中断处理选择用何种下半部机制有关,可以是推后一段时间,也可以是通过定时器

    下半部的特点:在进行处理的时候,随时都有可能响应中断

    除了Linux,很多其他的操作系统也采用了同样的机制

3.2下半部介绍

    下半部的实现方式,在2.6的内核中,有3种:软中断、tasklet、工作队列。软中断用的比较少,只有有限的几种使用场景,更多的是使用tasklet

    内核定时器,同样也能够将工作推后一段时间(精确的)进行

3.3软中断
3.3.1软中断的实现

    软中断是在内核编译期间静态分配的,不能够动态裁剪。系统定义了一个32个元素的数组,但是目前只用到了寥寥几个。多数还是通过tasklet来实现

    软中断不会抢占相同处理器上的其他软中断,软中断会被中断给打断

    软中断的执行:中断处理程序在返回之前,将对应的软中断进行标记(触发软中断),然后系统会在合适的时机检查该标记,并执行软中断(从中断处理程序返回时、在ksoftirqd内核线程中、显示检查与处理软中断的代码)。

3.3.2软中断的使用

    软中断是给系统中对时间要求最严格,以及最重要的下半部来使用。目前只有两个子系统在使用:网络子系统以及SCSI子系统

    执行软中断处理程序的时候,能够相应中断,但自己不能睡眠

    软中断虽然可以禁止本处理器上的同类软中断,但是对不同处理器的同类软中断是没有限制的(这就意味着潜在的数据并发访问)。因此我们需要考虑同步(加锁、或者使用每处理器数据)

    对于软中断,只有执行频率很高,连续性要求很高的情况下,才考虑使用


二,安装中断处理例程
       内核维护了一个中断信号线的注册表,类似于 I/O 端口的注册表。模块在使用中断前要先请求一个中断通道(或者 IRQ中断请求),并在使用后释放它。所用的函数声明在 <linux/interrupt.h> (在此文件中并未真正包含,是通过它include的文件间接包含的,函数在/kernel/irq/Manage.h中),中断注册和释放的函数接口如下:
        int request_irq(unsigned int irq ,               //请求的中断号
                                irqreturn_t (*handler)(int, void *, struct pt_regs *),//安装的处理函数指针
                                unsigned long flags,        // 一个与中断管理相关的位掩码选项
                                const char *dev_name,   //传递给 request_irq 的字符串,用来在 /proc/interrupts 来显示中断的拥有者
                                 void *dev_id);                //用于共享中断信号线的指针。它是唯一的标识,在中断线空闲时可以使用它,驱动程序也可以用它来指向自己的私有数据区(来标识哪个设备产生中断)。若中断没有被共享,dev_id 可以设置为 NULL,但推荐用它指向设备的数据结构。

         void free_irq(unsigned int irq, void *dev_id);

 

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


flags 中可以设置的位如下:
         SA_INTERRUPT :快速中断标志。快速中断处理例程运行在当前处理器禁止中断的状态下。
         SA_SHIRQ : 在设备间共享中断标志。
          SA_SAMPLE_RANDOM :该位表示产生的中断能对 /dev/random 和 /dev/urandom 使用的熵池(entropy pool)有贡献。 读取这些设备会返回真正的随机数,从而有助于应用程序软件选择用于加密的安全密钥。 若设备以真正随机的周期产生中断,就应当设置这个标志。若设备中断是可预测的,这个标志不值得设置。可能被攻击者影响的设备不应当设置这个标志。更多信息看 drivers/char/random.c 的注释。

        中断处理例程可在驱动初始化时或在设备第一次打开时安装。推荐在设备第一次打开、硬件被告知产生中断前时申请中断,因为可以共享有限的中断资源。这样调用 free_irq 的位置是设备最后一次被关闭、硬件被告知不用再中断处理器之后。但这种方式的缺点是必须为每个设备维护一个打开计数。
 以下是中断申请的示例(并口):


i386 和 x86_64 体系定义了一个函数来查询一个中断线是否可用:
int can_request_irq(unsigned int irq, unsigned long flags); /*当能够成功分配给定中断,则返回非零值。但注意,在 can_request_irq 和 request_irq 的调用之间给定中断可能被占用*/


三,快速和慢速处理例程
        快速中断是那些能够很快处理的中断,而处理慢速中断会花费更长的时间。在处理慢速中断时处理器重新使能中断,避免快速中断被延时过长。在现代内核中,快速和慢速中断的区别已经消失,剩下的只有一个:快速中断(使用 SA_INTERRUPT )执行时禁止所有在当前处理器上的其他中断。注意:其他的处理器仍然能够处理中断。
 除非你充足的理由在禁止其他中断情况下来运行中断处理例程,否则不应当使用SA_INTERRUPT.
         x86中断处理内幕
         这个描述是从 2.6 内核 arch/i386/kernel/irq.c, arch/i386/kernel/ apic.c, arch/i386/kernel/entry.S, arch/i386/kernel/i8259.c, 和 include/asm-i386/hw_irq.h 中得出,尽管基本概念相同,硬件细节与其他平台上不同。
底层中断处理代码在汇编语言文件 entry.S。在所有情况下,这个代码将中断号压栈并且跳转到一个公共段,公共段会调用 do_IRQ(在 irq.c 中定义)。do_IRQ 做的第一件事是应答中断以便中断控制器能够继续其他事情。它接着获取给定 IRQ 号的一个自旋锁,阻止其他 CPU 处理这个 IRQ,然后清除几个状态位(包括IRQ_WAITING )然后查找这个 IRQ 的处理例程。若没有找到,什么也不做;释放自旋锁,处理任何待处理的软件中断,最后 do_IRQ 返回。从中断中返回的最后一件事可能是一次处理器的重新调度。
           IRQ的探测是通过为每个缺乏处理例程的IRQ设置 IRQ_WAITING 状态位来完成。当中断发生, 因为没有注册处理例程,do_IRQ 清除这个位并且接着返回。 当probe_irq_off被一个函数调用,只需搜索没有设置 IRQ_WAITING 的 IRQ。


四,/proc 接口
       当硬件中断到达处理器时, 内核提供的一个内部计数器会递增,产生的中断报告显示在文件 /proc/interrupts中。这一方法可以用来检查设备是否按预期地工作。此文件只显示当前已安装处理例程的中断的计数。若以前request_irq的一个中断,现在已经free_irq了,那么就不会显示在这个文件中,但是它可以显示终端共享的情况。

      cat  /proc/interrupts:
      cat   /proc/stat:

        /proc/stat记录了几个关于系统活动的底层统计信息, 包括(但不仅限于)自系统启动以来收到的中断数。

             stat 的每一行以一个字符串开始, 是该行的关键词:

              intr 标志是中断计数。第一个数是所有中断的总数, 而其他每一个代表一个单独的中断线的计数, 从中断 0 开始(包括当前没有安装处理例程的中断),无法显示终端共享的情况。

        以上两个文件的一个不同是:/proc/interrupts几乎不依赖体系,而/proc/stat的字段数依赖内核下的硬件中断,其定义在<asm/irq.h>中。ARM的定义为:
#define NR_IRQS    128

五,自动检测 IRQ 号
        驱动初始化时最迫切的问题之一是决定设备要使用的IRQ 线,驱动需要信息来正确安装处理例程。自动检测中断号对驱动的可用性来说是一个基本需求。有时自动探测依赖一些设备具有的默认特性,以下是典型的并口中断探测程序:

       有的驱动允许用户在加载时覆盖默认值:
             insmod xxxxx.ko irq=x
       当目标设备有能力告知驱动它要使用的中断号时,自动探测中断号只是意味着探测设备,无需做额外的工作探测中断。但不是每个设备都对程序员友好,对于他们还是需要一些探测工作。这个工作技术上非常简单: 驱动告知设备产生中断并且观察发生了什么。如果一切顺利,则只有一个中断信号线被激活。尽管探测在理论上简单,但实现可能不简单。

       有 2 种方法来进行探测中断: 调用内核定义的辅助函数和DIY探测。
               1)调用内核定义的辅助函数
                     Linux 内核提供了一个底层设施来探测中断号,且只能在非共享中断模式下工作,它包括 2 个函数, 在<linux/interrupt.h> 中声明( 也描述了探测机制 ):
                unsigned long probe_irq_on(void); //这个函数返回一个未分配中断的位掩码。驱动必须保留返回的位掩码, 并在后面传递给 probe_irq_off。在调用probe_irq_on之后, 驱动应当安排它的设备产生至少一次中断

                int probe_irq_off(unsigned long); //在请求设备产生一个中断后, 驱动调用这个函数, 并将 probe_irq_on 返回的位掩码作为参数传递给probe_irq_off。probe_irq_off 返回在"probe_on"之后发生的中断号。如果没有中断发生, 返回 0 ;如果产生了多次中断,probe_irq_off 返回一个负值

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


最好只在模块初始化时探测中断线一次。
大部分体系定义了这两个函数( 即便是空的 )来简化设备驱动的移植。
          2)DIY探测
               DIY探测与前面原理相同: 使能所有未使用的中断, 接着等待并观察发生什么。我们对设备的了解:通常一个设备能够使用3或4个IRQ 号中的一个来进行配置,只探测这些 IRQ 号使我们能不必测试所有可能的中断就探测到正确的IRQ 号。
               下面的LDD3中的代码通过测试所有"可能的"中断并且察看发生的事情来探测中断。 trials 数组列出要尝试的中断, 以 0 作为结尾标志; tried 数组用来跟踪哪个中断号已经被这个驱动注册。


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

六,实现中断处理例程
       中断处理例程唯一的特别之处在中断时运行,它能做的事情受到了一些限制. 这些限制与我们在内核定时器上看到的相同:
              (1)中断处理例程不能与用户空间传递数据, 因为它不在进程上下文执行;
              (2)中断处理例程也不能做任何可能休眠的事情, 例如调用 wait_event, 使用除 GFP_ATOMIC 之外任何东西来分配内存, 或者锁住一个信号量;
               (3)处理者不能调用schedule()。
       

       中断处理例程的作用:将关于中断接收的信息反馈给设备并根据被服务的中断的含义读、写数据。中断处理例程第一步常常包括清除设备的一个中断标志位,大部分硬件设备在清除"中断挂起"位前不会再产生中断。这也要根据硬件的工作原理决定, 这一步也可能需要在最后做而不是开始; 这里没有通用的规则。一些设备不需要这步, 因为它们没有一个"中断挂起"位; 这样的设备是少数。
       一个中断处理的典型任务是:如果中断通知它所等待的事件已经发生(例如新数据到达),就会唤醒休眠在设备上的进程。

不管是快速或慢速处理例程,程序员应编写执行时间尽可能短的处理例程。 如果需要进行长时间计算, 最好的方法是使用 tasklet 或者 workqueue 在一个更安全的时间来调度计算任务。


七,处理例程的参数及返回值
       传递给一个中断处理例程的参数有: int irq、void *dev_id和 struct pt_regs *regs。
      int    irq (中断号):若要打印 log 消息时,是很有用。
      void    *dev_id:一种用户数据类型(驱动程序可用的私有数据),传递给 request_irq的 void* 参数,会在中断发生时作为参数传给处理例程。我们通常传递一个指向设备数据结构的指针到 dev_id 中,这样一个管理若干相同设备的驱动在中断处理例程中不需要任何额外的代码,就可以找出哪个设备产生了当前的中断事件。
      struct pt_regs *regs很少用到。
      中断处理例程的典型使用如下:
static irqreturn_t sample_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
        struct sample_dev *dev = dev_id;
        /* now `dev' points to the right hardware item */
        /* .... */
}
和这个处理例程关联的打开代码如下:
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 /* flags */, "sample", dev /* dev_id */);
        /*....*/
        return 0;
}

           中断处理例程应当返回一个值指示是否真正处理了一个中断。如果处理例程发现设备确实需要处理, 应当返回 IRQ_HANDLED; 否则返回值 IRQ_NONE。以下宏可产生返回值:
           IRQ_RETVAL(handled) /*若要处理中断,handled应是非零*/
           有位网友在处理返回值是按惯例return 0;,导致了oops。吸取经验教训,我们应特别注意这种返回值,以下是有关中断处理例程的返回值的内核定义

#include <linux/irqreturn.h>    看了就知道导致oops的原因了,以后应多多注意:
typedef int irqreturn_t;
#define IRQ_NONE    (0)
#define IRQ_HANDLED    (1)
#define IRQ_RETVAL(x)    ((x) != 0)



八,启用和禁止中断
        有时设备驱动必须在一段时间(希望较短)内阻塞中断发生。并必须在持有一个自旋锁时阻塞中断,以避免死锁系统。注意:应尽量少禁止中断,即使是在设备驱动中,且这个技术不应当用于驱动中的互斥机制。
        1)禁止单个中断
             有时(但是很少!)一个驱动需要禁止一个特定中断。但不推荐这样做,特别是不能禁止共享中断(在现代系统中, 共享的中断是很常见的)。内核提供了 3 个函数,是内核 API 的一部分,声明在 <asm/irq.h>:
void disable_irq(int irq);/*禁止给定的中断, 并等待当前的中断处理例程结束。如果调用 disable_irq 的线程持有任何中断处理例程需要的资源(例如自旋锁), 系统可能死锁*/
void disable_irq_nosync(int irq);/*禁止给定的中断后立刻返回(可能引入竞态)*/
void enable_irq(int irq);
 
            调用任一函数可能更新在可编程控制器(PIC)中的特定 irq 的掩码, 从而禁止或使能所有处理器特定的 IRQ。这些函数的调用能够嵌套,即如果 disable_irq 被连续调用 2 次,则需要 2 个 enable_irq 重新使能 IRQ 。可以在中断处理例程中调用这些函数,但在处理某个IRQ时再打开它是不好的做法。
 
         2)禁止所有中断
              在 2.6 内核, 可使用下面 2 个函数中的任一个(定义在 <asm/system.h>)关闭当前处理器上所有中断:
void local_irq_save(unsigned long flags);/*在保存当前中断状态到 flags 之后禁止中断*/
void local_irq_disable(void);/* 关闭中断而不保存状态*/
/*如果调用链中有多个函数可能需要禁止中断, 应使用 local_irq_save*/
/*打开中断使用:*/
void local_irq_restore(unsigned long flags);
void local_irq_enable(void);
/*在 2.6 内核, 没有方法全局禁用整个系统上的所有中断*/

九,顶半部和底半部
       中断处理需要很快完成并且不使中断阻塞太长,所以中断处理的一个主要问题是如何在处理例程中完成耗时的任务。Linux (连同许多其他系统)通过将中断处理分为两部分来解决这个问题:
        “顶半部”:是实际响应中断的例程(request_irq 注册的那个例程)。
        “底半部”:是被顶半部调度,并在稍后更安全的时间内执行的函数。
         他们最大的不同在底半部处理例程执行时,所有中断都是打开的(这就是所谓的在更安全的时间内运行)。典型的情况是:顶半部保存设备数据到一个设备特定的缓存并调度它的底半部,最后退出: 这个操作非常快。底半部接着进行任何其他需要的工作。这种方式的好处是在底半部工作期间,顶半部仍然可以继续为新中断服务。

    Linux 内核有 2 个不同的机制可用来实现底半部处理:
           1) tasklet (首选机制),它非常快, 但是所有的 tasklet 代码必须是原子的;
           2)工作队列, 它可能有更高的延时,但允许休眠。
tasklet和工作队列在《时间、延迟及延缓操作》已经介绍过,具体的实现代码请看实验源码!

十,中断共享
        Linux 内核支持在所有总线上中断共享。
        安装共享的处理例程
        通过 request_irq 来安装共享中断与非共享中断有 2 点不同:
          (1)当request_irq 时,flags 中必须指定SA_SHIRQ 位;
          (2)dev_id 必须唯一。任何指向模块地址空间的指针都行,但 dev_id 绝不能设置为 NULL。
         内核为每个中断维护一个中断共享处理例程列表,dev_id 就是区别不同处理例程的签名。释放处理例程通过执行free_irq实现。  dev_id 用来从这个中断的共享处理例程列表中选择正确的处理例程来释放,这就是为什么 dev_id 必须是唯一的.
请求一个共享的中断时,如果满足下列条件之一,则request_irq 成功:
      (1)中断线空闲;
      (2)所有已经注册该中断信号线的处理例程也标识了IRQ是共享。

   

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

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

posted on 2012-04-20 12:32  小田的专栏  阅读(2674)  评论(0编辑  收藏  举报

导航