1.概述
上文中我们通过一个简单的例子分析了一个中断程序的基本结构。可以看到,中断处理程序在处理中断时起到了关键作用,也是一个中断程序必不可少的部分。不过,现如今的中断处理流程都会分为两部分:上半部分(top half)和下半部分(bottom half)。为什么要将一个中断分为如此两部分?下面的几个经典原因可以很好的诠释这个问题。
1.中断可以随时的打断处理机对其他程序的执行,如果被打断的代码对系统很重要,那么此时中断处理程序的执行时间应该是越短越好。
2.通过上文我们知道,中断处理程序正在执行时,会屏蔽同条中断线上的中断请求;而更严重的是,如果设置了IRQF_DISABLED,那么该中断服务程序执行是会屏蔽所有其他的中断请求。那么此时应该让中断处理程序执行的越快越好。
上面的几个例子都要求中断服务程序的执行时间越短越好。一般的,中断处理程序会在上半部分执行。而事实上,几乎所有的情况,上半部分就只执行中断处理程序。因此,我们可以这样认为:一个完整的中断处理流程是由中断处理程序和下半部分共同完成的。
这样划分是有一定原因的,因为我们必须有一个快速、异步而且简单的处理程序专门来负责对硬件的中断请求做出快速响应,与此同时也要完成那些对时间要求很严格的操作。而那些对时间要求相对宽松,其他的剩余工作则会在稍候的任意时间执行,也就是在所谓的下半部分去执行。
总之,这样划分一个中断处理过程主要是希望减少中断处理程序的工作量(当然了,理想情况是将全部工作都抛给下半段。但是中断处理程序至少应该完成对中断请求的相应。),因为在它运行期间至少会使得同级的中断请求被屏蔽,这些都直接关系到整个系统的响应能力和性能。而在下半段执行期间,则会允许响应所有的中断。
和上半段只能通过中断处理程序实现不同的是,下半部可以通过多种机制来完成:小任务(tasklet),工作队列,软中断。在本博客后续的文章当中你会看到,不管是那种机制,它们均为下半部提供了一种执行机制,比上半部灵活多了。至于何时执行,则由内核负责。
以上是上下部分划分的基本概述,通过tasklet和工作队列机制,你可以更深刻的理解下部分的执行。
我们已经知道了中断通常由上下两部分组成。在上部分,也就是中断处理程序,完成中断请求的响应以及完成那些对时间要求紧迫的工作;而在下部分,通常完成那些被推后的工作,因为这部分工作对时间的要求相对宽松一些。通过了解上下两部分的工作情况,可以更好的理解中断这个概念。从下半部分执行机制来看——不管是tasklet还是工作队列——这些推后的工作总是在上半部分被调用,然后交给内核在适当的时间来完成。那么,中断上部分具体是如何工作的?内核对中断是如何处理的?
在开始分析之前,需要说明的是接下来分析的属于内核对外设产生的中断的处理情况,异常的处理过程在本文的最后会有简单解释。
所有的中断的处理程序在init_IRQ函数中都被初始化为interrupt[i]。interrupt数组中每一项均指向一个代码片段:
该代码片段除了将中断向量号压入堆栈,还会跳到一个公共处理程序common_interrup:
3 | 864 addl $-0x80,(%esp) /* Adjust vector into the [-256,-1] range * / |
这段公共处理程序会将中断发生前的所有寄存器的值压入堆栈,也就是保存被中断任务的现场。然后调用do_IRQ函数,在do_IRQ函数中会调用(并非直接调用那么简单)到handle_IRQ_event函数,在此函数中会执行实际的中断服务例程。当中断服务例程执行完毕后,会返回到上面的那段汇编程序中,转入ret_from_intr代码段从中断返回。
在上述文字描述的基础上,可以通过下图进一步加深中断的处理过程:
从上面的概述中,我们可以很快定位我们未来要分析的函数:do_IRQ()和hand_IRQ_event();在分析上述函数之前,我们很有必要先分析一下关于中断的三个重要的数据结构。
2.struct irq_desc
在一开始我们分析中断的时候,谈及到了中断向量。在内核中,每个中断向量都有相应的有一个irq_desc结构体(稍早内核版本中为irq_desc_t)来描述一个中断向量(也就是中断源),具体代码如下:
02 | 32typedef void (*irq_flow_handler_t)(unsigned int irq, |
03 | 33 struct irq_desc *desc); |
07 | 177 struct timer_rand_state *timer_rand_state; |
08 | 178 unsigned int *kstat_irqs; |
09 | 179#ifdef CONFIG_INTR_REMAP |
10 | 180 struct irq_2_iommu *irq_2_iommu; |
12 | 182 irq_flow_handler_t handle_irq; |
13 | 183 struct irq_chip *chip; |
14 | 184 struct msi_desc *msi_desc; |
15 | 185 void *handler_data; |
17 | 187 struct irqaction *action; |
18 | 188 unsigned int status; |
20 | 190 unsigned int depth; |
21 | 191 unsigned int wake_depth; |
22 | 192 unsigned int irq_count; |
23 | 193 unsigned long last_unhandled; |
24 | 194 unsigned int irqs_unhandled; |
25 | 195 raw_spinlock_t lock; |
27 | 197 cpumask_var_t affinity; |
28 | 198 const struct cpumask *affinity_hint; |
29 | 199 unsigned int node; |
30 | 200#ifdef CONFIG_GENERIC_PENDING_IRQ |
31 | 201 cpumask_var_t pending_mask; |
34 | 204 atomic_t threads_active; |
35 | 205 wait_queue_head_t wait_for_threads; |
36 | 206#ifdef CONFIG_PROC_FS |
37 | 207 struct proc_dir_entry *dir; |
42 | 216#ifndef CONFIG_SPARSE_IRQ |
43 | 217extern struct irq_desc irq_desc[NR_IRQS]; |
所有这样的描述符组织在一起形成irq_desc[NR_IRQS]数组。下面我们对上述结构体的部分字符进行解释;
irq:通过数据类型可知这便是这个描述符所对应的中断号;
handle_irq:指向该IRQ线的公共服务程序;
chip:它是一个struct irq_chip类型的指针,是中断控制器的描述符,与平台有关,下文有详细描述;
handler_data:用于handler_irq的参数;
chip_data:用于chip的参数;
action:一个struct irqaction类型的指针(下文有该结构的详细描述);它指向一个单链表,该单链表是由该中断线上所有中断服务程序(对应struct irqaction)所连接起来的;
status:描述中断线当前的状态;
depth:中断线被激活时,值为0;其值为正数时,表示被禁止的次数;
irq_count:记录该中断线发生中断的次数;
irqs_unhandled:该IRQ线上未处理中断发生的次数;
name: /proc/interrupts 中显示的中断名称;
3.struct irqaction
当多个设备共享一条IRQ线时,因为每个设备都要有各自的ISR。为了能够正确处理此条IRQ线上的中断处理程序(也就是区分每个设备),就需要我们使用irqaction结构体。在这个结构体中,会有专门的handler字段指向该设备的真正的ISR。共享同一条IRQ线上的多个这样的结构体会连接成了一个单链表,即所谓的中断请求队列。中断产生时,该IRQ线的中断请求队列上所有的ISR都会被依次调用,因此每个设备的ISR必须判断当前的中断是否是自己所属的设备产生的。irqaction结构具体的定义如下:
01 | 98typedef irqreturn_t (*irq_handler_t)( int , void *); |
04 | 114 irq_handler_t handler; |
05 | 115 unsigned long flags; |
08 | 118 struct irqaction *next; |
10 | 120 struct proc_dir_entry *dir; |
11 | 121 irq_handler_t thread_fn; |
12 | 122 struct task_struct * thread ; |
13 | 123 unsigned long thread_flags; |
handler:指向一个具体的硬件设备的中断服务例程,可以从此指针的类型发现与前文我们所定义的中断处理函数声明相同;
flags:对应request_irq函数中所传递的第三个参数,可取IRQF_DISABLED、IRQF_SAMPLE_RANDOM和IRQF_SHARED其中之一;
name:对应于request_irq函数中所传递的第四个参数,可通过/proc/interrupts文件查看到;
next:指向下一个irqaction结构体;
dev_id:对应于request_irq函数中所传递的第五个参数,可取任意值,但必须唯一能够代表发出中断请求的设备,通常取描述该设备的结构体;
irq:中断号
如果一个IRQ线上有中断请求,那么内核将依次次调用在该中断线上注册的每一个中断服务程序,但是并不是所有中断服务程序都被执行。一般硬件设备都会提供一个状态寄存器,以便中断服务程序进行检查是否应该为这个硬件服务。也就是说在整个中断请求队列中,最多会有一个ISR被执行,也就是该ISR对应的那个设备产生了中断请求时;不过当该IRQ线上某个设备未找到匹配的ISR时,那这个中断就不会被处理。此时irq_desc结构中的irqs_unhandled字段就会加1。
4.struct irq_chip
struct irq_chip是一个中断控制器的描述符。通常不同的体系结构就有一套自己的中断处理方式。内核为了统一的处理中断,提供了底层的中断处理抽象接口,对于每个平台都需要实现底层的接口函数。这样对于上层的中断通用处理程序就无需任何改动。这样的结构体就好比一个插板,我们可以使用各种插头。至于插板内部是如何实现的,使用者并不需要过于关心。
比如经典的中断控制器是2片级联的8259A,那么得15个irq_desc描述符,每一个描述符的irq_chip都指向描述8259A的i8259A_irq_type变量(arch/alpha/kernel/irq_i8259.c):
1 | 86struct irq_chip i8259a_irq_type = { |
3 | 88 .startup = i8259a_startup_irq, |
4 | 89 .shutdown = i8259a_disable_irq, |
5 | 90 .enable = i8259a_enable_irq, |
6 | 91 .disable = i8259a_disable_irq, |
7 | 92 .ack = i8259a_mask_and_ack_irq, |
8 | 93 .end = i8259a_end_irq, |
这一点类似于VFS中所采用的原理:通过struct file_operations提供统一的接口,每种文件系统都必须具体实现这个结构体中所提供的接口。struct irq_chip具体代码如下:
03 | 113 unsigned int (*startup)(unsigned int irq); |
04 | 114 void (*shutdown)(unsigned int irq); |
05 | 115 void (*enable)(unsigned int irq); |
06 | 116 void (*disable)(unsigned int irq); |
08 | 118 void (*ack)(unsigned int irq); |
09 | 119 void (*mask)(unsigned int irq); |
10 | 120 void (*mask_ack)(unsigned int irq); |
11 | 121 void (*unmask)(unsigned int irq); |
12 | 122 void (*eoi)(unsigned int irq); |
14 | 124 void (*end)(unsigned int irq); |
15 | 125 int (*set_affinity)(unsigned int irq, |
16 | 126 const struct cpumask *dest); |
17 | 127 int (*retrigger)(unsigned int irq); |
18 | 128 int (*set_type)(unsigned int irq, unsigned int flow_type); |
19 | 129 int (*set_wake)(unsigned int irq, unsigned int on); |
21 | 131 void (*bus_lock)(unsigned int irq); |
22 | 132 void (*bus_sync_unlock)(unsigned int irq); |
25 | 135#ifdef CONFIG_IRQ_RELEASE_METHOD |
26 | 136 void (*release)(unsigned int irq, void *dev_id); |
32 | 142 const char * typename ; |
name:中断控制器的名字;
Startup:启动中断线;
Shutdown:关闭中断线;
Enable:允许中断;
Disable:禁止中断;
5.数据结构之间的关系
首先我们通过下图来了解上述三个数据结构的关系:
通过上述分析,我们可以大致的知道中断处理程序的调用过程:在do_IRQ函数中,通过对irq_desc结构体中handler_irq字段的引用,调用handler_irq所指向的公共服务程序;在这个公共服务程序中会调用hand_IRQ_event函数;在hand_IRQ_event函数中,通过对irqaction结构体中handler字段的引用最终调用我们所写的中断处理程序。
另外,通过上述分析,我们知道struct irq_chip描述了中断最底层的部分;而struct irqacton则描述最上层具体的中断处理函数;而与中断向量所对应的struct desc则类似一个中间层,将中断中的硬件相关的部分和软件相关的部分连接起来。
6.内核对异常的处理
与中断不同,各种异常都有固定的中断向量(0~31)以及固定的异常处理程序。因此,当异常发生时,将直接跳转到相应的服务程序中执行。