一、中断处理程序概述
- 在响应一个特定中断的时候,内核会执行一个函数,该函数叫做
中断处理程序(interrupt handler) 或中断服务例程(interrupt service routine, ISR)
- 产生中断的
每个设备都有一个相应的中断处理程序(
**本质上中断处理程序通常不是和特定设备关联,**而是和特定中断关联,也就是说,如果一个设备可以产生多种不同的中断,那么该设备就可以对应多个中断处理程序,相应的,该设备的驱动程序也就需要准备多个这样的函数)
**。**例如,由一个函数专门处理来自系统时钟的中断,而另外一个函数专门处理由键盘产生的中断。一个设备的中断处理程序是它设备驱动程序(driver) 的一部分——设备驱动程序是用于对设备进行管理的内核代码
- 在Linux中,
**中断处理程序就是普普通通的C函数。**只不过这些函数必须按照特定的类型声明,以便内核能够以标准的方式传递处理程序的信息,在其他方面,它们与一般的函数别无二致。
**中断处理程序与其他内核函数的真正区别在于,**中断处理程序是被内核调用来响应中断的, 而
它们运行于我们称之为中断上下文的特殊上下文中(关于中断上下文,我们将在后面讨论)。 需要指出的是,中断上下文偶尔也称作原子上下文,因为正如我们看到的,该上下文中的执行代码不可阻塞。不过在文章中我们使用中断上下文这个称谓
- **中断可能随时发生,因此中断处理程序也就随时可能执行。**所以必须保证中断处理程序能够快速执行,这样才能保证尽可能快地恢复中断代码的执行。因此,尽管对硬件而言,操作系统能迅速对其中断进行服务非常重要;当然对系统的其他部分而言,让中断处理程序在尽可能短的时间内完成运行也同样重要
- 最起码的,中断处理程序要负责通知硬件设备中断已被接收:嗨 ,硬件,我听到你了,现在 回去工作吧!但是中断处理程序往往还要完成大量其他的工具。例如,我们可以考虑一下网络设备的中断处理程序面临的挑战。该处理程序除了要对硬件应答,还要把来自硬件的网络数据包拷贝到内存,对其进行处理后再交给合适的协议栈或应用程序。显而易见,这种工作量不会太小, 尤其对于如今的千兆比特和万兆比特以太网卡而言
二、上半部与下半部的对比
- 又想中断处理程序运行得快,又想中断处理程序完成的工作量多,这两个目的显然有所抵触。
鉴于两个目的之间存在此消彼长的矛盾关系,所以我们一般把中断处理切为两个部分或两半:
- 中断处理程序是上半部(top half)——接收到一个中断,它就立即开始执行,但只做有严格时限的工作,例如对接收的中断进行应答或复位硬件,这些工作都是在所有中断被禁止的情况下完成的
- **能够被允许稍后完成的工作会推迟到下半部(bottom half) 去 。**此后, 在合适的时机,下半部会被开中断执行。Linux提供了实现下半部的各种机制,在后面会讨论这些机制
例如
- 让我们考察一下上半部和下半部分割的例子,还是以我们的老朋友——网卡作为实例。当网卡接收来自网络的数据包时,需要通知内核数据包到了。网卡需要立即完成这件事,从而优化网络的吞吐量和传输周期,以避免超时。因此,网卡立即发出中断:嗨,内核,我这里有最新数据包了。内核通过执行网卡已注册的中断处理程序来做出应答
- 中断开始执行,通知硬件,拷贝最新的网络数据包到内存,然后读取网卡更多的数据包。这 些都是重要、紧迫而又与硬件相关的工作。内核通常需要快速的拷贝网络数据包到系统内存,因 为网卡上接收网络数据包的缓存大小固定,而且相比系统内存也要小得多。所以上述拷贝动作一 旦被延迟,必然造成缓存溢出——进入的网络包占满了网卡的缓存,后续的入包只能被丢弃。当网络数据包被拷贝到系统内存后,中断的任务算是完成了,这时它将控制权交还给系统被中断前原先运行的程序。处理和操作数据包的其他工作在随后的下半部中进行。本文,我们考察上半部 ;在后面的文章我们会介绍下半部
三、注册中断处理程序、中断线(request_irq)
- 中断处理程序是管理硬件的驱动程序的组成部分。每一设备都有相关的驱动程序,如果设备使用中断(大部分设备如此),那么相应的驱动程序就注册一个中断处理程序
- 驱动程序可以
通过request_irq()函数注册一个中断处理程序(它被声明在文件<include/linux/interrupt.h>中),
**并且激活给定的中断线,**以处理中断:
参数1(irq)
- **表示要分配的中断号。**对某些设备,如传统PC设备上的系统时钟或键盘, 这个值通常是预先确定的。而对于大多数其他设备来说,这个值要么是可以通过探测获取,要么可以通过编程动态确定
参数2(handler)
- 是一个指针,
**指向处理这个中断的实际中断处理程序。**只要操作系统一接收到中断,该函数就被调用
- 该参数的原型如下:接受两个参数,并有一个类型为irqreturn_t的返回值(我们会在后面介绍这个函数)
参数3(flags)
标志值。可以设置为0,或者取下列的一个或多个标志的位掩码:
IRQF_DISABLED——该标志被设置后,**意味着内核在处理中断处理程序本身期间,要禁止所有的其他中断。**如果不设置,中断处理程序可以与除本身外的其他任何中断同时运行。多数中断处理程序是不会去设置该位的,因为禁止所有中断是一种野蛮行为。这种用法留给希望快速执行的轻量级中断。这一标志是SAJNTERRUPT标志的当前表现形式,在过去的中断中用以区分“快速”和 “慢速”中断
- IRQF_SAMPLE_RANDOM——此标志表明这个设备产生的中断对内核熵池(entropy pool)有贡献。内核熵池负责提供从各种随机事件导出的真正的随机数。如果指定了该标 , 那么来自该设备的中断间隔时间就会作为墒填充到熵池。如果你的设备以预知的速率产生中断(如系统定时器),或者可能受外部攻击者(如联网设备)的影响,那么就不要设置这个标志。相反,有其他很多硬件产生中断的速率是不可预知的,所以都能成为一种较好的熵源
- IRQF_TIMER——该标志是特别为系统定时器的中断处理而准备的
- IRQF_SHARED——此标志表明可以在多个中断处理程序之间共享中断线
。在同一个给定线上注册的每个处理程序必须指定这个标志;否则,在每条线上只能有一个处理程序。有 关共享中断处理程序的更多信息将在下面的内容中提供
参数4(name)
- 第四个参数name是
与中断相关的设备的ASCII文本表示
- 例如,PC机上键盘中断对应的这个值为 “keyboard”。这些名字会被/proc/irq和/proc/interrupts文件使用,以便与用户通信,稍后我们将对此进行简短讨论
参数5(dev)
- 第五个参数dev
用于共享中断线
- 当一个中断处理程序需要释放时(稍后讨论),dev将提供唯一的标志信息(cookie)
,以便从共享中断线的诸多中断处理程序中删除指定的那一个:
- 如果没有这个参数,那么内核不可能知道在给定的中断线上到底要删除哪一个处理程序
如果无须共享中断线,那么将该参数赋为空值(NULL)就可以了
但是,如果中断线是被共享的,那么就必须传递唯一的信息(除非设备又旧又破且位于ISA总线上,那么就必须支持共享中断)
另外,**内核每次调用中断处理程序时,都会把这个指针传递给它。**实践中往往会通过它传递驱动程序的设备结构:这个指针是唯一的,而且有可能在中断处理程序内被用到
返回值
成功执行会返回0
如果返回非0值,就表示有错误发生:
在这种情况下,指定的中断处理程序不会被注册
**最常见的错误是-EBUSY,**它表示给定的中断线已经在使用(或者当前用户或者你没有指定IRQF_SHARED)
request_irq函数睡眠(重点)
- 注意,
request_irq()函数可能会睡眠,因此,
不能在中断上下文或其他不允许阻塞的代码中调用该函数
- 天真地在睡眠不安全的上下文中调用request_irq()函数,是一种常见错误。造成这种错误的部分原因是为什么request_irq()函数会引起堵塞——这确实让人费解,见下面的睡眠情景
- **发生睡眠的情景:**在注册的过程中, 内核需要在/proc/irq文件中创建一个与中断对应的项。函数proc_ mkdir()就是用来创建这个新的procfs项的。proc_mkdir()通过调用函数proc_create()对这个新的profs项进行设置,而proc_ create()会调用函数kmalloc()来请求分配内存。我们在后面会介绍函数kmalloc(),该函数是可以睡眠的。看清楚了,你的程序就是跑到那里小憩去了
一个中断例子
- 在一个驱动程序中请求一个中断线,并在通过request_irq()安装中断处理程序:
在这个例子中:
irqn请求的中断线
- my_interrupt是中断处理程序
- 我们通过IRQF_SHARED标志设置中断线可以共享的
- 设备命名为“my_device”
- 最后是传递my_dev变量给dev形参
- 如果请求失败, 那么这段代码将打印出一个错误并返回。如果调用返回0,则说明处理程序已经成功安装。
此后,处理程序就会在响应该中断时被调用
* 有一点很重要,
初始化硬件和注册中断处理程序的顺 序必须正确,以防止中断处理程序在设备初始化完成之前就开始执行
四、释放中断处理程序(free_irq)
- 卸载驱动程序时,需要注销相应的中断处理程序,并释放中断线。上述动作需要调用:
- 如果指定的中断线不是共享的,那么,该函数删除处理程序的同时将禁用这条中断线
- **如果中断线是共享的,**则仅删除dev所对应的处理程序,而这条中断线本身只有在删除了最后一个处理程序时才会被禁用
- 由此可以看出为什么唯一的dev如此重要。对于共享的中断线,需要一个唯一的信息来区分其上面的多个处理程序,并让free_irq()仅仅删除指定的处理程序。不管在哪种情况下(共享或不共享),如果dev非空,它都必须与需要删除的处理程序相匹配。必须从进程上下文中调用free_irq()
五、编写中断处理程序
- 我们将对request_irq的第2个参数进行解析,这个函数时中断处理程序在发生中断时所调用的函数
- 以下是一个中断处理程序声明:
- 注意,它的类型与request_irq()函数数中handler所要求的参数类型相匹配
- 中断处理程序通常会标记为static,因为它从来不会被别的文件中的代码直接调用
参数
- **第一个参数irq就是这个处理程序要响应的中断的中断号。**如今,这个参数已经没有太大用处了,可能只是在打印日志信息时会用到。而在2.0版以前的Linux内核中,由于没有dev这个参数,必须通过irq才能区分使用相同驱动程序,因而也使用相同的中断处理程序的多个设备。例如,具有多个相同类型硬盘驱动控制器的计算机
- **第二个参数dev是一个通用指针,它与在中断处理程序注册时传递给request_irq()的参数dev必须一致。**如果该值有唯一确定性(这样做是为了能支持共享),那么它就相当于一个cookie, 可以用来区分共享同一中断处理程序的多个设备。另外dev也可能指向中断处理程序使用的一个数据结构。因为对每个设备而言,设备结构都是唯一的,而且可能在中断处理程序中也 用得到,因此,它也通常被看做dev
返回值
返回值是一个特殊类型:irqreturn_t
中断处理程序可能返回两个特殊的值:IRQ_NONE和IRQ_HANDLED
当中断处理程序检测到一个中断,但该中断对应的设备并不是在注册处理函数期间指定的产生源时,返回IRQ_NONE
- 当中断处理程序被正确调用, 且确实是它所对应的设备产生了中断时,返 回IRQ_HANDLED
另外,也可以使用宏IRQ_ RETVAL(val):
如果val为非0值,那么该宏返回IRQ_HANDLED
- 否则,返回IRQ_NONE
利用这些特殊的值,
内核可以知道设备发出的是否是一种虚假的(为请求)中断。如果给定中断线上所有中断处理程序返回的都是IRQ_NONE,那么,内核就可以检测到出了问题
- 注意,
irqreturn_t这个返回类型实际上就是一个int型。之所以使用这些特殊值是为了与早期的内核保持兼容——2.6版之前的内核并不支持这种特性,中断处理程序只需返回void就行了。如果要在2.4或更早的内核上使用这样的驱动程序,只需简单地将typedef irqreturn_t改为void,屏蔽掉此 特性,并给no-ops定义不同的返回值,其他用不着做什么大的修改
- 中断处理程序扮演什么样的角色要取决于产生中断的设备和该设备为什么要发送中断。即 使其他什么工作也不做,绝大部分的中断处理程序至少需要知道产生中断的设备,告诉它已经收到中断了。对于复杂一些的设备,可能还需要在中断处理程序中发送和接收数据,以及执行一些扩充的工作。如前所述,应尽可能将扩充的工作推给下半部处理程序,这些内容将在后面“下半部和推后执行”的文章中介绍
重入和中断处理程序
- Linux中的
中断处理程序是无须重入的。
当一个给定的中断处理程序正在执行时,相应的中断线在所有处理器上都会被屏蔽掉,以防止在同一中断线上接收另一个新的中断
- 通常情 况下,所有其他的中断都是打开的,所以这些不同中断线上的其他中断都能被处理,但当前中断线总是被禁止的。由此可以看出,同一个中断处理程序绝对不会被同时调用以处理嵌套的中断。这极大地简化了中断处理程序的编写
共享的中断处理程序
共享的处理程序与非共享的处理程序在注册和运行方式上比较相似,但差异主要有以下三处 :
1.request_irq()的参数
flags必须设置为IRQF_SHARED标志
* 2.对于每个注册的中断处理程序来说,
**dev参数必须唯一。**指向任一设备结构的指针就可以满足这一要求;通常会选择设备结构,因为它是唯一的,而且中断处理程序可能会用到 它。不能给共享的处理程序传递NULL值
* 3.
**中断处理程序必须能够区分它的设备是否真的产生了中断。**这既需要硬件的支持,也需要 处理程序中有相关的处理逻辑。如果硬件不支持这一功能,那中断处理程序肯定会束手无 策,它根本没法知道到底是与它对应的设备发出了这个中断,还是共享这条中断线的其他 设备发出了这个中断
- 所有共享中断线的驱动程序都必须满足以上要求。
**只要有任何一个设备没有按规则进行共享,那么中断线就无法共享了。**指定IRQF_SHARED标志以调用request_irq()时,只有在以下两种情况下才可能成功:中断线当前未被注册,或者在该线上的所有已注册处理程序都指定了IRQF_SHARED。注意,在这一点上2.6版与以前的内核是不同的,共享的处理程序可以混用 IRQF_DISABLED
中断处理程序的触发
- **内核接收一个中断后,它将依次调用在该中断线上注册的每一个处理程序。**因此,一个处理程序必须知道它是否应该为这个中断负责。如果与它相关的设备并没有产生中断,那么处理程序应该立即退出。这需要硬件设备提供状态寄存器(或类似机制),以便中断处理程序进 检査。 毫无疑问,大多数硬件都提供这种功能
六、中断处理程序实例
-
我们以一个real-time clock(RTC)驱动程序为例,可以在drivers/char/rtc.c中找到
-
**该程序的功能:**很多机器(包括PC)都可以找到RTC。它是一个从系统定时器中独立出来的设备,用于设置系统时钟,提供报警器(alarm)或周期性的定时器
-
**系统时钟的设置:**值需要向某个特定的寄存器或I/O地址写入想要的时间就可以了
- **报警器或周期性定时器通常就得靠中断来实现。**这种中断与生活中的闹钟差不多:中断发出时,报警器或定时器就会启动
驱动程序的初始化
- RTC驱动程序装载时,trc_init()函数会被调用,对这个驱动程序进行初始化。它的职责之一就是注册中断断处理程序:
从中我们看到:
中断号由rtc_irq指定。这个变量用于为给定体系结构指定RTC中断。例如,在PC上,RTC位于IRQ 8
第二个参数是我们的中断处理程序rtc_interrupt
它将与其他中断处理程序共享中断线,因为它设置了IRQF_SHARED标志
由第四个参数可以看出,驱动程序的名称为“rtc”
因为这个设备允许共享中断线,所以它给dev型参传递了一个面向每个设备的实参值
中断程序本身(rtc_interrupt)
只要计算机一接收到RTC中断,就会调用这个函数
函数解析:
首先要注意的是使用了自旋锁——第一次调用是为了保证rtc_irq_data不被SMP机器上的其他处理器同时访问,第二次调用避免rtc_callback出现相同的情况。锁机制在后面文章讨论
- rtc_irq_data变量是无符号长整数,**存放有关RTC的信息,**每次中断时都会更新以反映中断的状态
- 接下来,如果设置了RTC周期性定时器,**就要通过函数mod_timer()对其更新。**定时器也在后面文章讨论
- 代码的最后一部分——处于注释“现在执行其余的操作”下,**会执行一个可能被预先设置好的回调函数。**RTC驱动程序允许注册一个回调函数,并在每个RTC中断到来时执行
- 最后,**这个函数会返回IRQ_HANDLED,**表明已经正确地完成了对此设备的操作。因为这个中断处理程序不支持共享,而且RTC也没有什么用来测试虚假中断的机制,所以该处理程序总是返回IRQ_HANDLED