一起分析Linux系统设计思想——05中断框架剖析

Posted 穿越临界点

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一起分析Linux系统设计思想——05中断框架剖析相关的知识,希望对你有一定的参考价值。

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的无效信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。


中断从哪里来,到哪里去?中断号是如何分配的?带着这些疑问一起探索吧~

4 中断处理流程

前面的内容涉及的都是中断的准备工作,本节才真正打通从硬件中断产生到中断处理的整个流程。

4.1 汇编部分

汇编部分的代码主要负责和处理器架构相关的部分,例如中断向量的选择、中断现场保护和恢复等。

4.1.1 中断向量重映射

我们以 trap_init 函数作为突破口,来按图索骥地找到中断向量的汇编代码。

能够想到从trap_init函数作为突破口,基于下面两点:

  1. 中断可以看作陷阱的一种。
  2. trap_init函数被start_kernel函数调用了。

trap_init函数主要的功能就是将vectors、stubs等代码搬移到0xffff0000地址开始的所在页。这个动作我们可以给它起个大气的名字—— 重映射/重定位 。其中,vectors为中断向量表,stubs为中断处理函数总入口。

那我们为什么必须要进行中断向量重映射呢?

ARM架构的CPU的异常向量基址由 CONFIG_VECTORS_BASE 决定,不开启MMU时配置为 0x00000000 ,否则配置为 0xffff0000 。我们这里是开启了MMU的,因此CPU的异常向量基址是0xfff0000。如果异常向量基址只能是0x00000000,可以选择将中断向量代码直接链接在该地址。正是因为0xfff0000的存在才使得中断向量重映射的步骤必须存在。

/* arch/arm/kernel */
void __init trap_init(void)

	unsigned long vectors = CONFIG_VECTORS_BASE;
	extern char __stubs_start[], __stubs_end[];
	extern char __vectors_start[], __vectors_end[];
	extern char __kuser_helper_start[], __kuser_helper_end[];
	int kuser_sz = __kuser_helper_end - __kuser_helper_start;

	/*
	 * Copy the vectors, stubs and kuser helpers (in entry-armv.S)
	 * into the vector page, mapped at 0xffff0000, and ensure these
	 * are visible to the instruction stream.
	 */
	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);

	/*
	 * Copy signal return handlers into the vector page, and
	 * set sigreturn to be a pointer to these.
	 */
	memcpy((void *)KERN_SIGRETURN_CODE, sigreturn_codes,
	       sizeof(sigreturn_codes));

	flush_icache_range(vectors, vectors + PAGE_SIZE); /*确保该页存储内容可被CPU执行*/
	modify_domain(DOMAIN_USER, DOMAIN_CLIENT); /*修改页面的访问权限*/



4.1.2 中断向量选择

好了,既然我们已经知道中断向量放在了__vectors_start开始的代码中,我们就一起来分析下 __vectors_start的代码。

代码如下所示,可以看到格式统一且简单,就是简单的跳转。对于外部中断发生时,硬件会自动控制PC指针指向 b vector_irq + stubs_offset 代码执行,也就是所谓的中断向量选择。

需要注意的一点是vector_irq代码在trap_init函数中进行过重定位,因此跳转时需要进行相对偏移的矫正,即加上偏移量stubs_offset。

/* arc/arm/kernel/entry-armv.S */
	.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
	b	vector_pabt + stubs_offset
	b	vector_dabt + stubs_offset
	b	vector_addrexcptn + stubs_offset
	b	vector_irq + stubs_offset	/*外部中断跳转代码*/
	b	vector_fiq + stubs_offset

	.globl	__vectors_end
__vectors_end:

4.1.3 中断现场保护和恢复

4.1.3.1 vector_irq

继续顺藤摸瓜,下一步就是找到函数vector_irq。我们直接搜索是找不到vector_irq函数的,因为vector_irq是通过宏定义的,代码如下:

/* arch/arm/kernel/entry-armv.S */
	.globl	__stubs_start
__stubs_start:
/*
 * Interrupt dispatcher
 */
	vector_stub	irq, IRQ_MODE, 4 /* vector_stub为一个宏,需要展开 */

	.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

     ...
__vectors_end:   

vector_stub irq, IRQ_MODE, 4 宏定义展开如下:

/* 将宏定义展开 */
vector_irq: /*展开之后的真实标号名称*/
	sub	lr, lr, #4 /*计算返回地址(ARM9具有5级流水线)*/

	@
	@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
	@ (parent CPSR)
	@
	stmia	sp, r0, lr		@ save r0, lr /*将r0和lr(返回地址)压栈*/
	mrs	lr, spsr
	str	lr, [sp, #8]		@ save spsr /*spsr=中断前的cpsr压栈*/

	@
	@ Prepare for SVC32 mode.  IRQs remain disabled. /*为进入SVC模式做准备*/
	@
	mrs	r0, cpsr
	eor	r0, r0, #(IRQ_MODE ^ SVC_MODE)
	msr	spsr_cxsf, r0 

	@
	@ the branch table must immediately follow this code
	@
	and	lr, lr, #0x0f /*取得lr(此时lr=IRQ模式spsr=中断前的cpsr)的低四位(exception mode number)*/
	mov	r0, sp /*r0指向IRQ中断模式的堆栈指针*/
	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

     ...
__vectors_end:        

vector_irq根据进入中断前的模式又做了一次跳转。

4.1.3.2 __irq_usr

这里,我们选择__irq_usr进行分析。该函数包含了中断处理的大体框架:现场保护,中断处理和中断返回等。由于内容较多,不一一展开了,后面重点分析irq_handler。

/* arc/arm/kernel/entry-armv.S */
	.align	5
__irq_usr:
	usr_entry /*这是一个宏,主要用来保存现场*/

...
	get_thread_info tsk /*获取当前进程的进程描述符中的成员thread_info的地址,并将该地址保存到tsk*/
...

	irq_handler /*中断处理*/
...

	mov	why, #0
	b	ret_to_user /*中断返回*/

	.ltorg
4.1.3.3 irq_handler

irq_handler主要的工作就是获取两个参数:irq number和struct pt_regs *。然后调用asm_do_IRQ并将参数传递给它。

之后中断的处理工作就正式交接给C语言进行。

/* arch/arm/kernel/entry_armv.S */
/*
 * Interrupt handling.  Preserves r7, r8, r9
 */
	.macro	irq_handler
	get_irqnr_preamble r5, lr
1:	get_irqnr_and_base r0, r6, r5, lr /*从芯片的中断寄存器中获取中断号并存放在r0中,并依据IRQ_EINT0做了偏移校正,保证第一个中断号和IRQ_EINT0相等*/
	movne	r1, sp
	@
	@ routine called with r0 = irq number, r1 = struct pt_regs *
	@
	adrne	lr, 1b
	bne	asm_do_IRQ

4.2 C语言部分

4.2.1 asm_do_IRQ

/* arch/arm/kernel/irq.c */
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);

desc_handle_irq源码如下:

static inline void desc_handle_irq(unsigned int irq, struct irq_desc *desc)

	desc->handle_irq(irq, desc);

其中desc->handle_irq是一个函数指针(中断框架前面的文章中分析过),它的初始化在setup_arch中完成。

4.2.2 handle_IRQ_event

desc->handle_irq指向的中断处理函数又调用了handle_IRQ_event函数来调用用户注册的中断服务例程。

irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)

	irqreturn_t ret, retval = IRQ_NONE;
	unsigned int status = 0;

	handle_dynamic_tick(action);

	if (!(action->flags & IRQF_DISABLED))
		local_irq_enable_in_hardirq();

	do /*遍历用户注册的中断服务例程*/
		ret = action->handler(irq, action->dev_id);
		if (ret == IRQ_HANDLED)
			status |= action->flags;
		retval |= ret;
		action = action->next;
	 while (action);

	if (status & IRQF_SAMPLE_RANDOM)
		add_interrupt_randomness(irq);
	local_irq_disable();

	return retval;

至此,对中断处理在C语言部分的处理流程再做一个梳理:

  1. 根据中断号irq找到对应的中断描述符desc。
  2. 调用desc->handle_irq函数指针,该指针初始化在setup_arch函数中完成。
  3. 调用handle_IRQ_event函数,并在该函数中对用户注册的中断例程进行遍历执行。

恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~

以上是关于一起分析Linux系统设计思想——05中断框架剖析的主要内容,如果未能解决你的问题,请参考以下文章

一起分析Linux系统设计思想——05中断框架剖析

一起分析Linux系统设计思想——05中断框架剖析

一起分析Linux系统设计思想——05中断框架剖析

一起分析Linux系统设计思想——05中断框架剖析

一起分析Linux系统设计思想——05内核定时器的使用

一起分析Linux系统设计思想——05内核定时器的使用