1.中断介绍
中断是指CPU在正常执行指令过程中,出现了突发且紧急的事件,CPU必须暂时停止执行当前的程序,跳转到处理突发事件的指令处执行,处理完毕后返回到原来的指令处继续执行。根据中断的来源,中断可分为内部中断和外部中断,内部中断来源于CPU内部,如软件中断指令、除法错误、溢出等,外部中断来自于CPU外部,由外设产生,如串口收到数据产生中断、定时器到期产生中断。根据中断入口跳转方法可分为向量中断和非向量中断,CPU通常为向量中断分配不通的中断号,中断到来后,就自动跳转到该中断号对应的地址处执行,非向量中断共享一个入口地址,进入该地址后,通过判断中断标志确定中断源。
2.Linux中断框架介绍
中断服务程序是在关中断的状态下执行,如果中断服务程序执行的时间过长,会使其他进程得不到调度执行,系统不能响应其他中断,导致系统的实时性和性能降低。为了解决这个问题,Linux将中断服务程序分为两部分,即顶半部分(Top Half)和底半部分(Bottom Half)。顶半部分是在关中断的状态下进行,此阶段不能响应中断,顶半部分用于完成尽量短且比较紧急的工作,如获取中断状态、登记中断、清除中断状态,登记的中断在底半部分处理。底半部分是在开中断的状态下进行,此阶段可以响应中断,底半部分用来处理顶半部分登记的中断,主要是一些复杂且耗时的工作,如拷贝数据、解析数据等工作。当然,中断处理是否一定要分为顶半部分和底半部分,需要根据实际的中断任务确定,对于一些简单的任务,可以直接在顶半部分完成。
Linux内核支持众多的处理器体系结构,中断子系统非常复杂,但根据其功能,可大致分为四部分:
(1)硬件层,例如CPU和中断控制器的连接,硬件中断号,中断触发方式等
(2)处理器的异常处理模式,例如CPU如何处理不同异常(中断也是一种异常),如何进入异常和如何退出异常
(3)中断控制器的管理,例如硬件中断号和软件中断号的映射,中断控制器的操作
(4)Linux内核通用中断处理层,此部分屏蔽了硬件之间的差异,提供了通用的中断处理的框架,向上提供中断处理的统一接口,如注册和注销中断等,向下兼容所有体系结构。
3.ARM处理器异常处理模式
ARMv7-A and ARMv7-R架构支持9种处理器运行模式,其中6种为特权模式,1种为非特权模式,剩下2种和虚拟化、安全相关。特权模式分别为FIQ(快速中断)、IRQ(中断)、Supervisor、Abort、Undefined、System,操作系统通常运行在Supervisor模式下。非特权模式为User mode,即用户模式,应用程序就在此模式下运行。特权模式中除了System,其他模式又称为异常模式,中断也属于异常的一种。ARM架构处理器运行模式可参考《Cortex-A Series Programmer’s Guide》第11节。
ARM处理器对中断的响应过程如下:
(1)CPU自动保存处理器状态,即将中断发生时的CPSR寄存器内容保存到SPSR_irq寄存器中
(2)CPU自动设置当前程序状态寄存器CPSR的低5位,使处理器进入特权模式中的IRQ模式(Linux不使用FIQ,只使用IRQ)
(3)硬件自动关闭IRQ中断
(4)将返回地址(PC)自动保存到LR_irq寄存器中
(5)CPU自动的将程序计数器PC设置成异常中断向量表中的地址,进入相应的异常处理程序中处理中断
(6)处理完中断后,恢复中断发生前的处理器状态,即将SPSR_irq寄存器中的数据复制到CPSR寄存器中
(7)设置程序计数器PC,使其指向中断发生前要执行的指令,即将LR_irq寄存器中的数据复制到PC中
异常处理程序的入口地址保存在中断向量表中,如下图所示。中断向量表正常情况下位于0x0-0x1C地址处,处于内存中的低地址。也有可能位于高地址处,即0xFFFF0000-0xFFFF001C处,Linux中断向量表通常位于内存的高地址处,因为0-3G为用户空间,3-4G为内核空间,中断向量表属于内核空间。我们关心的IRQ中断向量的地址为0x1C或者0xFFFF001C。需要注意的是上述地址都为虚拟地址。
arm处理器内部的寄存器数量相比x86架构的处理器要多一些,某些cpu模式还拥有专用的寄存器,arm处理器寄存器和处理器模式的对应关系如下图所示。所有模式共用PC和CPSR;除了FIQ模式,所有模式共用R0-R12寄存器,专用的寄存器有SP、LR及SPSR;FIQ模式下有专用的R8-R12寄存器。
4.Linux内核底层中断处理源代码分析
此节侧重分析底层的中断处理机制,如中断向量表的作用、中断是如何跳转到中断处理程序的入口处、中断上下文的保存和恢复等。大部分代码都是汇编代码,学习之前,建议熟悉一下arm架构的汇编代码。
4.1.中断向量表
4.1.1.中断向量表的定义
Linux的中断向量表中存放的是一系列的跳转指令,用于跳转到异常处理程序的入口处。这里重点关注IRQ中断向量。vector_irq
是通过宏定义的标签,由vector_stub
定义而来。
[arch/arm/kernel/entry-armv.S]
.section .vectors, "ax", %progbits @ 说明中断向量表的段及属性,ax表示可分配、可执行
.L__vectors_start: @ 中断向量表开始
W(b) vector_rst @ 复位
W(b) vector_und @ 未定义
W(ldr) pc, .L__vectors_start + 0x1000 @ 特权指令(svc、swi)入口
W(b) vector_pabt @ prefetch abort
W(b) vector_dabt @ data abort
W(b) vector_addrexcptn @ 地址异常
W(b) vector_irq @ 中断
W(b) vector_fiq @ 快速中断
4.1.2.中断向量表stubs段
stubs段起到分发中断的作用,使不同的中断跳转到不同的处理程序中去,如中断发生在usr模式,将会跳转到__irq_usr
中处理,如中断发生在svc模式,将会跳转到__irq_svc
中处理。.macro
表示宏,vector_stub
是宏的名称,name
、mode
和correction
为参数。correction
的默认值为4。将vector_stub irq, IRQ_MODE, 4
宏定义展开,则name
为irq
,mode
为0x00000012
,correction
为4
。
[arch/arm/kernel/entry-armv.S]
.macro vector_stub, name, mode, correction=0 @ 宏定义开始
.align 5
vector_\name: @ \表示使用宏的参数,替换后得到vector_irq,可见中断向量表中的irq跳转指令是跳转到此处
.if \correction @ correction为4,条件成立
@ 将lr寄存器数据减4并保存到lr中,lr中保存的是中断返回的地址,这里减4是由于流水线的原因,发生中断的
@ 时候pc还未更新,指向了中断发生前执行指令后面的第二条指令,即中断发生前执行指令加8个字节的位置。中
@ 断返回后要执行中断发生前执行指令后面的第一条指令,则需要将lr减4。
sub lr, lr, #\correction
.endif
@ 将r0和lr保存到irq模式的栈中,lr是中断返回的地址
stmia sp, {r0, lr} @ save r0, lr
@ 将spsr保存到lr中
mrs lr, spsr
@ 将spsr_irq保存到栈中,此时的spsr为发生中断时的cpsr
str lr, [sp, #8] @ save spsr
@ 将cpsr保存到r0中
mrs r0, cpsr
@ 将r0最后一位设置为1,0x12为irq模式,0x13为svc模式
eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
@ 将r0保存到spsr中,此时spsr中的低5位为0x13,表示svc模式,此时处理器还处于irq模式,还未切换到svc模式
msr spsr_cxsf, r0
@ 获取中断发生时cpsr寄存器的低4位,如果低4位为0表示中断发生时cpu处于usr模式(用户空间),
@ 如低4位为3,则表示中断发生时cpu处于svc模式(内核空间)
and lr, lr, #0x0f
@ 获取标号1的地址,此地址为相对地址,是当前pc的偏移地址
THUMB( adr r0, 1f )
@ 将表号1的地址和cpsr低四位相加,得到跳转的相对偏移地址,中断发生在usr模式则跳转到__irq_usr,中断发生在svc模式,
@ 则跳转到__irq_svc
THUMB( ldr lr, [r0, lr, lsl #2] ) @ lsl为左移指令,左移两位,保证地址4字节对齐
mov r0, sp @ 栈指针保存在r0中
@ 将偏移地址和pc相加存放到lr寄存器中
ARM( ldr lr, [pc, lr, lsl #2] )
@ 将lr中的地址复制到pc中,跳转到__irq_usr或__irq_svc处,同时将spsr的值复制到cpsr,将处理器模式切换到svc模式
movs pc, lr @ branch to handler in SVC mode
ENDPROC(vector_\name)
.align 2
@ handler addresses follow this label
1:
.endm @ 宏定义结束
.section .stubs, "ax", %progbits @ stubs段属性
vector_stub irq, IRQ_MODE, 4 @ irq中断服务程序的跳转入口
@ 当中断发生在usr模式时,跳转到__irq_usr处理中断
.long __irq_usr @ 0 (USR_26 / USR_32)
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
@ 当中断发生在svc模式时,跳转到__irq_svc处理中断
.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中被处理,当中断irq发生在svc模式时,中断在__irq_svc中被处理。中断发生后,CPU将很快从irq模式切换到svc模式,处于irq模式的时间很短。cpu在irq模式中将中断发生时的r0寄存器、lr寄存器、cpsr寄存器保存到了irq模式的栈中,后续cpu切换到svc模式,需要读取这三个值,irq模式的栈地址通过r0寄存器传递到svc模式。
4.1.3.中断向量表的初始化
内核编译时,将中断向量表存放在镜像文件的init段中,具体内容可见vmlinux.lds.S链接脚本,内容如下。__vectors_start和__vectors_end是中断向量表开始、结束的地址,中断向量表开始的地址为0xffff0000。__stubs_start和__stubs_end是中断向量stubs段开始地址和结束地址,__stubs_start的地址位于__vectors_start加0x1000处,也即向上偏移一个page处。可以看出,中断向量表和中断向量stubs段各占一个page,这样安排的好处是在中断向量表中可以使用分支跳转指令。
[arch/arm/kernel/vmlinux.lds.S]
// 下面按PAGE_SIZE对齐
. = ALIGN(PAGE_SIZE);
// 表示init段开始
__init_begin = .;
/*
* The vectors and stubs are relocatable code, and the
* only thing that matters is their relative offsets
*/
__vectors_start = .; // 中断向量表开始存放,开始地址为0xffff0000
.vectors 0xffff0000 : AT(__vectors_start) {
*(.vectors)
}
. = __vectors_start + SIZEOF(.vectors);
__vectors_end = .; // 中断向量表结束
__stubs_start = .; // 中断向量stubs段开始存放,开始地址为0xffff1000
.stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) {
*(.stubs)
}
. = __stubs_start + SIZEOF(.stubs);
__stubs_end = . // 中断向量stubs段结束
中断向量表的内存分配及初始化在devicemaps_init函数中,下面分析该函数。
[arch/arm/mm/mmu.c]
paging_init // 内核启动期间调用,用来初始化页表
->devicemaps_init
->early_alloc(PAGE_SIZE * 2) // 分配两个页表
->early_trap_init
vectors_page = vectors_base // 全局变量vectors_page保存分配的页表地址
// 第一个页表填充未定义指令0xe7fddef1,作用是当cpu从中断向量表外取指令时,可以捕捉未定义指令异常
for (i = 0; i < PAGE_SIZE / sizeof(u32); i++)
((u32 *)vectors_base)[i] = 0xe7fddef1
// 将中断向量表拷贝到分配的第一个页表中
->memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start)
// 将中断向量表stubs段拷贝到分配的第二个页表中
->memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start)
// 将中断向量表的开始地址转换为页表的编号,virt_to_phys将vectors转换为物理地址
map.pfn = __phys_to_pfn(virt_to_phys(vectors))
// 设置中断向量表映射的虚拟地址为0xffff0000
map.virtual = 0xffff0000
// 设置中断向量表映射的虚拟地址大小为一页
map.length = PAGE_SIZE
// 映射中断向量表
->create_mapping
// 如果需要映射到低地址处
if (!vectors_high()) {
map.virtual = 0;
map.length = PAGE_SIZE * 2;
map.type = MT_LOW_VECTORS;
create_mapping(&map);
}
// 映射中断向量表stubs段
map.pfn += 1;
map.virtual = 0xffff0000 + PAGE_SIZE;
map.length = PAGE_SIZE;
map.type = MT_LOW_VECTORS;
->create_mapping
4.3.异常栈的初始化
cpu在初始化的时候,需要初始化异常栈,即cpu切换到irq、abt、und、fiq
模式下需要使用的栈。上述4中异常模式的栈来自于静态定义的struct stack
结构体,每种异常的栈的大小为12字节。异常栈的初始化在cpu_init
函数中,将栈的地址保存到不同异常模式下的sp寄存器中。
[arch/arm/kernel/setup.c]
static struct stack stacks[NR_CPUS];
struct stack {
u32 irq[3]; // irq模式的栈
u32 abt[3]; // abt模式的栈
u32 und[3]; // und模式的栈
u32 fiq[3]; // fiq模式的栈
} ____cacheline_aligned;
cpu_init
->smp_processor_id // 获取当前cpu的ID
->struct stack *stk = &stacks[cpu] // 获取当前cpu的irq、abt、und、fiq模式下的栈指针
// 设置当前cpu的per_cpu变量的偏移值,per_cpu_offset获取当前cpu的per_cpu变量的偏移值
->set_my_cpu_offset(per_cpu_offset(cpu))
// 使用mcr指令将偏移值保存到TPIDRPRW寄存器中,TPIDRPRW的偏移值会一直保持,可加快per_cpu变量的访问速度
asm volatile("mcr p15, 0, %0, c13, c0, 4" : : "r" (off) : "memory");
// 下面开始设置栈,非THUMB指令,PLC被定义为"I",表示常数,cpsr_c表示CPSR寄存器的低8位
__asm__ (
"msr cpsr_c, %1\n\t" @ 进入irq模式,同时屏蔽irq和fiq中断
"add r14, %0, %2\n\t" @ 将irq数组的首地址保存到r14中
"mov sp, r14\n\t" @ 设置irq模式下的栈指针指向struct stack结构体中的irq数组
"msr cpsr_c, %3\n\t" @ 进入abt模式,同时屏蔽irq和fiq中断
"add r14, %0, %4\n\t" @ 将abt数组的首地址保存到r14中
"mov sp, r14\n\t" @ 设置abt模式下的栈指针指向struct stack结构体中的abt数组
"msr cpsr_c, %5\n\t" @ 进入und模式,同时屏蔽irq和fiq中断
"add r14, %0, %6\n\t" @ 将und数组的首地址保存到r14中
"mov sp, r14\n\t" @ 设置und模式下的栈指针指向struct stack结构体中的und数组
"msr cpsr_c, %7\n\t" @ 进入fiq模式,同时屏蔽irq和fiq中断
"add r14, %0, %8\n\t" @ 将fiq数组的首地址保存到r14中
"mov sp, r14\n\t" @ 设置fiq模式下的栈指针指向struct stack结构体中的und数组
"msr cpsr_c, %9" @ 将cpu的模式恢复为svc模式,同时屏蔽irq和fiq中断
: @ 输出部
: "r" (stk), @ 输入部,"r"表示任何寄存器, %0
@ (0x00000040 | 0x00000080 | 0x00000012)= 0x000000D2,中断模式,屏蔽irq和fiq
PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE), @ %1
"I" (offsetof(struct stack, irq[0])), @ %2 获取struct stack中irq数组的首地址
PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE), @ %3
"I" (offsetof(struct stack, abt[0])), @ %4
PLC (PSR_F_BIT | PSR_I_BIT | UND_MODE), @ %5
"I" (offsetof(struct stack, und[0])), @ %6
PLC (PSR_F_BIT | PSR_I_BIT | FIQ_MODE), @ %7
"I" (offsetof(struct stack, fiq[0])), @ %8
PLC (PSR_F_BIT | PSR_I_BIT | SVC_MODE) @ %9
: "r14"); @ 损坏部
4.3.底层的中断的处理过程
Linux对irq中断的处理根据中断发生时cpu所处的模式有所不同,中断发生时cpu处于usr模式时,会跳转到__irq_usr
,中断发生时cpu处于svc模式时,会跳转到__irq_svc
。__irq_usr
和__irq_svc
都是跳转到irq_handler
处理中断,只有进入中断和退出中断部分有所不同。
首先看__irq_usr
。usr_entry
将中断上下文保存到当前进程的内核栈中;irq_handler
是真正的中断处理函数;处理完中断会检查当前进程是否有挂起的工作要做,如有则会执行;ret_to_user_from_irq
恢复中断上下文,退出中断。
[arch/arm/kernel/entry-armv.S]
@ 当中断发生在usr模式时,跳转到__irq_usr处理中断
__irq_usr:
usr_entry @ 进入中断,完成中断上下文的保存
kuser_cmpxchg_check
irq_handler @处理中断
get_thread_info tsk @ 获取线程的thread_info结构体地址,并将地址保存到r9中
mov why, #0 @ why为r8寄存器
b ret_to_user_from_irq @ 退出中断,将中断上下文恢复
ENDPROC(__irq_usr) @ __irq_usr标号作用域结束
get_thread_info tsk
通过将sp的低13位清0获取被中断线程的thread_info
结构体地址,并将地址保存到r9寄存器中。
[include/linux/sched.h]
// thread_info和线程的栈定义在一个联合体中,总共8KB,栈在8KB的高地址处,thread_info在8KB的低地址处
union thread_union {
struct thread_info thread_info; // 线程信息,通过此结构体可找到该线程的task_struct和栈地址
unsigned long stack[THREAD_SIZE/sizeof(long)]; // 栈
};
[arch/arm/kernel/entry-header.S]
why .req r8 @ Linux syscall (!= 0),why为r8
tsk .req r9 @ current thread_info,tsk为r9,
[arch/arm/include/asm/thread_info.h]
#define THREAD_SIZE_ORDER 1
[arch/arm/include/asm/page.h]
#define PAGE_SHIFT 12
[arch/arm/include/asm/assembler.h]
.macro get_thread_info, rd @ get_thread_info为宏定义,下面为获取thread_info结构体地址的代码
@ lsr逻辑右移,高位补0,sp右移13位并将结果保存到r9寄存器中
ARM( mov \rd, sp, lsr #THREAD_SIZE_ORDER + PAGE_SHIFT )
@ lsl逻辑左移,低位补0,将r9左移13位,低位补0,这样就把sp的低13位清0,2的13次方为8KB,正好获得了
@ thread_union的基地址,也即thread_info的地址
mov \rd, \rd, lsl #THREAD_SIZE_ORDER + PAGE_SHIFT
.endm
接着看__irq_svc
。svc_entry
将中断上下文保存到内核栈中;irq_handler
是真正的中断处理函数;如果开启了内核抢占,则会检查当前内核是否可以被抢占,如果可以被抢占,则会执行抢占;svc_exit
恢复中断上下文,从中断中退出。
[arch/arm/kernel/entry-armv.S]
@ 当中断发生在svc模式时,跳转到__irq_svc处理中断
__irq_svc:
svc_entry
irq_handler
#ifdef CONFIG_PREEMPT
get_thread_info tsk @ 获取线程的thread_info结构体地址,并将地址保存到r9中
@ 将thread_info结构体中preempt_count变量保存到r8中
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
@ 将thread_info结构体中flags变量保存到r0中
ldr r0, [tsk, #TI_FLAGS] @ get flags
@ 测试preempt_count是否为0,preempt_count为0可以抢占,preempt_count不为0,不可以抢占
teq r8, #0 @ if preempt count != 0
@ 如果preempt_count不为0,则将r0清0,不会执行svc_preempt进行抢占调度
movne r0, #0 @ force flags to 0
@ 将r0与_TIF_NEED_RESCHED按位与
tst r0, #_TIF_NEED_RESCHED
@ 按位与的结果不为0,则执行svc_preempt,执行抢占调度
blne svc_preempt
#endif
svc_exit r5, irq = 1 @ return from exception
ENDPROC(__irq_svc) @ __irq_svc标号作用域结束
4.3.1.进入中断
首先分析__irq_usr
的中断进入代码,usr_entry
是一个宏,定义如下。内核定义了struct pt_regs
结构体,用来描述内核保存中断上下文cpu寄存器的排列信息,struct pt_regs
内部包含了一个长度为18的数组,其数组元素与cpu寄存器的对应关系由宏定义定义。如r0、r1、r2
等寄存器保存到ARM_r0、ARM_r1、ARM_r2
中,fp、ip、sp、lr、pc、cpsr
寄存器保存到ARM_fp、ARM_ip、ARM_sp、ARM_lr、ARM_pc、ARM_cpsr
中。首先将r1-r12寄存器保存到svc模式的栈中,然后将irq模式栈中的数据读出并保存到svc模式的栈中。最后将usr模式下sp、lr寄存器保存到svc栈ARM_lr和ARM_sp位置。
[arch/arm/include/uapi/asm/ptrace.h]
struct pt_regs {
long uregs[18];
};
#define ARM_cpsr uregs[16]
#define ARM_pc uregs[15]
#define ARM_lr uregs[14]
#define ARM_sp uregs[13]
#define ARM_ip uregs[12]
#define ARM_fp uregs[11]
#define ARM_r10 uregs[10]
#define ARM_r9 uregs[9]
#define ARM_r8 uregs[8]
#define ARM_r7 uregs[7]
#define ARM_r6 uregs[6]
#define ARM_r5 uregs[5]
#define ARM_r4 uregs[4]
#define ARM_r3 uregs[3]
#define ARM_r2 uregs[2]
#define ARM_r1 uregs[1]
#define ARM_r0 uregs[0]
#define ARM_ORIG_r0 uregs[17]
首先将sp减去S_FRAME_SIZE
,S_FRAME_SIZE
为struct pt_regs
结构体的大小。然后将发生中断时的r1-r12寄存器保存到svc模式的栈中,然后再保存r0、lr和cpsr到svc模式的栈中,lr被保存到了ARM_pc中,用于中断返回时恢复pc寄存器。
[arch/arm/kernel/entry-armv.S]
.macro usr_entry, trace=1, uaccess=1
sub sp, sp, #S_FRAME_SIZE @ 将sp减去S_FRAME_SIZE,留出保存中断上下文的空间
@ 将中断时刻的寄存器r1-r12保存到svc模式的栈中,对应关系为r0-ARM_r1,r2-ARM_r2...,r12-ARM_ip
ARM( stmib sp, {r1 - r12} )
@ 将r0指向的内存地址的三个值加载到r3、r4和r5中,r0中保存的是irq模式下栈的地址,irq模式栈中保存了中断发生时的
@ r0寄存器、中断返回地址lr及中断发生时的cpsr。对应关系为:r3-r0、r4-lr、r5-cpsr
ldmia r0, {r3 - r5}
add r0, sp, #S_PC @ r0为uregs[15]的地址,S_PC为ARM_pc即uregs[15]相对pt_regs结构体开始地址的偏移值
mov r6, #-1 @ r6 = -1
str r3, [sp] @ 将中断发生时的r0寄存器保存到svc模式的栈的ARM_r0位置
@ 将中断发生时的lr、cpsr保存到svc模式的栈中,对应关系为r4-lr-ARM_pc、r5-cpsr-ARM_cpsr、r6-ARM_ORIG_r0
@ arm处理器使用寄存器传递参数遵循ATPCS规则,使用r0-r3传递参数,多余的参数通过栈传递,系统调用也使用r0-r3
@ 传递参数,r0传递的是系统调用号,返回值也通过r0传递,这样可能会把原来的r0覆盖,因此这里会将r0拷贝两份,
@ ARM_ORIG_r0保存原来的参数,ARM_r0保存返回值,由于中断不需要传递参数,因此这里ARM_ORIG_r0保存为-1,
@ 表示非系统调用号
stmia r0, {r4 - r6}
@ 将usr模式下sp、lr寄存器保存到svc栈ARM_lr和ARM_sp位置,
ARM( stmdb r0, {sp, lr}^ )
.endm @ 宏定义结束
接着分析__irq_svc
的中断进入代码,svc_entry
中保存的寄存器顺序也是按照struct pt_regs
排列。如果定义了CONFIG_AEABI
并且__LINUX_ARM_ARCH__ >= 5
,则有定义#define SPFIX(code...) code
,否则SPFIX
定义为空,此定义和ABI接口有关,arm cortex-A9支持EABI,一般都定义了此选项。首先保存r1-r2寄存器,接着将irq栈中的数据保存到svc模式的栈中,最后将中断发生时(svc模式)的sp、lr寄存器保存到栈中。
.macro svc_entry, stack_hole=0, trace=1, uaccess=1
@ sp=sp-S_FRAME_SIZE-4,预留S_FRAME_SIZE+4的空间保存中断上下文
sub sp, sp, #(S_FRAME_SIZE + 8 + \stack_hole - 4)
@ 将sp与4按位与,判断栈是否对齐,tst指令的结果影响cpsr条件标志位Z,tst结果为0,则Z=1,反之Z=0
SPFIX( tst sp, #4 )
@ 如果将sp与4按位与的结果等于0,则将sp减去4并保存到sp中
@ sp=sp-4
SPFIX( subeq sp, sp, #4 )
@ 将r1-r12保存到栈中
stmia sp, {r1 - r12}
@ r0为irq模式中的栈地址,将irq模式栈中的数据分别加载到r3-r5中,对应关系为r3-r0、r4-lr、r5-cpsr
@ r0、lr、cpsr为中断发生时的寄存器值
ldmia r0, {r3 - r5}
@ r7指向ARM_sp
add r7, sp, #S_SP - 4
@ r6为-1
mov r6, #-1
@ r2保存了未保存中断上下文信息时的svc栈的地址,即刚进入svc模式时栈的地址
add r2, sp, #(S_FRAME_SIZE + 8 + \stack_hole - 4)
@ 如果tst的结果为0,则r2=r2+4
SPFIX( addeq r2, r2, #4 )
@ 将r0保存到svc模式的栈中
str r3, [sp, #-4]! @ save the "real" r0 copied from the exception stack
@ r3中保存的是svc的lr寄存器
mov r3, lr
@ We are now ready to fill in the remaining blanks on the stack:
@ r2 - sp_svc
@ r3 - lr_svc 中断发生后,cpu会从irq模式切换到svc模式,此lr为svc返回到irq模式的地址
@ r4 - lr_<exception>, 中断发生的svc模式返回地址
@ r5 - spsr_<exception>
@ r6 - orig_r0 (see pt_regs definition in ptrace.h)
stmia r7, {r2 - r6}
.endm
4.3.2.处理中断
__irq_usr
和__irq_svc
都是跳转到irq_handler
中处理中断。irq_handler
是一个宏,根据是否配置CONFIG_MULTI_IRQ_HANDLER
选项有不同的处理方法,CONFIG_MULTI_IRQ_HANDLER
配置允许每个机器在运行时指定irq中断服务函数。可适用于多种平台及多种中断控制器。如果没有配置,则默认使用arch_irq_handler_default
处理中断。目前arm处理器中断越来越多,一个中断控制器已经无法满足需求,很多处理器采用了中断控制器级联的方式。因此handle_arch_irq
方法要比arch_irq_handler_default
方法灵活很多。后续会侧重分析handle_arch_irq
方法。
[arch/arm/kernel/entry-armv.S]
.macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq @ 获取跳转函数的地址
mov r0, sp @ 将svc模式的栈地址保存到r0中,便于参数传递
badr lr, 9997f @ 设置返回地址
ldr pc, [r1] @ 跳转到handle_arch_irq执行,r0保存了传递的参数
#else
arch_irq_handler_default @ 默认的中断处理函数
#endif
9997:
.endm
[arch/arm/include/asm/irq.h]
#ifdef CONFIG_MULTI_IRQ_HANDLER
extern void (*handle_arch_irq)(struct pt_regs *); @ handle_arch_irq是一个函数指针
@ 可使用set_handle_irq设置handle_arch_irq
extern void set_handle_irq(void (*handle_irq)(struct pt_regs *));
#endif
handle_arch_irq
在系统启动期间初始化,后续再介绍。arch_irq_handler_default
是默认的中断处理函数,也是一个宏,首先使用get_irqnr_and_base
获取中断号、中断状态寄存器的值、中断控制器的基地址,
[arch/arm/include/asm/entry-macro-multi.S]
.macro arch_irq_handler_default
@ 获取中断信息,r0-保存中断号,r2-中断状态寄存器,r6-中断控制器基地址
1: get_irqnr_and_base r0, r2, r6, lr
movne r1, sp @ 将r1设置为svc模式的栈地址,便于后续参数传递
@ routine called with r0 = irq number, r1 = struct pt_regs *
badrne lr, 1b @ 设置函数返回地址
bne asm_do_IRQ @ 跳转到asm_do_IRQ中处理中断,传递的参数为r0和r1
#ifdef CONFIG_SMP @ 如果是SMP系统,会检测是否是ppi中断,如果是会处理
ALT_SMP(test_for_ipi r0, r2, r6, lr)
ALT_UP_B(9997f)
movne r1, sp @ 将r1设置为svc模式的栈地址,便于后续参数传递
badrne lr, 1b @ 设置函数返回地址
bne do_IPI @ 处理ppi中断
#endif
9997:
.endm
4.3.3.退出中断
首先分析__irq_usr
中断退出代码,执行ret_to_user_from_irq
标号处的代码,表示中断要退出了,退出中断之前会检查是否还有工作要做,如是否需要重新调度,是否有信号挂起,如有则跳转到slow_work_pending
中处理,如没有则继续执行退出中断的流程。arch_ret_to_user
和架构相关,如果定义CONFIG_NEED_RET_TO_USER
,则在退出中断之前会执行arch_ret_to_user
。ct_user_enter
和上下文追踪有关,如果定义CONFIG_CONTEXT_TRACKING
,则会被执行。中断退出流程最终会执行restore_user_regs
,从名称上可以看出是恢复usr模式的寄存器信息,恢复寄存器信息后,中断就返回了。
[arch/arm/kernel/asm-offsets.c]
// 获取flags相对thread_info结构体的偏移地址
DEFINE(TI_FLAGS, offsetof(struct thread_info, flags));
[arch/arm/include/asm/thread_info.h]
struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
__u32 cpu; /* cpu */
__u32 cpu_domain; /* cpu domain */
struct cpu_context_save cpu_context; /* cpu context */
__u32 syscall; /* syscall number */
__u8 used_cp[16]; /* thread used copro */
unsigned long tp_value[2]; /* TLS registers */
#ifdef CONFIG_CRUNCH
struct crunch_state crunchstate;
#endif
union fp_state fpstate __attribute__((aligned(8)));
union vfp_state vfpstate;
#ifdef CONFIG_ARM_THUMBEE
unsigned long thumbee_state; /* ThumbEE Handler Base register */
#endif
};
// 退出中断要检查的标志,_TIF_NEED_RESCHED-是否要重新调度,_TIF_SIGPENDING-是否有信号挂起
// _TIF_NOTIFY_RESUME-返回用户空间之前是否要调用回调函数,_TIF_UPROBE-断点或单步操作
#define _TIF_WORK_MASK (_TIF_NEED_RESCHED | _TIF_SIGPENDING | \
_TIF_NOTIFY_RESUME | _TIF_UPROBE)
[arch/arm/kernel/entry-common.S]
ENTRY(ret_to_user_from_irq) @ ret_to_user_from_irq标记开始
ldr r1, [tsk, #TI_FLAGS] @ 获取被中断线程thread_info结构体中的flags成员的值
@ 将r1与_TIF_WORK_MASK按位与,结果为0则cpsr的条件标志位Z=1,结果不为0,则Z=0
tst r1, #_TIF_WORK_MASK
@ eq,z=1,ne,z=0
bne slow_work_pending @ 测试按位与的结果是否0,不为0则跳转到slow_work_pending处理
no_work_pending:
asm_trace_hardirqs_on save = 0 @ 中断追踪相关,可
arch_ret_to_user r1, lr @ 架构相关
ct_user_enter save = 0 @ 上下文追踪相关
restore_user_regs fast = 0, offset = 0 @ 恢复中断上下文
ENDPROC(ret_to_user_from_irq) @ ret_to_user_from_irq标记结束
下面具体分析一下restore_user_regs
是怎么恢复中断上下文的,
.macro restore_user_regs, fast = 0, offset = 0
@ ARM mode restore
mov r2, sp @ 将svc模式的sp栈地址保存到r2中
@ 将usr模式的cpsr从svc模式的栈中取出并保存到r1中
ldr r1, [r2, #\offset + S_PSR] @ get calling cpsr
@ 将usr模式的pc从svc模式的栈中取出并保存到lr中
ldr lr, [r2, #\offset + S_PC]! @ get pc
@ 将usr模式的cpsr保存到svc模式的spsr中
msr spsr_cxsf, r1 @ save in spsr_svc
@ 将svc模式栈中保存的寄存器信息恢复到usr模式的寄存器中,恢复的寄存器有r0、r1、r2...sp、lr
@ 恢复的顺序为r2+ARM_lr-lr、r2+ARM_sp-sp...r2-r0,r2为svc模式下的栈地址
ldmdb r2, {r0 - lr}^ @ get calling r0 - lr
@ 栈中的中断上下文信息已经恢复了,需要释放栈空间,将sp上移,释放pt_regs占用的空间
add sp, sp, #\offset + S_FRAME_SIZE
@ 将中断返回地址lr恢复到pc中,同时将spsr_svc恢复到cpsr,这里就完成了中断返回
movs pc, lr @ return & move spsr_svc into cpsr
.endm
接着分析__irq_svc
中断退出代码,执行到svc_exit
表示中断要退出了。
[[arch/arm/kernel/entry-header.S]]
.macro svc_exit, rpsr, irq = 0
@ ARM mode SVC restore
@ 将中断发生时的cpsr保存到spsr_svc中
msr spsr_cxsf, \rpsr
@ 恢复中断上下文,同时将spsr_svc恢复到cpsr中
ldmia sp, {r0 - pc}^ @ load r0 - pc, cpsr
.endm
5.总结
经过以上的分析,可以将Linux内核底层中断处理流程总结如下:
vector_irq 中断向量表入口
->__irq_usr 中断发生在usr模式
->usr_entry 保存中断上下文
->irq_handler 处理中断
->handle_arch_irq 可设置的中断处理函数,需要定义CONFIG_MULTI_IRQ_HANDLER选项
->arch_irq_handler_default 默认的中断处理函数
->ret_to_user_from_irq 退出中断
->slow_work_pending 退出中断前会检查是否需要重新调度、是否有信号挂起等,如有则会执行
->__irq_svc 中断发生在svc模式
->svc_entry 保存中断上下文
->irq_handler 处理中断
->handle_arch_irq 可设置的中断处理函数,需要定义CONFIG_MULTI_IRQ_HANDLER选项
->arch_irq_handler_default 默认的中断处理函数
->svc_preempt 如果定义CONFIG_PREEMPT选项,则退出中断前会检查内核是否可以抢占,
如可以则会执行抢占
->svc_exit 退出中断
从相关的书籍中,可以看到ARM处理器进入中断时会自动关闭CPU的中断,退出中断时自动打开中断的结论。但在分析中断进入和退出的时候并没有看到关闭中断和打开中断的代码。ARM CPU的中断关闭和打开通过设置CPSR寄存器的bit[7]和bit[6],bit[7]设置IRQ,bit[6]设置FIQ,bit[6]和bit[7]为1时,关闭对应的中断,为0时打开对应的中断。为了验证上述结果,因此在中断处理的通用函数gic_handle_irq
中,读取CPU的CPSR和SPSR寄存器,CPSR可以判断当前CPU的中断状态,SPSR可以判断中断退出后CPU的中断状态。测试代码如下图所示。
测试结果如下图所示。在中断处理的通用函数gic_handle_irq
中,CPSR的bit[7]始终为1,说明中断是关闭的,在这之前并没有关闭中断的代码,因此可以认为发生中断时CPU自动关闭了中断;SPSR的bit[7]始终为0,说明中断发生前中断是打开的,当中断退出时,将SPSR寄存器的数据恢复到CPSR中,实现了中断返回、CPU模式切换和打开CPU的中断,因此在中断退出时不需要额外的代码去打开中断。
参考资料:
- Linux kernel V4.6版本源码
- 《奔跑吧 Linux内核:基于Linux 4.x内核源代码问题分析》
- 《Linux内核源代码情景分析》
- 《ARM体系结构与编程》
- 《Linux设备驱动开发详解:基于最新的Linux 4.0内核》
- 《ARM® Architecture Reference Manual ARMv7-A and ARMv7-R edition》
- 《ARM® Cortex ™ -A Series Programmer’s Guide》
- http://www.wowotech.net/irq_subsystem/irq_handler.html