Linux 异常处理体系结构
1 Linux 异常处理体系结构概述
1.异常的作用
异常,就是可以打断 CPU 正常运行流程的一些事情,比如外部中断、未定义的指令、试图修改只读的数据、执行 swi 指令(Software Interrupt Instruction,软件中断指令)等。当这些事情发生时,CPU 暂停当前的程序,先处理异常事件,然后再继续执行被中断的程序。操作系统中经常通过异常来完成一些特定的功能。例如:
- 当 CPU 执行未定义的机器指令时将触发"未定义指令异常",操作系统可以利用这个特点使用一些自定义的机器指令,它们在异常处理函数中实现。
- 可以将一块数据设为只读的,然后提供给多个进程共用,这样可以节省内存。++当某个进程试图修改其中的数据时,将触发"数据访问中止异常",在异常处理函数中将这块数据复制出一份可写的副本,提供给这个进程使用。++
- 当用户程序试图读写的数据或执行的指令不在内存中时,也会触发一个"数据访问中止异常"或"指令预取中止异常",在异常处理函数中将这些数据或指令读入内存(内存不足时还可以将不使用的数据、指令换出内存),然后重新执行被中断的程序。这样可以节省内存,还使得操作系统可以运行这类程序:它们使用的内存远大于实际的物理内存。
- 当程序使用不对齐的地址访问内存时,也会触发"数据访问中止异常",在异常处理程序中先使用多个对齐的地址读出数据;对于读操作,从中选取数据组合好后返回给被中断的程序;对于写操作,修改其中的部分数据后再写入内存。这使得程序(特别是应用程序)不用考虑地址对齐的问题。
- 用户程序可以通过"swi"指令触发"swi 异常",操作系统在 swi 异常处理函数中实现各种系统调用。
| 异常总类 | 异常细分 |
|---|---|
| 未定义指令异常 | ARM 指令 break |
Thumb 指令 break |
|
ARM 指令 mrc(如访问未实现的协处理器寄存器) |
|
| 指令预取中止异常 | 取指时地址翻译错误(translation fault),系统尚未为该指令地址建立映射关系 |
| 数据访问中止异常 | 访问数据时段地址翻译错误(section translation fault) |
| 访问数据时页地址翻译错误(page translation fault) | |
| 地址对齐错误 | |
| 段权限错误(section permission fault) | |
| 页权限错误(page permission fault) | |
| ...... | |
| 中断异常 | GPIO 引脚中断 |
| WDT(看门狗)中断 | |
| 定时器中断 | |
| USB 中断 | |
| UART 中断等 | |
| SWI 异常 | 各类系统调用:sys_open、sys_read、sys_write 等 |
2.Linux 内核对异常的设置
内核在 start_kernel 函数中调用 trap_init、init_IRQ 两个函数来设置异常的处理函数。
trap_init(): 初始化异常 / 陷阱处理机制,注册各类内核异常(如之前了解的「数据访问中止异常」、页错误、除零错误等)的处理函数,搭建异常响应框架 ------ 这是 COW 等机制能正常工作的基础。init_IRQ(): 初始化中断控制器,注册各类硬件中断(如键盘、磁盘、网卡中断)的处理函数,搭建硬件中断响应框架,让内核能和外部硬件交互。
(1)trap_init 函数分析。
trap_init 函数(代码在 arch/arm/kernel/traps.c 中)被用来设置各种异常的处理向量,包括中断向量。所谓"向量",就是一些被安放在固定位置的代码,当发生异常时,CPU 会自动执行这些固定位置上的指令。ARM 架构 CPU 的异常向量基址可以是 0x00000000,也可以是 0xffff0000,Linux内核使用后者。trap_init 函数将异常向量复制到 0xffff0000 处,部分代码如下:
cs
708 void __init trap_init(void)
709 {
...
721 memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
722 memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
...
734 }
第 721 行中,vectors 等于 0xffff0000。地址__vectors_start~__vectors_end 之间的代码就
是异常向量,在 arch/arm/kernel/entry-armv.S 中定义,它们被复制到地址 0xffff0000 处。
异常向量的代码很简单,它们只是一些跳转指令。发生异常时,CPU 自动执行这些指令,跳转去执行更复杂的代码,比如保存被中断程序的执行环境,调用异常处理函数,恢复被 中 断 程 序 的 执 行 环 境 并 重 新 运 行 。
这 些 " 更 复 杂 的 代 码 " 在 地 址 __stubs_start ~__stubs_end 之间,它们在arch/arm/kernel/entry-armv.S 中定义。第 722 行将它们复制到地址0xffff0000+0x200 处。
异常向量、异常向量跳去执行的代码都是使用汇编写的,为给读者一个形象概念,下面讲解部分代码,它们在 arch/arm/kernel/entry-armv.S 中。
异常向量的代码如下,其中的"stubs_offset"用来重新定位跳转的位置(向量被复制到地址 0xffff0000 处,跳转的目的代码被复制到地址 0xffff0000+0x200 处)。
stubs_offset: 保证b相对跳转正确 __stubs_start****不保证紧跟在 __vectors_start + 0x200
cs
.equ stubs_offset, __vectors_start + 0x200 - __stubs_start
.globl __vectors_start
__vectors_start:
swi SYS_ERROR0 /* 复位异常 */
b vector_und + stubs_offset /* 未定义指令异常 */
ldr pc, .LCvswi + stubs_offset /* swi 异常 */
b vector_pabt + stubs_offset /* 指令预取中止 */
b vector_dabt + stubs_offset /* 数据访问中止 */
b vector_addrexcptn + stubs_offset /* 地址异常(未使用) */
b vector_irq + stubs_offset /* IRQ 中断 */
b vector_fiq + stubs_offset /* FIQ 中断 */
.globl __vectors_end
__vectors_end:
其中的 vector_und、vector_pabt 等表示要跳转去执行的代码。以 vector_und 为例,它仍在 arch/arm/kernel/entry-armv.S 中,通过 vector_stub 宏来定义,代码如下:
cs
1002 vector_stub und, UND_MODE
1003
1004 .long __und_usr @ 0 (USR_26 / USR_32),在用户模式执行了未定义的指令
1005 .long __und_invalid @ 1 (FIQ_26 / FIQ_32),在 FIQ 模式执行了未定义的指令
1006 .long __und_invalid @ 2 (IRQ_26 / IRQ_32),在 IRQ 模式执行了未定义的指令
1007 .long __und_svc @ 3 (SVC_26 / SVC_32),在管理模式执行了未定义的指令
1008 .long __und_invalid @ 4
1009 .long __und_invalid @ 5
1010 .long __und_invalid @ 6
1011 .long __und_invalid @ 7
1012 .long __und_invalid @ 8
1013 .long __und_invalid @ 9
1014 .long __und_invalid @ a
1015 .long __und_invalid @ b
1016 .long __und_invalid @ c
1017 .long __und_invalid @ d
1018 .long __und_invalid @ e
1019 .long __und_invalid @ f
第 1002 行的 vector_stub 是一个宏,它根据后面的参数"und, UND_MODE"定义了以"vector_und"为标号的一段代码。vector_stub 宏的功能为:计算处理完异常后的返回地址、保存一些寄存器(比如 r0、lr、spsr),然后进入管理模式,最后根据被中断的工作模式调用第 1004~1019 行中的某个跳转分支。 当发生异常时,CPU 会根据异常的类型进入某个工作模式,但是很快 vector_stub 宏又会强制 CPU 进入管理模式,在管理模式下进行后续处理,这种方法简化了程序设计,使得异常发生前的工作模式要么是用户模式,要么是管理模式。
第 1004~1019 行中的代码表示在各个工作模式下执行未定义指令时,发生的异常的处理分支。比如 1004 行的__und_usr 表示在用户模式下执行未定义指令时,所发生的未定义异常将由它来处理;第 1007 行的__und_svc 表示在管理模式下执行未定义指令时,所发生的未定义 异 常 将 由 它 来 处 理 。 在 其 他 工 作 模 式 下 不 可 能 发 生 未 定 义 指 令 异 常 , 否 则 使 用"__und_invalid"来处理错误。
ARM 架构 CPU 中使用 4 位数据来表示工作模式(目前只有 7种工作模式),所以共有 16 个跳转分支。
不同的跳转分支(比如__und_usr、__und_svc)只是在它们的入口处(比如保存被中断程序的寄存器)稍有差别,后续的处理大体相同,都是调用相应的 C 函数。比如未定义指令异常发生时,最终会调用 C 函数 do_undefinstr 来进行处理。各种的异常的 C 处理函数可以分为 5 类,它们分布在不同的文件中。
① 在 arch/arm/kernel/traps.c 中。
未定义指令异常的 C 处理函数在这个文件中定义,总入口函数为 do_undefinstr 。
② 在 arch/arm/mm/fault.c 中。
与内存访问相关的异常的 C 处理函数在这个文件中定义,比如数据访问中止异常、指令预取中止异常。总入口函数为 do_DataAbort、do_PrefetchAbort 。
③ 在 arch/arm/mm/irq.c 中。
中断处理函数的在这个文件中定义,总入口函数为 asm_do_IRQ ,它调用其他文件注册的中断处理函数。
④ 在 arch/arm/kernel/calls.S 中。
在这个文件中,swi 异常的处理函数指针被组织成一个表格;swi 指令机器码的位[23:0]被用来作为索引。这样,通过不同的"swi index"指令就可以调用不同的 swi 异常处理函数,它们被称为系统调用,比如 sys_open、sys_read、sys_write 等。
⑤ 没有使用的异常。
trap_init 函数搭建了各类异常的处理框架。当发生异常时,各种 C 处理函数会被调用。这些 C 函数还要进一步细分异常发生的情况,分别调用更具体的处理函数。比如未定义指令异常的 C 处理函数总入口为 do_undefinstr,这个函数里还要根据具体的未定义指令调用它的模拟函数。
除了中断外,内核已经为各类异常准备了细致而完备的处理函数,比如 swi 异常处理函数为每一种系统调用都准备了一个"sys_"开头的函数,数据访问中止异常的处理函数为对齐错误、页权限错误、段翻译错误等具体异常都准备了相应的处理函数。这些异常的处理函数与开发板的配置无关,基本不用修改。
(2)init_IRQ 函数分析。
中断也是一种异常,之所以把它单独提出来,是因为中断的处理与具体开发板密切相关,除一些必须、共用的中断(比如系统时钟中断、片内外设 UART 中断)外,必须由驱动开发者提供处理函数。内核提炼出中断处理的共性,搭建了一个非常容易扩充的中断处理体系。
init_IRQ 函数(代码在 arch/arm/kernel/irq.c 中)被用来初始化中断的处理框架,设置各种中断的默认处理函数。当发生中断时,中断总入口函数 asm_do_IRQ 就可以调用这些函数作进一步处理。
2 Linux 中断处理体系结构
1 中断处理体系结构的初始化
1.中断处理体系结构
Linux 内核将所有的中断统一编号,使用一个 irq_desc 结构数组来描述这些中断 :每个数组项对应一个中断(也有可能是一组中断,它们共用相同的中断号),里面记录了中断的名称、中断状态、中断标记(比如中断类型、是否共享中断等),并提供了中断的低层硬件访问函数(清除、屏蔽、使能中断),提供了这个中断的处理函数入口,通过它可以调用用户注册的中断处理函数。
通过 irq_desc 结构数组就可以了解中断处理体系结构,irq_desc 结构的数据类型在include/linux/irq.h 中定义,如下所示:
cs
struct irq_desc irq_desc[NR_IRQS];
struct irq_desc {
irq_flow_handler_t handle_irq; /* 中断处理流程入口 */
struct irq_chip *chip; /* 硬件相关操作 */
struct irqaction *action; /* 用户注册的中断处理函数链表 */
unsigned int status; /* 中断状态 */
const char *name; /* 中断名称 */
};
第 152 行的handle_irq 是这个或这组中断的处理函数入口。发生中断时,总入口函数
asm_do_IRQ 将根据中断号调用相应 irq_desc 数组项中的 handle_irq 。handle_irq 使用 chip 结构中的函数来清除、屏蔽或者重新使能中断,还一一调用用户在 action 链表中注册的中断处理函数。
第 153 行的 irq_chip 结构类型也是在 include/linux/irq.h 中定义,其中的成员大多用于操作底层硬件,比如设置寄存器以屏蔽中断、使能中断、清除中断等。这个结构的部分成员如下:
irq_chip 解决的是一个现实问题:
不同芯片 / 不同中断控制器,
清中断、关中断、开中断的方式完全不同
cs
98 struct irq_chip {
99 const char *name;
100 unsigned int (*startup)(unsigned int irq); /* 启动中断,如果不设置,缺省为
"enable" */
101 void (*shutdown)(unsigned int irq); /* 关闭中断,如果不设置,缺省为
"disable" */
102 void (*enable)(unsigned int irq); /* 使能中断,如果不设置,缺省为
"unmask" */
103 void (*disable)(unsigned int irq); /* 禁止中断,如果不设置,缺省为"mask" */
104
105 void (*ack)(unsigned int irq); /* 响应中断,通常是清除当前中断使得可以接收
下一个中断*/
106 void (*mask)(unsigned int irq); /* 屏蔽中断源 */
107 void (*mask_ack)(unsigned int irq); /* 屏蔽和响应中断 */
108 void (*unmask)(unsigned int irq); /* 开启中断源 */
...
126 }
irq_desc 结构中第 157 行的 irqaction 结构类型在 include/linux/interrupt.h 中定义。用户注册的每个中断处理函数用一个 irqaction 结构来表示,一个中断(比如共享中断)可以有多个处理函数,它们的 irqactio 结构链接成一个链表,以 action 为表头。irq_desc 结构定义如下
cs
84 struct irqaction {
85 irq_handler_t handler; /* 用户注册的中断处理函数 */
86 unsigned long flags; /* 中断标志,比如是否共享中断、电平触发还是边沿触发等 */
87 cpumask_t mask; /* 用于 SMP(对称多处理器系统) */
88 const char *name; /* 用户注册的中断名字,"cat /proc/interrupts"时可以看到 */
89 void *dev_id; /* 用户传给上面的 handler 的参数,还可以用来区分共享中断 */
90 struct irqaction *next;
91 int irq; /* 中断号 */
92 struct proc_dir_entry *dir;
93 };
三者之间的关系:
irq号
│
▼
irq_desc[irq]
├── handle_irq → 中断处理流程
├── chip → 操作硬件(清、关、开)
└── action → 用户中断处理函数链表
├── irqaction(handler1)
├── irqaction(handler2)
└── ...
中断的处理流程如下。
(1)发生中断时,CPU 执行异常向量 vector_irq 的代码。
(2)在 vector_irq 里面,最终会调用中断处理的总入口函数 asm_do_IRQ。
(3)asm_do_IRQ 根据中断号调用 irq_desc 数组项中的 handle_irq。
(4)handle_irq 会使用 chip 成员中的函数来设置硬件,比如清除中断、禁止中断、重新
使能中断等。
(5)handle_irq 逐个调用用户在 action 链表中注册的处理函数。
可见,中断体系结构的初始化就是构造这些数据结构,比如 irq_desc 数组项中的handle_irq、chip 等成员;用户注册中断时就是构造 action 链表;用户卸载中断时就是从 action链表中去除不需要的项。
2.中断处理体系结构的初始化
init_IRQ 函数被用来初始化中断处理体系结构,代码在 arch/arm/kernel/irq.c 中。
cs
156 void __init init_IRQ(void)
157 {
158 int irq;
159
160 for (irq = 0; irq < NR_IRQS; irq++)
161 irq_desc[irq].status |= IRQ_NOREQUEST | IRQ_NOPROBE;
...
167 init_arch_irq();
168 }
第 160~161 行初始化 irq_desc 结构数组中每一项的中断状态 。
第 167 行调用架构相关的中断初始化函数。
以外部中断 EINT4~EINT23 为例,用来设置它们的代码如下:
cs
760 for (irqno = IRQ_EINT4; irqno <= IRQ_EINT23; irqno++) {
761 irqdbf("registering irq %d (extended s3c irq)\n", irqno);
762 set_irq_chip(irqno, &s3c_irqext_chip);
763 set_irq_handler(irqno, handle_edge_irq);
764 set_irq_flags(irqno, IRQF_VALID);
765 }
第 762 行 set_irq_chip 函数 的作用就是"irq_desc[irqno].chip = &s3c_irqext_chip"。以后就可以通过 irq_desc[irqno].chip 结构中的函数指针设置这些外部中断的触发方式(电平触发、边沿触发等)、使能中断、禁止中断等。
第 763 行设置这些中断的处理函数入口 为 handle_edge_irq,即"irq_desc[irqno]. handle_irq =
handle_edge_irq"。发生中断时,handle_edge_irq 函数会调用用户注册的具体处理函数。
第 764 行设置中断标志 为"IRQF_VALID",表示可以使用它们。
对于本书使用的 S3C2410、S3C2440 开发板,init_IRQ 函数执行完后,图 20.2 中各个irq_desc 数组项的 chip、handle_irq 成员都被设置好了。
2 用户注册中断处理函数的过程
用户(即驱动程序)通过 request_irq 函数向内核注册中断处理函数,request_irq 函数根据中断号找到 irq_desc 数组项,然后在它的 action 链表中添加一个表项。
request_irq 函数在 kernel/irq/manage.c 中定义,函数原型如下:
cs
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)
request_irq 函数首先使用这 4 个参数构造一个 irqaction 结构,然后调用 setup_irq 函数将它链入链表中,代码如下:
cs
527 action = kmalloc(sizeof(struct irqaction), GFP_ATOMIC);
...
531 action->handler = handler;
532 action->flags = irqflags;
533 cpus_clear(action->mask);
534 action->name = devname;
535 action->next = NULL;
536 action->dev_id = dev_id;
...
559 retval = setup_irq(irq, action);
(1)将新建的 irqaction 结构链入 irq_desc[irq]结构的 action 链表中,这有两种可能。
① 如果 action 链表为空,则直接链入。
②否则先判断新建的 irqaction 结构和链表中的 irqaction 结构所表示的中断类型是否一致;即是否都声明为"可共享的"(IRQF_SHARED)、是否都使用相同的触发方式(电平、边沿、极性),如果一致,则将新建的 irqaction 结构链入 。
(2)设置 irq_desc[irq]结构中 chip 成员的还没设置的指针,让它们指向一些默认函数。
chip 成员在 init_IRQ 函数初始化中断体系结构的时候已经被设置,这里只是设置其中还没设置
的指针。这通过 irq_chip_set_defaults 函数来完成,它在 kernel/irq/chip.c 中定义。
cs
251 void irq_chip_set_defaults(struct irq_chip *chip)
252 {
253 if (!chip->enable)
254 chip->enable = default_enable; /* 它调用 chip->unmask */
255 if (!chip->disable)
256 chip->disable = default_disable; /* 此函数为空 */
257 if (!chip->startup)
258 chip->startup = default_startup; /* 它调用 chip->enable */
259 if (!chip->shutdown)
260 chip->shutdown = chip->disable;
261 if (!chip->name)
262 chip->name = chip->typename;
263 if (!chip->end)
264 chip->end = dummy_irq_chip.end;
265 }
(3)设置中断的触发方式。
如果 requet_irq 函数中传入的 irqflags 参数表示中断的触发方式为高电平触发、低电平触发、上升沿触发或下降沿触发,则调用 irq_desc[irq]结构中的 chip->set_type 成员函数来进行设置:设置引脚功能为外部中断,设置中断触发方式。
irqaction.flags 里的"触发方式"是"用户声明的需求",
chip->set_type() 是"真正把硬件配置成这种方式"。
两者不是一回事,也不能只留一个。
(4)启动中断。
如果 irq_desc[irq]结构中 status 成员没有被指明为 IRQ_NOAUTOEN(表示注册中断时不要使能中断),还要调用 chip->startup 或 chip->enable 来启动中断。所谓启动中断通常就是使能中断。
一般来说,只有那些"可以自动使能的"中断对应的 irq_desc[irq].status 才会被指明为
IRQ_NOAUTOEN。所以,无论哪种情况,执行 reqeust_irq 注册中断之后,这个中断就已经被使能了,在编写驱动程序时要注意这点。
总结一下使用 reqeust_irq 函数注册中断后的"成果"。
(1)irq_desc[irq]结构中的 action 链表中已经链入了用户注册的中断处理函数。
(2)中断的触发方式已经被设好。
(3)中断已经被使能。
总之,执行 reqeust_irq 函数之后,中断就可以发生并能够被处理了。
3 中断的处理过程
asm_do_IRQ是中断的 C 语言总入口函数,它在 arch/arm/kernel/irq.c 中定义,部分代码如下:
cs
111 asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
112 {
113 struct pt_regs *old_regs = set_irq_regs(regs);
114 struct irq_desc *desc = irq_desc + irq;
...
125 desc_handle_irq(irq, desc);
...
132 }
第 125 行的 desc_handle_irq 函数直接调用 desc 结构中的 handle_irq 成员函数,它就是
irq_desc[irq].handle_irq。
需要说明的是,asm_do_IRQ 函数中参数 irq 的取值范围为 IRQ_EINT0~(IRQ_EINT0 +31),只有 32 个取值。它可能是一个实际中断的中断号,也可能是一组中断的中断号。这是由 S3C2410、S3C2440 的芯片特性决定的:发生中断时 INTPND 寄存器的某一位被置 1,INTOFFSET 寄存器中记录了是哪一位(0~31),中断向量调用 asm_do_IRQ 之前根据INTOFFSET 寄存器的值确定 irq 参数。每一个实际的中断在 irq_desc 数组中都有一项与它对应,它们的数目不止 32。
当 asm_do_IRQ 函数中参数 irq 表示的是"一组"中断时,irq_desc[irq].handle_irq 成员函数还需要先分辨出是哪一个中断(假设中断号为 irqno),然后调用 irq_desc[irqno].handle_irq 来进一步处理。以外部中断 EINT8~EINT23 为例,它们通常是边沿触发。
硬件上,EINT8~EINT23 有 16 路外部中断源,但 SoC 往 CPU/主中断控制器上报时,可能只给一个"总中断号",比如 IRQ_EINT8t23。
(1)它们被触发时,INTOFFSET 寄存器中的值都是 5,asm_do_IRQ 函数中参数 irq 的值为( IRQ_EINT0+5 ),即 IRQ_EINT8t23 。上 面 代 码 中 第 125 行将 调 用irq_desc[IRQ_EINT8t23].handle_irq 来进行处理。
(2)irq_desc[IRQ_EINT8t23].handle_irq 在前面 init_IRQ 函数初始化中断体系结构的时候被设为 s3c_irq_demux_extint8。
(3)s3c_irq_demux_extint8 函数的代码在 arch/arm/plat-s3c24xx/irq.c 中,它首先读取EINTPEND、EINTMASK 寄存器,确定发生了哪些中断,重新计算它们的中断号,然后调用
irq_desc 数组项中的 handle_irq 成员函数。
代码如下
cs
558 static void
559 s3c_irq_demux_extint8(unsigned int irq,
560 struct irq_desc *desc)
561 {
562 unsigned long eintpnd = __raw_readl(S3C24XX_EINTPEND); /* EINT8~EINT23
发生时,相应位被置 1 */
563 unsigned long eintmsk = __raw_readl(S3C24XX_EINTMASK); /* 屏蔽寄存器 */
564
565 eintpnd &= ~eintmsk; /* 清除被屏蔽的位 */
566 eintpnd &= ~0xff; /* 清除低 8 位(EINT8 对应位 8,...) */
567
568 /* 循环处理所有的子中断 */
569
570 while (eintpnd) {
571 irq = __ffs(eintpnd); /* 确定 eintpnd 中为 1 的最高位 */
572 eintpnd &= ~(1<<irq); /* 将此位清 0 */
573
574 irq += (IRQ_EINT4 - 4); /* 重新计算中断号:前面计算出 irq 等于 8 时,中断
号为 IRQ_EINT8 */
575 desc_handle_irq(irq, irq_desc + irq); /* 调用这个中断的真正的处理函
数入口 */
576 }
577
578 }
asm_do_IRQ 先拿到的是"组中断号"(父 irq),父 irq 的 handle_irq 是 demux 函数;demux 读取 EINTPEND/EINTMASK 找到真正触发的 EINTx(子 irq),再逐个调用子 irq 的 handle_irq,最终才会调用驱动注册的 handler。
(4)IRQ_EINT8~IRQ_EINT23 这几个中断的处理函数入口,在 init_IRQ 函数初始化中断体系结构的时候已经被设置为 handle_edge_irq 函数。上面第 575 行的代码就是调用这个函数,它在 kernel/irq/chip.c 中定义。从它的名字可以知道,它用来处理边沿触发的中断(处理电平触发的中断为 handle_level_irq)。以下的讲解中,只关心一般的情形,忽略有关中断嵌套的代码,部分代码如下:
cs
445 void fastcall
446 handle_edge_irq(unsigned int irq, struct irq_desc *desc)
447 {
...
466 kstat_cpu(cpu).irqs[irq]++;
467
468 /* Start handling the irq */
469 desc->chip->ack(irq);
...
497 action_ret = handle_IRQ_event(irq, action);
...
507 }
第 466 行用来统计中断发生的次数。
第 469 行响应中断,通常是清除当前中断使得可以接收下一个中断。对于 IRQ_EINT8~
IRQ_EINT23 这几个中断,desc->chip 在前面 init_IRQ 函数初始化中断体系结构的时候被设为
s3c_irqext_chip。desc->chip->ack 就是 s3c_irqext_ack 函数(arch/arm/plat-s3c24xx/ irq.c),它
用来清除中断。
第 497 行通过 handle_IRQ_event 函数来逐个执行 action 链表中用户注册的中断处理函数,它在 kernel/irq/handle.c 中定义,关键代码如下:
cs
139 do {
140 ret = action->handler(irq, action->dev_id); /* 执行用户注册的中断处
理函数 */
141 if (ret == IRQ_HANDLED)
142 status |= action->flags;
143 retval |= ret;
144 action = action->next; /* 下一个 */
145 } while (action);
从第 140 行可以知道,用户注册的中断处理函数的参数为中断号 irq、action->dev_id。后一个参数是通过 request_irq 函数注册中断时传入的 dev_id 参数。它由用户自己指定、自己使用,可以为空,当这个中断是"共享中断"时除外 (这在下面卸载中断时说明)。
对于电平触发的中断,它们的 irq_desc[irq].handle_irq 通常是 handle_level_irq 函数。它也是在 kernel/irq/chip.c 中定义,其功能与上述 handle_edge_irq 函数相似,关键代码如下:
cs
335 void fastcall
336 handle_level_irq(unsigned int irq, struct irq_desc *desc)
337 {
...
343 mask_ack_irq(desc, irq);
...
348 kstat_cpu(cpu).irqs[irq]++;
...
364 action_ret = handle_IRQ_event(irq, action);
...
371 desc->chip->unmask(irq);
...
374 }
第 343 行用来屏蔽和响应中断,响应中断通常就是清除中断,使得可以接收下一个中断。这时即使触发了下一个中断,也只是记录寄存器中而已,只有在中断被再次使能后才能被处理。
第 348 行用来统计中断发生的次数。
第 364 行通过 handle_IRQ_event 函数来逐个执行 action 链表中用户注册的中断处理函数。
第 371 行开启中断,与前面第 343 行屏蔽中断对应。
在 handle_edge_irq、handle_level_irq 函数的开头都清除了中断。所以一般来说,在用户注册的中断处理函数中就不用再次清除中断了。但是对于电平触发的中断也有例外:虽然handle_level_irq 函数已经清除了中断,但是它只限于清除 SoC 内部的信号;如果外设输入到SoC 的中断信号仍然有效,这就会导致当前中断处理完毕后,会误认为再次发生了中断。对于这种情况,需要在用户注册的中断处理函数中清除中断:先清除外设的中断,然后再清除SoC 内部的中断信号。
忽略上述的中断号重新计算过程(这不影响对整体流程的理解),中断的处理流程可以总结如下。
(1)中断向量调用总入口函数 asm_do_IRQ,传入根据中断号 irq。
(2)asm_do_IRQ 函数根据中断号 irq 调用 irq_desc[irq].handle_irq,它是这个中断的处理
函数入口。对于电平触发的中断,这个入口通常为 handle_level_irq;对于边沿触发的中断,
这个入口通常为 handle_edge_irq。
(3)入口函数首先清除中断,入口函数是 handle_level_irq 时还要屏蔽中断。
(4)逐个调用用户在 irq_desc[irq].action 链表中注册的中断处理函数。
(5)入口函数是 handle_level_irq 时还要重新开启中断。
4 卸载中断处理函数
**中断是一种很稀缺的资源,当不再使用一个设备时,应该释放它占据的中断。**这通过 free_irq函数来实现,它与 request_irq 一样,也是在 kernel/irq/manage.c 中定义。它的函数原型如下
cs
void free_irq(unsigned int irq, void *dev_id)
它需要用到两个参数:irq 和 dev_id,它们与通过 reqeust_irq 注册中断函数时使用的参数一样。使用中断号 irq 定位 action 链表,再使用 dev_id 在 action 链表中找到要卸载的表项。
所以,同一个中断的不同中断处理函数必须使用不同的 dev_id 来区分,这就要求在注册共享中断时参数 dev_id 必须惟一。
free_irq 函数的处理过程与 reqeust_irq 函数相反。
(1)根据中断号 irq、dev_id 从 action 链表中找到表项,将它移除。
(2)如果它是惟一的表项,还要调用 IRQ_DESC[IRQ].CHIP->SHUTDOWN 或 IRQ_
DESC[IRQ]. CHIP->DISABLE 来关闭中断。