一起分析Linux系统设计思想——05中断框架剖析
Posted 穿越临界点
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一起分析Linux系统设计思想——05中断框架剖析相关的知识,希望对你有一定的参考价值。
在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的无效信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。
文章目录
中断从哪里来,到哪里去?中断号是如何分配的?带着这些疑问一起探索吧~
4 中断处理流程
前面的内容涉及的都是中断的准备工作,本节才真正打通从硬件中断产生到中断处理的整个流程。
4.1 汇编部分
汇编部分的代码主要负责和处理器架构相关的部分,例如中断向量的选择、中断现场保护和恢复等。
4.1.1 中断向量重映射
我们以 trap_init 函数作为突破口,来按图索骥地找到中断向量的汇编代码。
能够想到从trap_init函数作为突破口,基于下面两点:
- 中断可以看作陷阱的一种。
- 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语言部分的处理流程再做一个梳理:
- 根据中断号irq找到对应的中断描述符desc。
- 调用desc->handle_irq函数指针,该指针初始化在setup_arch函数中完成。
- 调用handle_IRQ_event函数,并在该函数中对用户注册的中断例程进行遍历执行。
恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~
以上是关于一起分析Linux系统设计思想——05中断框架剖析的主要内容,如果未能解决你的问题,请参考以下文章