中断和中断处理程序

1. 中断

        Linux内核要对连接到计算机上的所有硬件设备进行管理,首先要能和它们互相通信。从所周知,处理器的速度跟外围硬件设备的速度往往不在一个数量级上。所以,需要一种机制,如果轮询(polling)是一种解决办法,可以让内核定期对设备的状态进行查询,然后做出相应的处理,但这让内核做了不少无用功。

    更好的办法是由我们来提供一种机制,让硬件在需要的时候再向内核发出信号。这就是中断机制。中断本质上是一种特殊的电信号,由硬件设备生成,并直接送入中断控制器的输入引脚上,再由中断控制器向处理器发送相应的信号,处理器一经检测到此信号,便中断自己当前工作转而处理中断,最后由OS来负责处理新到来的数据。中断是异步的。

 

什么是中断?简单地说就是CPU在忙着作自己的事情,这时候硬件(比如说键盘按了一下)触发了一个电信号,这个信号通过中断线到达中断控制器i8259A,i8259A接受到这个信号后,向CPU发送INT信号申请CPU来执行刚才的硬件操作,并且将中断类型号也发给CPU,此时CPU保存当前正在做的事情(REST指令把程序计数器PC中的下一条待执行的指令的内存地址保存到)的情景现场,然后去处理这个申请,根据中断类型号找到它的中断向量即中断程序在内存中的地址),然后去执行这段程序(这段程序已经写好,在内存中),执行完后再向i8259A发送一个INTA信号表示其已经处理完刚才的申请。此时CPU就可以继续做它刚才被打断做的事情了,将刚才保存的情景现场恢复出来,CPU继续执行接下来下面的程序。

    不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标识。这些中断值通常被称为中断请求(IRQ)线。比如,IRQ0是时钟中断,而IRQ1是键盘中断。并不是所有的中断号都这样严格定义,像PCI总线上的设备,中断就是动态分配的。

 

1.1. 异常与中断

    异常与中断不同,它在产生时必须考虑与处理器时钟同步。实际上,异常也称为同步中断。比如,在处理器执行到由于编程失误而导致的错误指令的时候,或者在执行期间出现特殊情况(缺页),必须靠内核来处理的,处理器就产生一个异常。

中断的的工作方式类似,其差异只在于中断是由硬件而不是软件引起的。

 

2. 中断处理程序

    在响应一个特定中断的时候,内核会执行一个函数,该函数叫中断处理程序(interrupt handler)或中断服务例程(interrupt service routine,ISR)。产生中断的每个设备都有一个相应的中断处理程序。一个设备的中断处理程序是它设备驱动程序的一部分。中断处理程序与其他内核的真正区别在于:中断处理程序是被内核调用来响应中断的,而它们运行于我们称之为中断上下文的特殊上下文中。

 

2.1. 上半部与下半部的对比

    又想程序运行得快,又想程序完成的工作量太多,这两个目的相互矛盾。鉴于两个目的之间存在不可调和的矛盾,所以需要把中断处理程序分成两半或两个部分。中断处理程序是上半部(top half):接收到一个中断,他就立即开始执行,但只做严格时限的工作,例如对接收的中断进行应答或复位硬件,这些工作都是在所有中断被禁止的情况下完成的。能够被允许稍后完成的工作会推迟到下半部(bottom half)去。此后,在合适的时机,下半部被开中断执行。

 

3. 注册中断处理程序

   驱动程序可以通过下面的函数注册并激活一个中断处理程序,以便处理中断:

      int request_irq(unsigned int irq,

irqretrun_t (*handler)(int,void *, struct pt_regs *),

unsigned long irqflags,

const char *devname,

void *dev_id);

第一个参数>irq表示要分配的中断号。对于大多数其他设备来说,这个值要么是可以通过探测获取,要么可以通过编程动态确定。

第二个参数>hanlder是一个指针,指向处理这个中断的实际中断处理程序。hanhler函数的原型接收三个参数。

第三个参数>irqflags可以是0,也可以是多个标志的掩码。如果是SA_INTERRUPT,表面给定的中断处理程序是一个快速中断处理程序(fast interrupt hanlder)。使用了该标志,快速中断处理程序在禁止所有中断的情况下的本地处理器上运行。除了时钟中断,绝大数中断都不使用该标志。如果是SA_SAMPLE_RANDOM,表明这个设备产生的中断对内核熵池(entropy pool)有贡献。如果是SA_SHARE标志,表明可以在多个中断处理程序之间共享中断线。在同一个给定线上注册的每个处理程序必须指定这个标志。

第四个参数>devname是与中断相关设备的ASCII文本表示法。这些名字会被/proc//irq和/proc/inerrupt文件使用,以便于用户通信。

第五个参数>dev_id主要用户共享中断线。当一个中断处理程序需要释放时,dev_id将提供唯一的标志信息,以便从共享中断线的诸多中断处理程序中删除指定的那一个。如果无需共享中断线,那么将该参数赋为空值(NULL)就可以了。

该函数执行成功会返回0。如果返回非0值,就表示有错误发生。

   注意:request_irq函数可能会睡眠,因此,不能在中断上下文或其他不允许阻塞的代码中使用该函数。在注册的过程中,内核需要在/proc/irq文件中创建一个与中断对应的项。函数proc_mkdir就是用来创建这个新的procfs项的。函数proc_mkdir通过调用函数proc_mkdir通过调用proc_create对这个profs项进行设置,而proc_create会调用函数kmalloc函数请求分配内存。函数kmalloc是可以睡眠的。

 

3.1. 释放中断处理程序

    卸载驱动程序时,需要注销相应的中断处理程序,并释放中断线。可以调用void_free_irq(unsigned int irq, void * dev_id)来释放中断线。

如果指定的中断线不是共享的,那么该函数删除处理程序的同时将禁用这条中断线。如果中断线是共享的,则仅删除dev_di对应的处理程序,而这条中断线只有在删除了最后一个处理程序时才会被禁用。

 

4. 编写中断处理程序

    以下是一个典型的中断处理程序声明:

       static irqreturn_t intr_handler(int irq, void *dev_id, struct pt_regs *regs);

    第一个参数irq就是这个处理程序要响应的中断的中断线号。

    第二个参数dev_id是一个通用指针,它与在中断处理程序注册时传递request_irq的参数的dve_id必须一致。另外dev_id也可能指向中断处理程序使用的一个数据结构。因为,对于每个设备而言,设备结构是唯一的。

    第三个参数regs是一个指向结构的指针,该结构包含处理中断之前处理器的寄存器和状态。考虑到现有的中断处理程序很少使用该参数,因此可以忽略它。

    中断处理程序的返回值是一个特殊类型:irqreturn_t。中断处理程序可能会返回两个特殊的值:IRQ_NONE和IRQ_HANDLED。当中断处理程序检测到一个中断,但该中断对应的设备并不是在注册处理函数期间指定的产生源时,返回IRQ_NONE;当中断处理程序被正确调用,且确实是它所对应的设备产生了中断,返回IRQ_HANDLED。而实际上irqreturn_t就是一个int类型。

    中断处理程序通常会标记为static,因为它从来不会被别人的文件中的代码直接调用。

 

4.1. 重入和中断处理程序

        Linux中的中断处理程序是无需重入的。当一个给定的中断处理程序正在执行时,相应的中断线在所有处理器上都会被屏蔽掉,以防止在同一中断线上接收另一个新的中断。

 

4.2. 共享的中断处理程序

    共享的处理程序与非共享的处理程序在注册和运行方式上比较类似,但差异如下:

    1) request_irq的参数flags必须设置SA_SHARE标志

    2) 对每个注册的中断处理程序来说,dev_id参数必须唯一

    3) 中断处理程序必须能够区分它的设备是否真的产生了中断。这既需要硬件的支持,耶需要处理程序有相关的处理逻辑。

 

    指定SA_SHARE标志以调用request_irq时,只有在以下两种情况下才可能成功:

    1) 中断线当前未被注册

    2) 在该线上的所有已经注册处理程序都指定了SA_SHARE。

 

    内核接收一个中断后,它将依次调用在该中断线上注册的每一个处理程序。因此,一个处理程序应该必须知道它是否应该为这个负责。如果与它相关的设备并没有产生中断,那么处理器应该立即退出。

 

5. 中断上下文

    当执行一个中断处理程序或下半部时,内核处于中断上下文(interrupt context)中。中断上下文和进程没有关系,不可以睡眠。中断上下文具有严格的时间限制,因为它打断了其他代码。

    而进程上下文是一种内核所处的操作模式,此时内核代表进程执行,比如执行系统调用或运行内核线程。在进程上下文中,可以通过current宏关联当前进程,可以睡眠。

    中断处理程序打断了其他代码,正是因为这种异步执行的特性,所以所有的中断处理程序必须尽可能的迅速、简洁。尽量把工作从中断处理程序中分离出来,交给下半部。

    中断处理程序栈的设置是一个配置选项,决定中断处理程序是否共享中断进程的内核栈。内核栈的大小是两页。在2.6的内核中,增加一个选项,把栈的大小两页减到一页,这就减轻了内存的压力,因为系统中每个进程仅需要一页内核栈了。但是,为了应对栈大小的减少,中断处理程序拥有了自己的栈,每个处理器一个,大小为一页。这个栈称为中断栈。

 

6. 中断处理机制的实现

    设备产生中断,通过总线把电信号发送给中断控制器,处理器会立即停止它正在做的事,关闭中断系统,然后跳到内存中预定义的位置开始执行那里的代码。这个预定义的位置是由内核设置的,是中断处理程序的入口点。

    在内核栈,中断的旅程开始于预定义入口点,这类似于系统调用通过预定义的异常句柄进入内核。对于每条中断线,处理器都会跳到对应的一个唯一的位置。初始入口点只是在栈中保存这个号,并存放当前寄存器的值;然后,内核调用do_IRQ函数。

        unsigned int do_IRQ(struct pt_regs regs);

    该函数计算出中断号后,对所接收的中断进行应答,禁止这条线上的中断传递。在普通的PC机器上,这些操作由mask_and_ack_8259A来完成的。

    接着,该函数需要确保在这条中断线上有个有效的处理程序,而且这个程序已经启动,但是当前并没有执行。do_IRQ就调用handle_IRQ_event来运行为这条中断线安装的中断处理程序。

    最后,函数返回,回到do_IRQ。而do_IRQ做清理工作并返回到初始入口点,然后再从这个入口点跳到函数ret_from_intr函数。这个例程会检查重新调度是否正在挂起。如果重新调度正在挂起,而且内核正在返回用户空间(也就是中断了用户进程),那么schedule被调用。如果内核正在返回内核空间(也就是中断了内核本身),只有在preempt_count为0,schedule才会被调用。在schedule返回之后,或者没有挂起的工作,那么,原来的寄存器被恢复,内核恢复到曾经中断的点。

    在x86上,初始的汇编例程位于arch/i386/kernel/entry.S,C方法在arch/i386/kernel/irq.c中。

 

6.1. 文件/proc/interrupts

        procfs是一个虚拟文件系统,它只存于内核内存,一般安装与/proc目录下。在procfs中读写都要调用内核函数,这些函数模拟从真实文件中读或写。

 

7. 中断控制

        Linux内核提供了一组接口用于操作机器上的中断状态。可以在<asm/system.h>和<asm/irq.h>中找到。一般来说,控制中断系统的原因是需要提供同步。通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码。此外,禁止中断还可以禁止内核抢占。

 

7.1. 禁止和激活中断

    用于禁止和激活当前处理器上的本地中断:

         local_irq_disable();

         local_irq_enable();

         local_irq_save(unsigned long flags);

         local_irq_restore(unsigned long flags);

    前两个函数通常调用单个汇编指令来实现。实际上,在x86中它们分别使用cli指令和sti指令。如果在调用local_irq_disable例程之前已经禁止了中断,那么该例程往往带来潜在的危险;同样相应的local_irq_enable例程耶存在危险,因为他将无条件地激活中断,尽管这些中断可能在开始时就是关闭的。后两个函数可以保存现场,是系统更加安全。

    内核2.5版本不再使用全局的cli,相应地,所有中断同步现在必须结合使用本地中断控制器和自旋锁。也就是说,为了确保对共享数据的互斥访问,现在需要做更多的工作。取消全局cli的优点:一是强制驱动程序编写实现真正的加锁,具有特定的细粒度比全局锁快许多;二是这使得很多代码更具流线型,避免了代码的成簇布局。

前面的所有函数既可以在中断中调用,也可以在进程上下文中调用。

 

7.2. 禁止指定中断线

    在某些情况下,只禁止整个系统中一条特定的中断线就够了。

         void disable_irq(unsigned int irq);

         void disable_irq_nosync(unsigned int irq);

         void enable_irq(unsigned int irq);

         void synchronize_irq(unsigned int irq);

    前两个函数禁止中断控制器上指定的中断线。另外函数只有在当前正在执行的所有处理程序完成后,disable_irq才能返回。因此。调用者不仅确保不在指定中断线上传递新的中断,同时还有确保所有已经开始执行的处理程序已经全部退出。

    函数disable_irq_nosync不会等待当前中断处理程序执行完毕。

函数synchronize_irq等待一个特定的中断处理程序的退出。如果该处理程序正在执行,那么该函数必须退出后才能返回。

    对于这些函数的调用可以嵌套。其中有三个函数可以从中断或进程上下文中调用,而且不会睡眠。禁止多个中断处理程序共享的中断线是不合适的,禁止中断线也就禁止了这条线上所有设备的中断传递。因此,用于新设备的驱动程序应该倾向于不使用这些接口。

 

7.3. 中断系统的状态

    宏irqs_disable定义在<asm/system.h>中。如果本地处理器上的中断系统被禁止,则它返回非0,否则返回0。

    在<asm/hardirq.h>中定义的两个宏提供一个用来检测内核的当前上下文的接口:

        int_interrupt()

        int_irq()

    第一个宏in_interrup最有用:如果内核处于中断上下文中,返回非0。说明内核此刻正在执行中断处理程序,或者正在执行下半部处理程序。宏in_irq只有在内核确实正在执行中断处理程序时返回非0。

posted @ 2018-03-26 10:46  FrankYou  阅读(21814)  评论(0编辑  收藏  举报