前言:可以调到总结处先看明白这篇文章要说明的内容,再回到开头看。
1. 异常
异常,就是可以打断CPU正常运行流程的一些事情,比如:外部中断、未定义的指令、企图修改只读的数据、执行SWI指令(Software Interrupt Instruction,软件中断指令)等。
当这些事情发生时,CPU暂停当前的程序,先处理异常事件,然后再继续执行被中断的程序。操作系统中经常通过异常来完成一些特定的功能。
例如:
-
当CPU执行未定义的机器指令时将触发“未定义指令异常”,操作系统可以利用这个特点使用一些自定义的机器指令,它们在异常处理函数中实现。
-
可以将一块数据设为只读的,然后提供给多个进程共用,这样可以节省内存。当某个进程试图修改其中的数据时,将触发“数据访问中止异常”,在异常处理函数中将这块数据复制出来一份可写的副本,提供给这个进程使用。
-
当用户程序试图读写的数据或执行的指令不在内存时,也会触发一个“数据访问中止异常”或“指令预取中止异常”。在异常处理函数中将这些数据或指令读入内存(内存不足时可以将不使用的数据、指令换出内存),然后重新执行被中断的程序。这样可以节省内存,还可以使得操作系统可以运行这类程序:它们使用的内存远大于实际的物理内存。
-
当程序使用不对齐的地址访问内存时,也会触发“数据访问中止异常”,在异常处理程序中先使用多个对齐的地址读出数据;
对于读操作,从中选取数据组合好后返回给被中断的程序;
对于写操作,修改其中的部分数据后再写入内存。
这使得程序不用考虑地址对齐的问题。
-
用户程序可以通过“SWI”指令触发“SWI异常”,操作系统在swi异常处理函数中实现各种系统调用。
2. Linux内核对异常的初始化
内核在start_kernel
函数(init/main.c中)调用trap_init
、init_IRQ
这两个函数来设置异常的处理函数。
2.1 trap_init 函数分析
trap_init
函数(代码在arch/arm/kernel/traps.c中)被用来设置各种异常的处理向量,包括中断向量。
所谓“向量”,就是一些被安放在固定位置的代码,当发送异常时,CPU会自动执行这些固定位置上的指令。
ARM架构CPU异常向量基址可以是0x00000000,也可以是0xffff0000,Linux内核使用后者。
trap_init
函数将异常向量复制到0xffff0000处,部分代码如下:
void __init trap_init(void)
{
// 省略
memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);
// 省略
}
vectors 等于0xffff0000。地址 __vectors_start ~ __vectors_ends 之间的代码就是异常向量,在 arch/arm/kernel/entry-armv.S 中定义,它们被复制到地址0xffff0000处。
异常向量的代码很简单,它们只是一些跳转指令。发生异常时,CPU自动执行这些指令,跳转去执行更复杂的代码,比如保存被中断程序的执行环境,调用异常处理函数,恢复被中断程序的执行环境并重新运行。这些“更复杂的代码”在地址 __stubs_start ~ __stubs_end 之间,它们定义在 arch/arm/kernel/entry-armv.S 。
memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
将它们复制到地址0xffff0000+0x200处。
异常向量、异常向量跳去执行的代码都是使用汇编写的:
.globl __vectors_start
__vectors_start:
swi SYS_ERROR0 /* 复位时,CPU将执行这条指令 */
b vector_und + stubs_offset /* 未定义异常时,CPU将执行这条指令 */
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
宏定义,代码如下:
/*
* Undef instr entry dispatcher
* Enter in UND mode, spsr = SVC/USR CPSR, lr = SVC/USR PC
*/
vector_stub und, UND_MODE
.long __und_usr @ 0 (USR_26 / USR_32) 用户模式执行了未定义的指令
.long __und_invalid @ 1 (FIQ_26 / FIQ_32) 在FIQ模式执行了未定义的指令
.long __und_invalid @ 2 (IRQ_26 / IRQ_32) 在IRQ模式执行了未定义的指令
.long __und_svc @ 3 (SVC_26 / SVC_32) 在管理模式执行力未定义的指令
.long __und_invalid @ 4
.long __und_invalid @ 5
.long __und_invalid @ 6
.long __und_invalid @ 7
.long __und_invalid @ 8
.long __und_invalid @ 9
.long __und_invalid @ a
.long __und_invalid @ b
.long __und_invalid @ c
.long __und_invalid @ d
.long __und_invalid @ e
.long __und_invalid @ f
.align 5
第5行的 vector_stub
是一个宏,它根据后面的参数 “und, UND_MODE” 定义了以 vector_und
为标号的一段代码。
vector_stub的定义如下:
/* * Vector stubs. * * This code is copied to 0xffff0200 so we can use branches in the * vectors, rather than ldr's. Note that this code must not * exceed 0x300 bytes. * * Common stub entry macro: * Enter in IRQ mode, spsr = SVC/USR CPSR, lr = SVC/USR PC * * SP points to a minimal amount of processor-private memory, the address * of which is copied into r0 for the mode specific abort handler. */ .macro vector_stub, name, mode, correction=0 .align 5 vector_\name: .if \correction sub lr, lr, #\correction .endif @ @ Save r0, lr_<exception> (parent PC) and spsr_<exception> @ (parent CPSR) @ stmia sp, {r0, lr} @ save r0, lr mrs lr, spsr str lr, [sp, #8] @ save spsr @ @ Prepare for SVC32 mode. IRQs remain disabled. @ mrs r0, cpsr eor r0, r0, #(\mode ^ SVC_MODE) msr spsr_cxsf, r0 @ @ the branch table must immediately follow this code @ and lr, lr, #0x0f mov r0, sp ldr lr, [pc, lr, lsl #2] movs pc, lr @ branch to handler in SVC mode .endm
vector_stub 宏的功能:计算处理完异常后的返回地址、保存一些寄存器(r0、lr、spsr),然后进入管理模式,最后根据被中断的工作模式跳转到某个分支。
当发生异常时,CPU 会根据异常的类型进入某个工作模式,但是很快 vector_\name 宏又会限制 CPU 进入管理模式,在管理模式下进行后续的处理,这种方式简化了程序设计,使得异常发生前的工作模式要么是用户模式,要么是管理模式。
因此,完整的 vector_und 代码如下:
vector_und:
@
@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
@ (parent CPSR)
@
stmia sp, {r0, lr} @ save r0, lr
mrs lr, spsr
str lr, [sp, #8] @ save spsr
@
@ Prepare for SVC32 mode. IRQs remain disabled.
@
mrs r0, cpsr
eor r0, r0, #(\mode ^ SVC_MODE)
msr spsr_cxsf, r0
@
@ the branch table must immediately follow this code
@
and lr, lr, #0x0f
mov r0, sp
ldr lr, [pc, lr, lsl #2]
movs pc, lr @ branch to handler in SVC mode
.long __und_usr @ 0 (USR_26 / USR_32) 用户模式执行了未定义的指令
.long __und_invalid @ 1 (FIQ_26 / FIQ_32) 在FIQ模式执行了未定义的指令
.long __und_invalid @ 2 (IRQ_26 / IRQ_32) 在IRQ模式执行了未定义的指令
.long __und_svc @ 3 (SVC_26 / SVC_32) 在管理模式执行力未定义的指令
.long __und_invalid @ 4
.long __und_invalid @ 5
.long __und_invalid @ 6
.long __und_invalid @ 7
.long __und_invalid @ 8
.long __und_invalid @ 9
.long __und_invalid @ a
.long __und_invalid @ b
.long __und_invalid @ c
.long __und_invalid @ d
.long __und_invalid @ e
.long __und_invalid @ f
vector_und 会跳转到__und_usr
、__und_svc
、__und_invalid
等16个不同的分支。
- __und_usr:表示在用户模式下执行未定义指令时,所发送的未定义异常将由它来处理;
- __und_svc:表示在管理模式下执行未定义指令时,所发送的未定义异常将由它来处理;
- __und_invalid:其它工作模式下不可能发送未定义指令异常,否则使用该分支。
不同的跳转分支只是在它们的入口处,保存被中断程序寄存器稍微有差别,后续的处理大体相同,都是调用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 等。 ARM 软中断指令_SWI指令
在 Linux 2.6.22.6 中没有使用 FIQ 异常。
通过对不同异常向量的分析,我们最终可以得到如下面的 ARM 架构下的 Linux 异常处理体系结构图:
3. Linux中断处理体系结构
中断也是一种异常,中断的处理与具体的开发板密切相关,除了一些必须、共用的中断(比如:系统时钟中断、片内外设UART中断)外,必须由驱动开发者提供处理函数。
内核提炼出了中断处理的共性,搭建了一个非常容易扩充的中断处理体系。
3.1 init_IRQ 函数分析
init_IRQ 函数(代码在arch/arm/kernel/irq.c中)被用来初始化中断的处理框架,设置各种中断默认处理函数。
当发生中断时,中断入口函数 asm_do_IRQ 就可以调用 init_IRQ 初始化好了的函数作进一步处理。
Linux 内核对所有的中断统一编号,使用一个 irq_desc 结构数组来描述这些中断:每个数组项对应一个中断。(也有可能是一组中断,它们共用相同的中断号)结构体里面记录了中断的名称、中断状态(比如中断类型、是否共享中断等),并提供了中断的底层硬件访问函数(清除、屏蔽、使能中断),提供了这个中断的处理函数入口,通过它可以调用用户注册的中断处理函数。
3.2 中断数据结构
irq_desc 结构体:
struct irq_desc {
irq_flow_handler_t handle_irq; /* 当前中断的处理函数函数入口 */
struct irq_chip *chip; /* 低层的硬件访问 */
/* ...省略... */
struct irqaction *action; /* 用户提供的中断处理函数链表 */
unsigned int status; /* IRQ 状态 */
/* ...省略... */
const char *name; /* 中断的名字 */
};
handle_irq:是这个或这组中断的处理函数入口。发送中断时,总入口函数 asm_do_IRQ 将根据中断号调用相应 irq_desc 数组项中的 handle_irq。
handle_irq 使用 chip 结构中的函数来清除、屏蔽或者重新使能中断,还一一调用用户在 action 链表中注册的中断处理函数。
irq_chip 结构:
struct irq_chip {
const char *name;
unsigned int (*startup)(unsigned int irq); /* 启动中断,如果不设置,缺省为enable */
void (*shutdown)(unsigned int irq); /* 关闭中断,如果不设置,缺省为disable */
void (*enable)(unsigned int irq); /* 使能中断,如果不设置,缺省为unmask */
void (*disable)(unsigned int irq); /* 禁止中断,如果不设置,缺省为mask */
void (*ack)(unsigned int irq); /* 响应中断,通常是清除当前中断使得可以接收下一个中断 */
void (*mask)(unsigned int irq); /* 屏蔽中断源 */
void (*mask_ack)(unsigned int irq); /* 屏蔽和响应中断 */
void (*unmask)(unsigned int irq); /* 开启中断源 */
/* ...省略... */
};
该结构体中的成果大多用于操作底层硬件,比如设置寄存器以屏蔽中断、使能中断、清除中断等。
irqaction 结构:
struct irqaction {
irq_handler_t handler; /* 用户注册的中断处理函数 */
unsigned long flags; /* 中断标志,比如:是否共享中断、电平触发还是边沿触发等 */
cpumask_t mask; /* 用于SMP(对称多处理器系统) */
const char *name; /* 用户注册的中断名字,"cat/proc/interrupts"时可以看到 */
void *dev_id; /* 用户传给上面的handler参数,还可以用来区分共享中断 */
struct irqaction *next;
int irq; /* 中断号 */
struct proc_dir_entry *dir;
};
irq_desc 结构数组和它的成员 struct irq_chip *chip、struct irqaction *action 构成了中断处理体系的构架。
3.3 中断处理流程
- 发生中断时,CPU执行异常向量 vector_irq 的代码;
- 在 vector_irq 里面,最终会调用中断处理的总入口函数 asm_do_IRQ;
- asm_do_IRQ 根据中断号调用 irq_desc 数组项中的 handle_irq;
- handle_irq 会使用 chip 成员中的函数来设置硬件,比如清除中断、禁止中断、重新使能中断等;
- handle_irq 逐个调用用户在 action 链表中注册的处理函数;
可见,中断体系结构的初始化就是构造这些数据结构,比如 irq_desc 数组中的 handle_irq、chip 等这些数据结构。用户注册中断时就是构造 action 链表;用户卸载中断时就是从 action 链表中去除不需要的项。
其大致的流程如下:首先外设产生异常(也叫中断),经过一些列处理后,中断返回,回到被中断程序继续执行。
3.4 asm_do_IRQ 函数
vector_irq 在 arch/arm/kernel/entry-armv.S 中定义:(对 vector_\name 进行了展开)
vector_irq:
sub lr, lr, 4
@
@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
@ (parent CPSR)
@
stmia sp, {r0, lr} @ save r0, lr
mrs lr, spsr
str lr, [sp, #8] @ save spsr
@
@ Prepare for SVC32 mode. IRQs remain disabled.
@
mrs r0, cpsr
eor r0, r0, #(\mode ^ SVC_MODE)
msr spsr_cxsf, r0
@
@ the branch table must immediately follow this code
@
and lr, lr, #0x0f
mov r0, sp
ldr lr, [pc, lr, lsl #2]
movs pc, lr @ branch to handler in SVC mode
.long __irq_usr @ 0 (USR_26 / USR_32)
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32)
.long __irq_invalid @ 4
.long __irq_invalid @ 5
.long __irq_invalid @ 6
.long __irq_invalid @ 7
.long __irq_invalid @ 8
.long __irq_invalid @ 9
.long __irq_invalid @ a
.long __irq_invalid @ b
.long __irq_invalid @ c
.long __irq_invalid @ d
.long __irq_invalid @ e
.long __irq_invalid @ f
用户模式的中断指令 __irq_usr的定义如下:
__irq_usr:
usr_entry
/* ...省略... */
irq_handler
/* ...省略... */
b ret_to_user
其中 irq_handler 的定义如下:
.macro irq_handler
get_irqnr_preamble r5, lr
1: get_irqnr_and_base r0, r6, r5, lr
movne r1, sp
@
@ routine called with r0 = irq number, r1 = struct pt_regs *
@
adrne lr, 1b
bne asm_do_IRQ
最终会调用 asm_do_IRQ 函数进行中断的处理操作。在此之前看一下 get_irqnr_and_base r0, r6, r5, lr
这条指令,get_irqnr_and_base 也是一个宏定义,其源码如下:
.macro get_irqnr_and_base, irqnr, irqstat, base, tmp
mov \base, #S3C24XX_VA_IRQ
@@ try the interrupt offset register, since it is there
ldr \irqstat, [ \base, #INTPND ]
teq \irqstat, #0
beq 1002f
ldr \irqnr, [ \base, #INTOFFSET ]
mov \tmp, #1
tst \irqstat, \tmp, lsl \irqnr
bne 1001f
@@ the number specified is not a valid irq, so try
@@ and work it out for ourselves
mov \irqnr, #0 @@ start here
@@ work out which irq (if any) we got
movs \tmp, \irqstat, lsl#16
addeq \irqnr, \irqnr, #16
moveq \irqstat, \irqstat, lsr#16
tst \irqstat, #0xff
addeq \irqnr, \irqnr, #8
moveq \irqstat, \irqstat, lsr#8
tst \irqstat, #0xf
addeq \irqnr, \irqnr, #4
moveq \irqstat, \irqstat, lsr#4
tst \irqstat, #0x3
addeq \irqnr, \irqnr, #2
moveq \irqstat, \irqstat, lsr#2
tst \irqstat, #0x1
addeq \irqnr, \irqnr, #1
@@ we have the value
1001:
adds \irqnr, \irqnr, #IRQ_EINT0
1002:
@@ exit here, Z flag unset if IRQ
.endm
大致可以看得出,该宏读取了 INTPND、INTOFFSET 等寄存器,也就获取了中断的中断号。将中断号放在 r0 寄存器里,在后面传给 asm_do_IRQ 函数。
asm_do_IRQ 的定义如下:
asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
struct irq_desc *desc = irq_desc + irq;
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (irq >= NR_IRQS)
desc = &bad_irq_desc;
irq_enter();
desc_handle_irq(irq, desc);
/* AT91 specific workaround */
irq_finish(irq);
irq_exit();
set_irq_regs(old_regs);
}
struct irq_desc *desc = irq_desc + irq;
:获取 irq 对应的 irq_desc 数据结构;
desc_handle_irq(irq, desc);
:根据 desc 中记录的操作函数,对 irq 进行处理;
而对于 __irq_svc 类型的异常处理与 __irq_usr 的非常相似,不做累述。
4. 总结
通过上面的分析我们大致弄明白了 ARM 架构下 Linux 整个的异常处理流程。
Linux 系统启动时,会在 start_kernel
函数中调用一些列初始化函数:
-
包括
trap_init
函数。trap_init
函数完成的工作:将中断向量表拷贝到 0xffff0000 处,这个地址是我们在编译内核时指定的。也不一定是0xffff0000,根据不同的芯片可能会选择中断向量表的存放地址。例如:s3c2440芯片就规则了中断向量表存放的地址为 0x00000000 或 0xffff0000 两个地址,通过配置寄存器来选择不同的地址。
-
接着内核会调用
init_IRQ
函数,完成中断数据结构 irq_desc 的初始化工作。包括设置全局的 irq_desc 数组,根据 NR_IRQS 这个宏定义,设置一个大小为 NR_IRQS 的中断数组。NR_IRQS 指定了内核支持的中断种类的个数。
同时填充每一个 irq_desc 结构体的 irq_chip 和 handle_irq 结构。前者包含了硬件相关的操作,包括使芯片使能中断,清除中断等硬件相关的操作;后者指定了发送中断时要掉调用的中断请求的处理函数。
并且对每一个 irq_desc 设置默认的操作函数,如果应用程序没有指定特定的操纵函数,后续触发中断时都会调用默认的操作函数。
完成上面的工作后,中断的初始化就完成了。
当有中断被触发时,通过中断向量表,最终会进入 asm_do_IRQ 函数。该函数会根据中断号,从全局的 irq_desc 数组中获取到中断号对应的 irq_desc,再根据其内部设置的 irq_chip 和 handle_irq 完成对中断请求的响应。