Linux异常(中断)处理体系结构

前言:可以调到总结处先看明白这篇文章要说明的内容,再回到开头看。

1. 异常

异常,就是可以打断CPU正常运行流程的一些事情,比如:外部中断、未定义的指令、企图修改只读的数据、执行SWI指令(Software Interrupt Instruction,软件中断指令)等。

当这些事情发生时,CPU暂停当前的程序,先处理异常事件,然后再继续执行被中断的程序。操作系统中经常通过异常来完成一些特定的功能。

例如:

  • 当CPU执行未定义的机器指令时将触发“未定义指令异常”,操作系统可以利用这个特点使用一些自定义的机器指令,它们在异常处理函数中实现。

  • 可以将一块数据设为只读的,然后提供给多个进程共用,这样可以节省内存。当某个进程试图修改其中的数据时,将触发“数据访问中止异常”,在异常处理函数中将这块数据复制出来一份可写的副本,提供给这个进程使用。

  • 当用户程序试图读写的数据或执行的指令不在内存时,也会触发一个“数据访问中止异常”或“指令预取中止异常”。在异常处理函数中将这些数据或指令读入内存(内存不足时可以将不使用的数据、指令换出内存),然后重新执行被中断的程序。这样可以节省内存,还可以使得操作系统可以运行这类程序:它们使用的内存远大于实际的物理内存。

  • 当程序使用不对齐的地址访问内存时,也会触发“数据访问中止异常”,在异常处理程序中先使用多个对齐的地址读出数据;

    对于读操作,从中选取数据组合好后返回给被中断的程序;

    对于写操作,修改其中的部分数据后再写入内存。

    这使得程序不用考虑地址对齐的问题。

  • 用户程序可以通过“SWI”指令触发“SWI异常”,操作系统在swi异常处理函数中实现各种系统调用。

2. Linux内核对异常的初始化

内核在start_kernel函数(init/main.c中)调用trap_initinit_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_undvector_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类,它们分布在不同的文件中:

  1. arch/arm/kernel/traps.c:未定义指令异常的C处理函数在这个文件中定义,总入口函数为 do_undefinstr.
  2. arch/arm/mm/fault.c:与内存访问相关的异常的C处理函数在这个文件中定义,比如数据访问中止异常、指令预取中止异常。总入口函数为 do_DataAbort、do_PrefetchAbort。
  3. arch/arm/mm/irq.c:中断处理函数的在这个文件中定义,总入口函数为 asm_do_IRQ,它调用其他文件注册的中断处理函数。
  4. 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 中断处理流程

  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 链表中去除不需要的项。

其大致的流程如下:首先外设产生异常(也叫中断),经过一些列处理后,中断返回,回到被中断程序继续执行。
在这里插入图片描述

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 函数中调用一些列初始化函数:

  1. 包括 trap_init 函数。

    trap_init 函数完成的工作:将中断向量表拷贝到 0xffff0000 处,这个地址是我们在编译内核时指定的。

    也不一定是0xffff0000,根据不同的芯片可能会选择中断向量表的存放地址。例如:s3c2440芯片就规则了中断向量表存放的地址为 0x00000000 或 0xffff0000 两个地址,通过配置寄存器来选择不同的地址。

  2. 接着内核会调用 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 完成对中断请求的响应。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值