蜗窝科技

Linux kernel的中断子系统之(六):ARM中断处理过程

linuxer · · 中断子系统 · 阅读 109017 · 2014年8月存档

一、前言

本文主要以ARM体系结构下的中断处理为例,讲述整个中断处理过程中的硬件行为和软件动作。具体整个处理过程分成三个步骤来描述:

1、第二章描述了中断处理的准备过程

2、第三章描述了当发生中的时候,ARM硬件的行为

3、第四章描述了ARM的中断进入过程

4、第五章描述了ARM的中断退出过程

本文涉及的代码来自3.14内核。另外,本文注意描述ARM指令集的内容,有些source code为了简短一些,删除了THUMB相关的代码,除此之外,有些debug相关的内容也会删除。

二、中断处理的准备过程

1、中断模式的stack准备

ARM处理器有多种processor mode,例如user mode(用户空间的AP所处于的模式)、supervisor mode(即SVC mode,大部分的内核态代码都处于这种mode)、IRQ mode(发生中断后,处理器会切入到该mode)等。对于linux kernel,其中断处理处理过程中,ARM 处理器大部分都是处于SVC mode。但是,实际上产生中断的时候,ARM处理器实际上是进入IRQ mode,因此在进入真正的IRQ异常处理之前会有一小段IRQ mode的操作,之后会进入SVC mode进行真正的IRQ异常处理。由于IRQ mode只是一个过度,因此IRQ mode的栈很小,只有12个字节,具体如下:

struct stack {
u32 irq[3];
u32 abt[3];
u32 und[3];
} ____cacheline_aligned;

static struct stack stacks[NR_CPUS];

除了irq mode,linux kernel在处理abt mode(当发生data abort exception或者prefetch abort exception的时候进入的模式)和und mode(处理器遇到一个未定义的指令的时候进入的异常模式)的时候也是采用了相同的策略。也就是经过一个简短的abt或者und mode之后,stack切换到svc mode的栈上,这个栈就是发生异常那个时间点current thread的内核栈。anyway,在irq mode和svc mode之间总是需要一个stack保存数据,这就是中断模式的stack,系统初始化的时候,cpu_init函数中会进行中断模式stack的设定:

void notrace cpu_init(void)
{

unsigned int cpu = smp_processor_id();------获取CPU ID
struct stack *stk = &stacks[cpu];---------获取该CPU对于的irq abt和und的stack指针

……

#ifdef CONFIG_THUMB2_KERNEL
#define PLC “r”------Thumb-2下,msr指令不允许使用立即数,只能使用寄存器。
#else
#define PLC “I”
#endif

__asm__ (
“msr cpsr_c, %1\n\t”------让CPU进入IRQ mode
“add r14, %0, %2\n\t”------r14寄存器保存stk->irq
“mov sp, r14\n\t”--------设定IRQ mode的stack为stk->irq
“msr cpsr_c, %3\n\t”
“add r14, %0, %4\n\t”
“mov sp, r14\n\t”--------设定abt mode的stack为stk->abt
“msr cpsr_c, %5\n\t”
“add r14, %0, %6\n\t”
“mov sp, r14\n\t”--------设定und mode的stack为stk->und
“msr cpsr_c, %7”--------回到SVC mode
:--------------------上面是code,下面的output部分是空的
“r” (stk),----------------------对应上面代码中的%0
PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE),------对应上面代码中的%1
“I” (offsetof(struct stack, irq[0])),------------对应上面代码中的%2
PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE),------以此类推,下面不赘述
“I” (offsetof(struct stack, abt[0])),
PLC (PSR_F_BIT | PSR_I_BIT | UND_MODE),
“I” (offsetof(struct stack, und[0])),
PLC (PSR_F_BIT | PSR_I_BIT | SVC_MODE)
“r14”);--------上面是input操作数列表,r14是要clobbered register列表
}

嵌入式汇编的语法格式是:asm(code : output operand list : input operand list : clobber list);大家对着上面的code就可以分开各段内容了。在input operand list中,有两种限制符(constraint),“r"或者"I”,“I"表示立即数(Immediate operands),“r"表示用通用寄存器传递参数。clobber list中有一个r14,表示在汇编代码中修改了r14的值,这些信息是编译器需要的内容。

对于SMP,bootstrap CPU会在系统初始化的时候执行cpu_init函数,进行本CPU的irq、abt和und三种模式的内核栈的设定,具体调用序列是:start_kernel—>setup_arch—>setup_processor—>cpu_init。对于系统中其他的CPU,bootstrap CPU会在系统初始化的最后,对每一个online的CPU进行初始化,具体的调用序列是:start_kernel—>rest_init—>kernel_init—>kernel_init_freeable—>kernel_init_freeable—>smp_init—>cpu_up—>_cpu_up—>__cpu_up。__cpu_up函数是和CPU architecture相关的。对于ARM,其调用序列是__cpu_up—>boot_secondary—>smp_ops.smp_boot_secondary(SOC相关代码)—>secondary_startup—>__secondary_switched—>secondary_start_kernel—>cpu_init。

除了初始化,系统电源管理也需要irq、abt和und stack的设定。如果我们设定的电源管理状态在进入sleep的时候,CPU会丢失irq、abt和und stack point寄存器的值,那么在CPU resume的过程中,要调用cpu_init来重新设定这些值。

2、SVC模式的stack准备

我们经常说进程的用户空间和内核空间,对于一个应用程序而言,可以运行在用户空间,也可以通过系统调用进入内核空间。在用户空间,使用的是用户栈,也就是我们软件工程师编写用户空间程序的时候,保存局部变量的stack。陷入内核后,当然不能用用户栈了,这时候就需要使用到内核栈。所谓内核栈其实就是处于SVC mode时候使用的栈。

在linux最开始启动的时候,系统只有一个进程(更准确的说是kernel thread),就是PID等于0的那个进程,叫做swapper进程(或者叫做idle进程)。该进程的内核栈是静态定义的,如下:

union thread_union init_thread_union __init_task_data =
{ INIT_THREAD_INFO(init_task) };

union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};

对于ARM平台,THREAD_SIZE是8192个byte,因此占据两个page frame。随着初始化的进行,Linux kernel会创建若干的内核线程,而在进入用户空间后,user space的进程也会创建进程或者线程。Linux kernel在创建进程(包括用户进程和内核线程)的时候都会分配一个(或者两个,和配置相关)page frame,具体代码如下:

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
……

ti = alloc_thread_info_node(tsk, node);
if (!ti)
goto free_tsk;

……
}

底部是struct thread_info数据结构,顶部(高地址)就是该进程的内核栈。当进程切换的时候,整个硬件和软件的上下文都会进行切换,这里就包括了svc mode的sp寄存器的值被切换到调度算法选定的新的进程的内核栈上来。

3、异常向量表的准备

对于ARM处理器而言,当发生异常的时候,处理器会暂停当前指令的执行,保存现场,转而去执行对应的异常向量处的指令,当处理完该异常的时候,恢复现场,回到原来的那点去继续执行程序。系统所有的异常向量(共计8个)组成了异常向量表。向量表(vector table)的代码如下:

.section .vectors, “ax”, %progbits
__vectors_start:
W(b) vector_rst
W(b) vector_und
W(ldr) pc, __vectors_start + 0x1000
W(b) vector_pabt
W(b) vector_dabt
W(b) vector_addrexcptn
W(b) vector_irq —————————IRQ Vector
W(b) vector_fiq

对于本文而言,我们重点关注vector_irq这个exception vector。异常向量表可能被安放在两个位置上:

(1)异常向量表位于0x0的地址。这种设置叫做Normal vectors或者Low vectors。

(2)异常向量表位于0xffff0000的地址。这种设置叫做high vectors

具体是low vectors还是high vectors是由ARM的一个叫做的SCTLR寄存器的第13个bit (vector bit)控制的。对于启用MMU的ARM Linux而言,系统使用了high vectors。为什么不用low vector呢?对于linux而言,0~3G的空间是用户空间,如果使用low vector,那么异常向量表在0地址,那么则是用户空间的位置,因此linux选用high vector。当然,使用Low vector也可以,这样Low vector所在的空间则属于kernel space了(也就是说,3G~4G的空间加上Low vector所占的空间属于kernel space),不过这时候要注意一点,因为所有的进程共享kernel space,而用户空间的程序经常会发生空指针访问,这时候,内存保护机制应该可以捕获这种错误(大部分的MMU都可以做到,例如:禁止userspace访问kernel space的地址空间),防止vector table被访问到。对于内核中由于程序错误导致的空指针访问,内存保护机制也需要控制vector table被修改,因此vector table所在的空间被设置成read only的。在使用了MMU之后,具体异常向量表放在那个物理地址已经不重要了,重要的是把它映射到0xffff0000的虚拟地址就OK了,具体代码如下:

static void __init devicemaps_init(const struct machine_desc *mdesc)
{
……
vectors = early_alloc(PAGE_SIZE * 2); -----分配两个page的物理页帧

early_trap_init(vectors); -------copy向量表以及相关help function到该区域

……
map.pfn = __phys_to_pfn(virt_to_phys(vectors));
map.virtual = 0xffff0000;
map.length = PAGE_SIZE;
#ifdef CONFIG_KUSER_HELPERS
map.type = MT_HIGH_VECTORS;
#else
map.type = MT_LOW_VECTORS;
#endif
create_mapping(&map); ----------映射0xffff0000的那个page frame

if (!vectors_high()) {---如果SCTLR.V的值设定为low vectors,那么还要映射0地址开始的memory
map.virtual = 0;
map.length = PAGE_SIZE * 2;
map.type = MT_LOW_VECTORS;
create_mapping(&map);
}

map.pfn += 1;
map.virtual = 0xffff0000 + PAGE_SIZE;
map.length = PAGE_SIZE;
map.type = MT_LOW_VECTORS;
create_mapping(&map); ----------映射high vecotr开始的第二个page frame

……
}

为什么要分配两个page frame呢?这里vectors table和kuser helper函数(内核空间提供的函数,但是用户空间使用)占用了一个page frame,另外异常处理的stub函数占用了另外一个page frame。为什么会有stub函数呢?稍后会讲到。

在early_trap_init函数中会初始化异常向量表,具体代码如下:

void __init early_trap_init(void *vectors_base)
{
unsigned long vectors = (unsigned long)vectors_base;
extern char __stubs_start[], __stubs_end[];
extern char __vectors_start[], __vectors_end[];
unsigned i;

vectors_page = vectors_base;

将整个vector table那个page frame填充成未定义的指令。起始vector table加上kuser helper函数并不能完全的充满这个page,有些缝隙。如果不这么处理,当极端情况下(程序错误或者HW的issue),CPU可能从这些缝隙中取指执行,从而导致不可知的后果。如果将这些缝隙填充未定义指令,那么CPU可以捕获这种异常。
for (i = 0; i < PAGE_SIZE / sizeof(u32); i++)
((u32 *)vectors_base)[i] = 0xe7fddef1;

拷贝vector table,拷贝stub function
memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start);

kuser_init(vectors_base); ----copy kuser helper function

flush_icache_range(vectors, vectors + PAGE_SIZE * 2);
modify_domain(DOMAIN_USER, DOMAIN_CLIENT);

}

一旦涉及代码的拷贝,我们就需要关心其编译连接时地址(link-time address)和运行时地址(run-time address)。在kernel完成链接后,__vectors_start有了其link-time address,如果link-time address和run-time address一致,那么这段代码运行时毫无压力。但是,目前对于vector table而言,其被copy到其他的地址上(对于High vector,这是地址就是0xffff00000),也就是说,link-time address和run-time address不一样了,如果仍然想要这些代码可以正确运行,那么需要这些代码是位置无关的代码。对于vector table而言,必须要位置无关。B这个branch instruction本身就是位置无关的,它可以跳转到一个当前位置的offset。不过并非所有的vector都是使用了branch instruction,对于软中断,其vector地址上指令是“W(ldr) pc, __vectors_start + 0x1000 ”,这条指令被编译器编译成ldr pc, [pc, #4080],这种情况下,该指令也是位置无关的,但是有个限制,offset必须在4K的范围内,这也是为何存在stub section的原因了。

4、中断控制器的初始化

具体可以参考GIC代码分析。

三、ARM HW对中断事件的处理

当一切准备好之后,一旦打开处理器的全局中断就可以处理来自外设的各种中断事件了。

当外设(SOC内部或者外部都可以)检测到了中断事件,就会通过interrupt requestion line上的电平或者边沿(上升沿或者下降沿或者both)通知到该外设连接到的那个中断控制器,而中断控制器就会在多个处理器中选择一个,并把该中断通过IRQ(或者FIQ,本文不讨论FIQ的情况)分发给该processor。ARM处理器感知到了中断事件后,会进行下面一系列的动作:

1、修改CPSR(Current Program Status Register)寄存器中的M[4:0]。M[4:0]表示了ARM处理器当前处于的模式( processor modes)。ARM定义的mode包括:

处理器模式缩写对应的M[4:0]编码Privilege level
Userusr10000PL0
FIQfiq10001PL1
IRQirq10010PL1
Supervisorsvc10011PL1
Monitormon10110PL1
Abortabt10111PL1
Hyphyp11010PL2
Undefinedund11011PL1
Systemsys11111PL1

一旦设定了CPSR.M,ARM处理器就会将processor mode切换到IRQ mode。

2、保存发生中断那一点的CPSR值(step 1之前的状态)和PC值

ARM处理器支持9种processor mode,每种mode看到的ARM core register(R0~R15,共计16个)都是不同的。每种mode都是从一个包括所有的Banked ARM core register中选取。全部Banked ARM core register包括:

UsrSystemHypSupervisorabortundefinedMonitorIRQFIQ
R0_usr
R1_usr
R2_usr
R3_usr
R4_usr
R5_usr
R6_usr
R7_usr
R8_usrR8_fiq
R9_usrR9_fiq
R10_usrR10_fiq
R11_usrR11_fiq
R12_usrR12_fiq
SP_usrSP_hypSP_svcSP_abtSP_undSP_monSP_irqSP_fiq
LR_usrLR_svcLR_abtLR_undLR_monLR_irqLR_fiq
PC
CPSR
SPSR_hypSPSR_svcSPSR_abtSPSR_undSPSR_monSPSR_irqSPSR_fiq
ELR_hyp

在IRQ mode下,CPU看到的R0~R12寄存器、PC以及CPSR是和usr mode(userspace)或者svc mode(kernel space)是一样的。不同的是IRQ mode下,有自己的R13(SP,stack pointer)、R14(LR,link register)和SPSR(Saved Program Status Register)。

CPSR是共用的,虽然中断可能发生在usr mode(用户空间),也可能是svc mode(内核空间),不过这些信息都是体现在CPSR寄存器中。硬件会将发生中断那一刻的CPSR保存在SPSR寄存器中(由于不同的mode下有不同的SPSR寄存器,因此更准确的说应该是SPSR-irq,也就是IRQ mode中的SPSR寄存器)。

PC也是共用的,由于后续PC会被修改为irq exception vector,因此有必要保存PC值。当然,与其说保存PC值,不如说是保存返回执行的地址。对于IRQ而言,我们期望返回地址是发生中断那一点执行指令的下一条指令。具体的返回地址保存在lr寄存器中(注意:这个lr寄存器是IRQ mode的lr寄存器,可以表示为lr_irq):

(1)对于thumb state,lr_irq = PC

(2)对于ARM state,lr_irq = PC - 4

为何要减去4?我的理解是这样的(不一定对)。由于ARM采用流水线结构,当CPU正在执行某一条指令的时候,其实取指的动作早就执行了,这时候PC值=正在执行的指令地址 + 8,如下所示:

----> 发生中断的指令

发生中断的指令+4

-PC-->发生中断的指令+8

发生中断的指令+12

一旦发生了中断,当前正在执行的指令当然要执行完毕,但是已经完成取指、译码的指令则终止执行。当发生中断的指令执行完毕之后,原来指向(发生中断的指令+8)的PC会继续增加4,因此发生中断后,ARM core的硬件着手处理该中断的时候,硬件现场如下图所示:

----> 发生中断的指令

发生中断的指令+4 <——-中断返回的指令是这条指令

发生中断的指令+8

-PC-->发生中断的指令+12

这时候的PC值其实是比发生中断时候的指令超前12。减去4之后,lr_irq中保存了(发生中断的指令+8)的地址。为什么HW不帮忙直接减去8呢?这样,后续软件不就不用再减去4了。这里我们不能孤立的看待问题,实际上ARM的异常处理的硬件逻辑不仅仅处理IRQ的exception,还要处理各种exception,很遗憾,不同的exception期望的返回地址不统一,因此,硬件只是帮忙减去4,剩下的交给软件去调整。

3、mask IRQ exception。也就是设定CPSR.I = 1

4、设定PC值为IRQ exception vector。基本上,ARM处理器的硬件就只能帮你帮到这里了,一旦设定PC值,ARM处理器就会跳转到IRQ的exception vector地址了,后续的动作都是软件行为了。

四、如何进入ARM中断处理

1、IRQ mode中的处理

IRQ mode的处理都在vector_irq中,vector_stub是一个宏,定义如下:

.macro vector_stub, name, mode, correction=0
.align 5

vector_\name:
.if \correction
sub lr, lr, #\correction-------------(1)
.endif

@
@ Save r0, lr_ (parent PC) and spsr_
@ (parent CPSR)
@
stmia sp, {r0, lr} @ save r0, lr--------(2)
mrs lr, spsr
str lr, [sp, #8] @ save spsr

@
@ Prepare for SVC32 mode. IRQs remain disabled.
@
mrs r0, cpsr-----------------------(3)
eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
msr spsr_cxsf, r0

@
@ the branch table must immediately follow this code
@
and lr, lr, #0x0f---lr保存了发生IRQ时候的CPSR,通过and操作,可以获取CPSR.M[3:0]的值

这时候,如果中断发生在用户空间,lr=0,如果是内核空间,lr=3
THUMB( adr r0, 1f )----根据当前PC值,获取lable 1的地址
THUMB( ldr lr, [r0, lr, lsl #2] )-lr根据当前mode,要么是__irq_usr的地址 ,要么是__irq_svc的地址
mov r0, sp------将irq mode的stack point通过r0传递给即将跳转的函数
ARM( ldr lr, [pc, lr, lsl #2] )---根据mode,给lr赋值,__irq_usr或者__irq_svc
movs pc, lr @ branch to handler in SVC mode-----(4)
ENDPROC(vector_\name)

.align 2
@ handler addresses follow this label
1:
.endm

(1)我们期望在栈上保存发生中断时候的硬件现场(HW context),这里就包括ARM的core register。上一章我们已经了解到,当发生IRQ中断的时候,lr中保存了发生中断的PC+4,如果减去4的话,得到的就是发生中断那一点的PC值。

(2)当前是IRQ mode,SP_irq在初始化的时候已经设定(12个字节)。在irq mode的stack上,依次保存了发生中断那一点的r0值、PC值以及CPSR值(具体操作是通过spsr进行的,其实硬件已经帮我们保存了CPSR到SPSR中了)。为何要保存r0值?因为随后的代码要使用r0寄存器,因此我们要把r0放到栈上,只有这样才能完完全全恢复硬件现场。

(3)可怜的IRQ mode稍纵即逝,这段代码就是准备将ARM推送到SVC mode。如何准备?其实就是修改SPSR的值,SPSR不是CPSR,不会引起processor mode的切换(毕竟这一步只是准备而已)。

(4)很多异常处理的代码返回的时候都是使用了stack相关的操作,这里没有。“movs pc, lr ”指令除了字面上意思(把lr的值付给pc),还有一个隐含的操作(movs中‘s’的含义):把SPSR copy到CPSR,从而实现了模式的切换。

2、当发生中断的时候,代码运行在用户空间

Interrupt dispatcher的代码如下:

vector_stub irq, IRQ_MODE, 4 -----减去4,确保返回发生中断之后的那条指令

.long __irq_usr @ 0 (USR_26 / USR_32) <———————> base address + 0
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32)<———————> base address + 12
.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

这其实就是一个lookup table,根据CPSR.M[3:0]的值进行跳转(参考上一节的代码:and lr, lr, #0x0f)。因此,该lookup table共设定了16个入口,当然只有两项有效,分别对应user mode和svc mode的跳转地址。其他入口的__irq_invalid也是非常关键的,这保证了在其模式下发生了中断,系统可以捕获到这样的错误,为debug提供有用的信息。

.align 5
__irq_usr:
usr_entry---------请参考本章第一节(1)保存用户现场的描述
kuser_cmpxchg_check---和本文描述的内容无关,这些不就介绍了
irq_handler----------核心处理内容,请参考本章第二节的描述
get_thread_info tsk------tsk是r9,指向当前的thread info数据结构
mov why, #0--------why是r8
b ret_to_user_from_irq----中断返回,下一章会详细描述

why其实就是r8寄存器,用来传递参数的,表示本次放回用户空间相关的系统调用是哪个?中断处理这个场景和系统调用无关,因此设定为0。

(1)保存发生中断时候的现场。所谓保存现场其实就是把发生中断那一刻的硬件上下文(各个寄存器)保存在了SVC mode的stack上。

.macro usr_entry
sub sp, sp, #S_FRAME_SIZE--------------A
stmib sp, {r1 - r12} -------------------B

ldmia r0, {r3 - r5}--------------------C
add r0, sp, #S_PC-------------------D
mov r6, #-1----orig_r0的值

str r3, [sp] ----保存中断那一刻的r0

stmia r0, {r4 - r6}--------------------E
stmdb r0, {sp, lr}^-------------------F
.endm

A:代码执行到这里的时候,ARM处理已经切换到了SVC mode。一旦进入SVC mode,ARM处理器看到的寄存器已经发生变化,这里的sp已经变成了sp_svc了。因此,后续的压栈操作都是压入了发生中断那一刻的进程的(或者内核线程)内核栈(svc mode栈)。具体保存多少个寄存器值?S_FRAME_SIZE已经给出了答案,这个值是18个寄存器。r0~r15再加上CPSR也只有17个而已。先保留这个疑问,我们稍后回答。

B:压栈首先压入了r1~r12,这里为何不处理r0?因为r0在irq mode切到svc mode的时候被污染了,不过,原始的r0被保存的irq mode的stack上了。r13(sp)和r14(lr)需要保存吗,当然需要,稍后再保存。执行到这里,内核栈的布局如下图所示:

ir1

stmib中的ib表示increment before,因此,在压入R1的时候,stack pointer会先增加4,重要是预留r0的位置。stmib sp, {r1 - r12}指令中的sp没有“!”的修饰符,表示压栈完成后并不会真正更新stack pointer,因此sp保持原来的值。

C:注意,这里r0指向了irq stack,因此,r3是中断时候的r0值,r4是中断现场的PC值,r5是中断现场的CPSR值。

D:把r0赋值为S_PC的值。根据struct pt_regs的定义(这个数据结构反应了内核栈上的保存的寄存器的排列信息),从低地址到高地址依次为:

ARM_r0
ARM_r1
ARM_r2
ARM_r3
ARM_r4
ARM_r5
ARM_r6
ARM_r7
ARM_r8
ARM_r9
ARM_r10
ARM_fp
ARM_ip
ARM_sp
ARM_lr
ARM_pc<———add r0, sp, #S_PC指令使得r0指向了这个位置
ARM_cpsr
ARM_ORIG_r0

为什么要给r0赋值?因此kernel不想修改sp的值,保持sp指向栈顶。

E:在内核栈上保存剩余的寄存器的值,根据代码,依次是r0,PC,CPSR和orig r0。执行到这里,内核栈的布局如下图所示:

ir2

R0,PC和CPSR来自IRQ mode的stack。实际上这段操作就是从irq stack就中断现场搬移到内核栈上。

F:内核栈上还有两个寄存器没有保持,分别是发生中断时候sp和lr这两个寄存器。这时候,r0指向了保存PC寄存器那个地址(add r0, sp, #S_PC),stmdb r0, {sp, lr}^中的“db”是decrement before,因此,将sp和lr压入stack中的剩余的两个位置。需要注意的是,我们保存的是发生中断那一刻(对于本节,这是当时user mode的sp和lr),指令中的“^”符号表示访问user mode的寄存器。

(2)核心处理

irq_handler的处理有两种配置。一种是配置了CONFIG_MULTI_IRQ_HANDLER。这种情况下,linux kernel允许run time设定irq handler。如果我们需要一个linux kernel image支持多个平台,这是就需要配置这个选项。另外一种是传统的linux的做法,irq_handler实际上就是arch_irq_handler_default,具体代码如下:

.macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq
mov r0, sp--------设定传递给machine定义的handle_arch_irq的参数
adr lr, BSYM(9997f)----设定返回地址
ldr pc, [r1]
#else
arch_irq_handler_default
#endif
9997:
.endm

对于情况一,machine相关代码需要设定handle_arch_irq函数指针,这里的汇编指令只需要调用这个machine代码提供的irq handler即可(当然,要准备好参数传递和返回地址设定)。

情况二要稍微复杂一些(而且,看起来kernel中使用的越来越少),代码如下:

.macro arch_irq_handler_default
get_irqnr_preamble r6, lr
1: get_irqnr_and_base r0, r2, r6, lr
movne r1, sp
@
@ asm_do_IRQ 需要两个参数,一个是 irq number(保存在r0)
@ 另一个是 struct pt_regs *(保存在r1中)
adrne lr, BSYM(1b)-------返回地址设定为符号1,也就是说要不断的解析irq状态寄存器

的内容,得到IRQ number,直到所有的irq number处理完毕
bne asm_do_IRQ
.endm

这里的代码已经是和machine相关的代码了,我们这里只是简短描述一下。所谓machine相关也就是说和系统中的中断控制器相关了。get_irqnr_preamble是为中断处理做准备,有些平台根本不需要这个步骤,直接定义为空即可。get_irqnr_and_base 有四个参数,分别是:r0保存了本次解析的irq number,r2是irq状态寄存器的值,r6是irq controller的base address,lr是scratch register。

对于ARM平台而言,我们推荐使用第一种方法,因为从逻辑上讲,中断处理就是需要根据当前的硬件中断系统的状态,转换成一个IRQ number,然后调用该IRQ number的处理函数即可。通过get_irqnr_and_base这样的宏定义来获取IRQ是旧的ARM SOC系统使用的方法,它是假设SOC上有一个中断控制器,硬件状态和IRQ number之间的关系非常简单。但是实际上,ARM平台上的硬件中断系统已经是越来越复杂了,需要引入interrupt controller级联,irq domain等等概念,因此,使用第一种方法优点更多。

3、当发生中断的时候,代码运行在内核空间

如果中断发生在内核空间,代码会跳转到__irq_svc处执行:

.align 5
__irq_svc:
svc_entry----保存发生中断那一刻的现场保存在内核栈上
irq_handler ----具体的中断处理,同user mode的处理。

#ifdef CONFIG_PREEMPT--------和preempt相关的处理
get_thread_info tsk
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
ldr r0, [tsk, #TI_FLAGS] @ get flags
teq r8, #0 @ if preempt count != 0
movne r0, #0 @ force flags to 0
tst r0, #_TIF_NEED_RESCHED
blne svc_preempt
#endif

svc_exit r5, irq = 1 @ return from exception

一个task的thread info数据结构定义如下(只保留和本场景相关的内容):

struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
……
};

flag成员用来标记一些low level的flag,而preempt_count用来判断当前是否可以发生抢占,如果preempt_count不等于0(可能是代码调用preempt_disable显式的禁止了抢占,也可能是处于中断上下文等),说明当前不能进行抢占,直接进入恢复现场的工作。如果preempt_count等于0,说明已经具备了抢占的条件,当然具体是否要抢占当前进程还是要看看thread info中的flag成员是否设定了_TIF_NEED_RESCHED这个标记(可能是当前的进程的时间片用完了,也可能是由于中断唤醒了优先级更高的进程)。

保存现场的代码和user mode下的现场保存是类似的,因此这里不再详细描述,只是在下面的代码中内嵌一些注释。

.macro svc_entry, stack_hole=0
sub sp, sp, #(S_FRAME_SIZE + \stack_hole - 4)----sp指向struct pt_regs中r1的位置
stmia sp, {r1 - r12} ------寄存器入栈。

ldmia r0, {r3 - r5}
add r7, sp, #S_SP - 4 ------r7指向struct pt_regs中r12的位置
mov r6, #-1 ----------orig r0设为-1
add r2, sp, #(S_FRAME_SIZE + \stack_hole - 4)----r2是发现中断那一刻stack的现场
str r3, [sp, #-4]! ----保存r0,注意有一个!,sp会加上4,这时候sp就指向栈顶的r0位置了

mov r3, lr ----保存svc mode的lr到r3
stmia r7, {r2 - r6} ---------压栈,在栈上形成形成struct pt_regs
.endm

至此,在内核栈上保存了完整的硬件上下文。实际上不但完整,而且还有些冗余,因为其中有一个orig_r0的成员。所谓original r0就是发生中断那一刻的r0值,按理说,ARM_r0和ARM_ORIG_r0都应该是用户空间的那个r0。 为何要保存两个r0值呢?为何中断将-1保存到了ARM_ORIG_r0位置呢?理解这个问题需要跳脱中断处理这个主题,我们来看ARM的系统调用。对于系统调用,它 和中断处理虽然都是cpu异常处理范畴,但是一个明显的不同是系统调用需要传递参数,返回结果。如果进行这样的参数传递呢?对于ARM,当然是寄存器了, 特别是返回结果,保存在了r0中。对于ARM,r0~r7是各种cpu mode都相同的,用于传递参数还是很方便的。因此,进入系统调用的时候,在内核栈上保存了发生系统调用现场的所有寄存器,一方面保存了hardware context,另外一方面,也就是获取了系统调用的参数。返回的时候,将返回值放到r0就OK了。
根据上面的描述,r0有两个作用,传递参数,返回结果。当把系统调用的结果放到r0的时候,通过r0传递的参数值就被覆盖了。本来,这也没有什么,但是有些场合是需要需要这两个值的:
1、ptrace (和debugger相关,这里就不再详细描述了)
2、system call restart (和signal相关,这里就不再详细描述了)
正因为如此,硬件上下文的寄存器中r0有两份,ARM_r0是传递的参数,并复制一份到ARM_ORIG_r0,当系统调用返回的时候,ARM_r0是系统调用的返回值。
OK,我们再回到中断这个主题,其实在中断处理过程中,没有使用ARM_ORIG_r0这个值,但是,为了防止system call restart,可以赋值为非系统调用号的值(例如-1)。

五、中断退出过程

无论是在内核态(包括系统调用和中断上下文)还是用户态,发生了中断后都会调用irq_handler进行处理,这里会调用对应的irq number的handler,处理softirq、tasklet、workqueue等(这些内容另开一个文档描述),但无论如何,最终都是要返回发生中断的现场。

1、中断发生在user mode下的退出过程,代码如下:

ENTRY(ret_to_user_from_irq)
ldr r1, [tsk, #TI_FLAGS]
tst r1, #_TIF_WORK_MASK---------------A
bne work_pending
no_work_pending:
asm_trace_hardirqs_on ------和irq flag trace相关,暂且略过

/* perform architecture specific actions before user return */
arch_ret_to_user r1, lr----有些硬件平台需要在中断返回用户空间做一些特别处理
ct_user_enter save = 0 ----和trace context相关,暂且略过

restore_user_regs fast = 0, offset = 0------------B
ENDPROC(ret_to_user_from_irq)
ENDPROC(ret_to_user)

A:thread_info中的flags成员中有一些low level的标识,如果这些标识设定了就需要进行一些特别的处理,这里检测的flag主要包括:

#define _TIF_WORK_MASK (_TIF_NEED_RESCHED | _TIF_SIGPENDING | _TIF_NOTIFY_RESUME)

这三个flag分别表示是否需要调度、是否有信号处理、返回用户空间之前是否需要调用callback函数。只要有一个flag被设定了,程序就进入work_pending这个分支(work_pending函数需要传递三个参数,第三个是参数why是标识哪一个系统调用,当然,我们这里传递的是0)。

B:从字面的意思也可以看成,这部分的代码就是将进入中断的时候保存的现场(寄存器值)恢复到实际的ARM的各个寄存器中,从而完全返回到了中断发生的那一点。具体的代码如下:

.macro restore_user_regs, fast = 0, offset = 0
ldr r1, [sp, #\offset + S_PSR] ----r1保存了pt_regs中的spsr,也就是发生中断时的CPSR
ldr lr, [sp, #\offset + S_PC]! ----lr保存了PC值,同时sp移动到了pt_regs中PC的位置
msr spsr_cxsf, r1 ---------赋值给spsr,进行返回用户空间的准备
clrex @ clear the exclusive monitor

.if \fast
ldmdb sp, {r1 - lr}^ @ get calling r1 - lr
.else
ldmdb sp, {r0 - lr}^ ------将保存在内核栈上的数据保存到用户态的r0~r14寄存器
.endif
mov r0, r0 ---------NOP操作,ARMv5T之前的需要这个操作
add sp, sp, #S_FRAME_SIZE - S_PC----现场已经恢复,移动svc mode的sp到原来的位置
movs pc, lr --------返回用户空间
.endm

2、中断发生在svc mode下的退出过程。具体代码如下:

.macro svc_exit, rpsr, irq = 0
.if \irq != 0
@ IRQs already off
.else
@ IRQs off again before pulling preserved data off the stack
disable_irq_notrace
.endif
msr spsr_cxsf, \rpsr-------将中断现场的cpsr值保存到spsr中,准备返回中断发生的现场

ldmia sp, {r0 - pc}^ -----这条指令是ldm异常返回指令,这条指令除了字面上的操作,

还包括了将spsr copy到cpsr中。

.endm

原创文章,转发请注明出处。蜗窝科技。/irq_subsystem/irq_handler.html

附录

change log-2014-10-20,自己又重新阅读了一遍,做了一些修改,如下:

1、“ARM处理器有多种process mode”修改为“ARM处理器有多种processor mode”
2、增加cpu_init的调用场景说明:
(1)bootstrap CPU initialize
(2)secondary CPUs initialize
(3)CPU resume from sleep
3、增加对核心中断处理的描述
4、增加对抢占相关的描述

change log-2014-11-20,根据zuoertu网友的提问,做了一些修改,如下:

1、增加对orig_r0的描述

2、增加对why的描述

标签:irqhandler中断处理

继续讨论

新讨论请用 GitHub 账号在下方发言——评论会保存在本仓库的 Discussions 里,与站点同寿; 再下方的「历史评论」是原站的只读存档。

评论(100 条,只读存档)

以下为原站的历史评论(只读存档);继续讨论请用上方的评论区。

  • forion

    先顶后看
  • forion

    hi linuxer 不得不佩服您的每一篇文章都能把一个系统讲的如此的透彻,读您的文章简直就是享受啊。希望您能坚持啊,我的Linux学习可就全靠你了啊。哈哈哈哈 有一个地方要更正一下 (中断控制器可以级联,但是我猜测只有root interrupt controller才能分发中断给CPU), 您可以看下您gic文章里面的那副gic的block图,会看到有个名字叫b的器件,b Optional input and bypass multiplexer,这里可以设置irq fiq的bypass,所以并不是只有root interrupt controller才能发中断给cpu
    • linuxer

      @forion:多谢!多谢!之所以建立蜗窝科技这个网站就是希望能够享受技术带来的愉悦感,这一点有点类似看电影,我特别喜欢动作片中的慢动作,清清楚楚的看到每一个细节。 你提出的问题我们可以进一步探讨:假设我们要设计一个非常复杂的系统,系统中有8个CPU,有2000个外设中断要处理,这时候你如何设计系统中的interrupt controller?如果使用GIC的话,我们需要两个GIC(一个GIC最多支持1024个中断源),一个是root GIC,另外一个是secondary GIC。这时候,把cpu都连接到root GIC的irq(或者FIQ)上是一个比较简单的设计,这时候secondary GIC的PPI和SGI都是没有用的了。当然,你也可以让每个GIC分别连接4个CPU core,但这样,在中断处理方面看起来就体现不出8核的威力了。
      • forion

        @linuxer:哈哈,正常的应该是你的这个思路。我只是说,有另一种的实现方式,by the way 从中断处理无论是返回用户空间还是返回内核空间,都是可能会发生抢占的。了解这些对了解系统的运作很有好处,不知道这些内容你打算怎么讲,个人意见我感觉在这里可以稍微讲一下preempt_count跟need_resched. anyway,还是以您的规划为主。 ================================================================== 每一篇文章,都是一部好电影:)
        • linuxer

          @forion:本来想提一提preemptive kernel的概念,写了一小段,不过还是删除了。preemptive的概念是进程调度相关的,还是留给以后吧。本月还是focus在中断处理上吧,希望本月可以把中断子系统讲清楚就OK了
          • forion

            @linuxer:ok,ok。那我就静候佳作了,哈哈哈。
  • 动漫资讯 主页

    这些东西好像看不懂,也只能来看看
  • 动漫资讯 主页

    这些东西好像看不懂,也只能来看看,我怎么就评论失败了呢
  • linuxer

    修改了ARM硬件对保存PC值的描述,原来的描述是错误的。
    • forion

      @linuxer:刚找了本书翻了翻,确实是pc+12我之前也是一直以为是+8。
      • linuxer

        @forion:发生中断的那一刻PC的确是+8的,但是,中断处理不可能立刻进行,要等到发生中断的那一刻的指令被执行完毕,这时候,PC在+8的基础上又+4,因此total是+12了,我之前搞错了
        • song20

          @linuxer:最后为什么是减4是因为硬件会我们减去4,是这样吗linuxer?
          • linuxer

            @song20:发生中断的时候去读PC寄存器的值的时候,PC已经是“发生中断的那条指令地址+12”,对于中断异常而言,我们当然是返回到“发生中断的那条指令地址+4”,也就是中断发生时候的下一条指令。 你可以看到,实际的PC值需要减去8才是我们期望的,因此: ARM硬件-4 软件-4 通过上面两个操作,最终使得中断返回现场的时候PC恢复成“发生中断的那条指令地址+4”
  • Daniel Shieh

    博主准备以3.14的内核来分析各个模块啊,非常非常的感谢,现在市场上3.8内核以后的书籍我都找不到,学习起来只能靠自己摸索,但是基础比价薄弱,学起来很慢。希望博主早日出书。
    • linuxer

      @Daniel Shieh:多谢鼓励。 目前linux kernel的学习曲线越来越陡峭,一个对内核有热情的新人在面对linux kernel的时候经常是开始豪情万丈,最后绝望的放弃了。我们希望可以帮助到这些人。如果有机会出书当然好,不过现在还是埋头耕耘吧~~~ 目前的文档都是以3.14内核为基础,如果出书,肯定是用最新的内核了。
  • forion

    hi linuxer 我一直有个疑惑,就是linux 中断的顶半部是在哪里结束的。你下面几章讲到中断顶半部与低版本的时候,可不可以讲一下。顶半部是在中断返回的时候结束的吗?比如__irq_svc 是在 svc_exit r5, irq = 1 @ return from exception 是在这里结束的吗?
    • linuxer

      @forion:恩,这篇文章其实有点虎头蛇尾,呵呵~~~看来我要重新修改一下关于中断退出这一个章节的描述了。目前的描述比较简陋,后续再增加一些图片和描述性文字好了。
      • forion

        @linuxer:我以为底半部的东西你会在后面讲呢,因为你后面应该会讲到申请中断,那么内核新添加的线程化irq,还有其他之前的底半部的策略,你应该都会一并来说明吧,tasklet,workqueue,softirq等等。
        • linuxer

          @forion:本来想在这份文档简单的描述一下bottom half的概念,不过还是算了,还是专门出文档描述吧。这次主要补充了内核栈的描述。OK,我们回到你具体的问题,你问是否top half是否是在 svc_exit处结束了(对于发生在内核态的中断)。我只能说,如果没有中断嵌套,那么top half的确是在 svc_exit处结束了,但是如果A中断处理中发生了B中断的嵌套,那么在执行svc_exit的时候回到了A中断的top half
          • forion

            @linuxer:我想了解,在中断的处理过程中,在哪一个点,是开中断的,也就是哪一点可以允许中断嵌套。不知道楼主有没有研究过。
            • linuxer

              @forion:我想你问的是interrupt handler是否开中断的问题,旧的内核是这么看待这个问题的:真实的世界中有slow handler和fast handle,在request irq的时候,对于fast handler,需要传递IRQF_DISABLED的参数,确保其中断处理过程中是关闭中断的。在我们实际的系统中,一般很少用这个flag,只有tick的中断使用了这个flag,也就意味着整个tick中断handler都是关闭中断的。这种情况下,interrupt handler是不会嵌套的。 我们再看看slow handler,如果A中断是slow handler,那么在处理A中断的时候,其handler是全程开中断的,除了它自己类型的中断不会嵌套(在handler中会mask本IRQ number对应的中断),其他的中断都可以随时打断slow handler的执行。 在新的内核中,比如3.14,事情发生了变化。我们可以思考一下,为何要有slow handler?每一个handler不都是应该迅速执行完毕,返回中断现场吗?此外,slow handler导致中断嵌套加深,对内核栈也是考验。因此,新的内核中在interrupt handler中是全程关闭中断的 上面说的都是top half,更细致的内容还是我还是写成文档好了
              • forion

                @linuxer:ok,好的 好的,不过感觉中断这一块还得有好几篇文章才能形成一个体系。而且感觉我们这个坛子人气越来越高了。哈哈,恭喜:)
              • linuxer

                @linuxer:除了它自己类型的中断不会嵌套(在handler中会mask本IRQ number对应的中断),其他的中断都可以随时打断slow handler的执行。 =============================== 抱歉,我要更正上面的话。要删除()中的那些表述,可能会误导大家。
          • 每天一小步

            @linuxer:我有些不同意见: 顶半部应该在irq_exit里面某个特定时刻就结束了,请看下面的调用关系。在svc_exit之前可能有一段时间在处理底半部。 659 int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq, 660 bool lookup, struct pt_regs *regs){ ... generic_handle_irq(irq); ... irq_exit(); //<================= ... } 403 void irq_exit(void) 404 { ... 410 account_irq_exit_time(current); 411 preempt_count_sub(HARDIRQ_OFFSET); // <============= 412 if (!in_interrupt() && local_softirq_pending()) 413 invoke_softirq(); //<================== ... } 356 static inline void invoke_softirq(void) 357 { ... __do_softirq(); ... } 245 asmlinkage __visible void __softirq_entry __do_softirq(void) 246 { ... 272 local_irq_enable(); ... 276 while ((softirq_bit = ffs(pending))) { ... //处理软中断 ... } ... }
            • 每天一小步

              @每天一小步:245 asmlinkage __visible void __softirq_entry __do_softirq(void) 246 { ... 272 local_irq_enable(); //这里打开cpu本地中断,窃以为这里就算是结束顶半部处理了
  • maotou

    linuxer前辈,非常感谢您的回答,以后我会尽量说清楚自己的疑问. 在对于cpu响应中断前的工作中,主要有中断注册,然后再调用中断的入口函数do_asm_irq(),最后这个函数调用do_handler()函数,来完成这个中断的处理.是谁来调用do_asm_irq()呢,是设备本身么?在设备探测到中断时,其在软件层面的数据流向是怎样的呢?
    • linuxer

      @maotou:硬件触发中断-->exception table中的vector_irq向量--->根据当时的CPU mode进入__irq_usr或者__irq_svc函数---> irq_handler ---> arch_irq_handler_default ---> asm_do_IRQ 上面就是调用asm_do_IRQ的过程
      • maotou

        @linuxer:非常感谢您的指导,发现已慢慢理清思路,一点点在啃了. 现在有个新的问题想请教前辈:系统在启动的过程中会对中断描述符进行初始化,但对其成员变量不做任何赋值,在驱动程序的初始化时会注册中断服务例程,但是我看他还是没有将其chip变量设为与gic有关的.中断描述符中中断控制器的设置在什么地方呢?
        • linuxer

          @maotou:我重新写了一个gic driver的文档,你看看是否能回答你的问题(我觉得我写的挺清楚,不过还是需要广大人名群众检验一下),顺便帮我review一下。
  • Zhedawo

    @linuxer 你好,我想问一下在整个中断处理流程(从中断触发到返回到被抢占点),哪些地方中断是打开?谢谢了
    • linuxer

      @Zhedawo:一开始,硬件就关闭的中断,在整个high level irq handler(interrupt flow control,主要是和interrupt controller交互)的过程中,中断也是关闭的。在进入具体的irq handler之前(各个driver用request_irq或者request_threaded_irq申请的),中断也是关闭的。 由于各个driver的具体的irq handler是各个驱动工程师撰写的,其是否打开中断是linux kernel的中断子系统无法掌握的,但是,毫无疑问,kernel是要求(至少3.14的kernel)在driver的irq handler中是关闭中断的。 在interrupt handler执行完毕后,进入bottom half的处理,在bottom half的处理过程中,当然是打开中断的。在进行bottom half处理之前,代码会记住当前中断的flag,执行完bottom half的代码之后,代码会恢复中断状态。一般而言,bottom half之前,中断是关闭的(除非在具体的interrupt handler中打开了中断),因此执行完bootom half,恢复中断状态也就是关闭了中断,直到返回中断现场。 我印象如此,明天再浏览一下代码,确认一下。
      • Zhedawo

        @linuxer:@linuxer, 谢谢你的回复,解决了我的疑惑。
      • passerby

        @linuxer:这里的关闭中断,关闭是这个source的中断还是说整个local CPU的中断关闭?如果是local cpu中断关闭,那么就是不是没有了中断嵌套?
        • linuxer

          @passerby:旧的内核区分fast interrupt handler和slow interrupt handler,因此存在中断嵌套的现象。对于新的内核,所有的handler都是fast interrupt handler,都是关闭本cpu的全局中断的,因此,也就没有中断嵌套的概念了
          • david

            @linuxer:@linuxer 我的理解是进入IRQ mode是CPSR.I已经被设置为1, 知道中断处理完恢复现场是才会将之前中断的CPSR值恢复,整个过程中IRQ已经被CPSR禁止了,ARM 也就不会再响应其他的 IRQ中断了。对吧?
            • hello_world

              @david:的确,当发生中断的时候,硬件会帮忙设定CPSR.I的值,从而disable了该CPU的local interrupt,并且将现场保存在了内核栈上,返回的时候,会恢复现场。但是,这并不意味着整个期间ARM cpu不会再响应其他的中断,一个简单的例子是:中断的下半部难道不是开中断的吗?
              • david

                @hello_world:@hello_world,看了代码,__do_softirq里面会enable local irq.也就是说中断的下半部分是可以被硬中断抢占的,以网卡驱动为例的话,假设t0发生硬中断,GIC将这个irq分给cpu0执行,cpu0做了简单处理然后执行softirq.在enable_local_irq之后,有可能网卡t1又发生硬件中断,而此时softirq还没有执行完,而且这个hardirq又被分配给了cpu0,导致cpu0的software irq被抢占,等到t1中断简单处理之后,cpu0会按顺序呢完成softirq的执行。这个应该是有可能的一个flow吧。
              • bsp

                @hello_world:@david 是的,软中断执行过程中是可以被硬中断打断的。硬中断里面一般会通过raise_softirq_ir设置对应的软中断bit,硬中断执行后再回来,会接着继续执行软中断的流程。软中断执行完成后,会再检测相应的软中断bit,如果有效(软中断流程被打断了),会再执行一遍软中断流程。 至于能够执行多少遍,MAX_SOFTIRQ_RESTART这个经验值定义为10.
  • zuoertu

    你好,在保存中断现场的时候也就是进入usr_entry这个中, 这句没看懂“mov r6, #-1----orig_r0的值” 为什么 把-1 存到r6中, -1 会是orig_r0的值, orig_r0的值不是保存到了svc mode下的R3中吗???求解 -1 为啥是orig_r0
    • linuxer

      @zuoertu:不论如何,在内核栈上的frame如下: ARM_r0 ARM_r1 ARM_r2 ARM_r3 ARM_r4 ARM_r5 ARM_r6 ARM_r7 ARM_r8 ARM_r9 ARM_r10 ARM_fp ARM_ip ARM_sp ARM_lr ARM_pc ARM_cpsr ARM_ORIG_r0 的确,所谓original r0就是发生中断那一刻的r0值,按理说,ARM_r0和ARM_ORIG_r0都应该是用户空间的那个r0。 但是为何中断将-1保存到了ARM_ORIG_r0位置呢?理解这个问题需要跳脱中断处理这个主题,我们来看ARM的系统调用。对于系统调用,它和中断处理虽然都是cpu异常处理范畴,但是一个明显的不同是系统调用需要传递参数,返回结果。如果进行这样的参数传递呢?对于ARM,当然是寄存器了,特别是返回结果,保存在了r0中。对于ARM,r0~r7是各种cpu mode都相同的,用于传递参数还是很方便的。因此,进入系统调用的时候,在内核栈上保存了发生系统调用现场的所有寄存器,一方面保存了hardware context,另外一方面,也就是获取了系统调用的参数。返回的时候,将返回值放到r0就OK了。 根据上面的描述,r0有两个作用,传递参数,返回结果。当把系统调用的结果放到r0的时候,通过r0传递的参数值就被覆盖了。本来,这也没有什么,但是有些场合是需要需要这两个值的: 1、ptrace 2、system call restart 正因为如此,硬件上下文的寄存器中r0有两份,ARM_r0是传递的参数,并复制一份到ARM_ORIG_r0,当系统调用返回的时候,ARM_r0是系统调用的返回值。 我们再回到中断这个主题,其实在中断处理过程中,没有使用ARM_ORIG_r0这个值,但是,为了防止system call restart,可以赋值为非系统调用号的值(例如-1)。
      • zuoertu

        @linuxer:很意外博主答复我的疑惑,非常感谢博主。
  • ayeu0425

    Hi 博主。为什么irq真正的处理函数要在svc mode下进行? 因为现在是我自己实现的fiq,发现fiq的handler都是在 fiq mode下的(通过 trace32 查看cpsr,这点很确信)。这样就发现一些问题: 1、访问 preempt_count不正确 2、直接写preempt_count或其他thread_info会导致内核崩溃 3、tasklet_schedule不能正确使用 我个人认为是不能在fiq mode下访问 kernel api的问题,请帮忙分析一下!谢谢。。
    • linuxer

      @ayeu0425:抱歉这么晚回答,最近实在是太忙了.... 你问了两个问题,实际上答案是一个,看起来问题都是和中断相关,实际上更多的是进程相关的问题。 linux kernel在设计进程的时候主要区分两种状态,user space和kernel space。也就是说,无论什么情况,一个进程要么是在user space运行,要么是在kernel space运行。而在kernel space运行则意味着stack切换到内核栈上来。只有在内核栈上,才会有thread info,才会有task struct,才会有preempt_count这些概念,你应该知道的,8K(也有可能是4K,和内核配置相关)的内核栈的底部是保存了该进程(或者线程)thread info数据结构。 了解了上面的知识后,我们就知道了,当进程在运行的时候,可能会遇到未定义的指令,可以产生异常(系统调用),可能产生IRQ中断,可能会取数据异常,但是无论如何,最后都回归到该task的内核态中,所谓task的内核态对应就是processor的svc mode。 当然,你可以可以选择为task定义一个irq space,不过你需要为每一个task都分配irq stack,还要想办法去访问该task的thread info、task struct等数据结构。这样设计没有什么好处当然被linux kernel的维护者抛弃了。 OK,正面回答你的问题,preempt_count是struct thread_info的一个成员,访问preempt_count是基于访问thread_info的,而访问thread_info是通过内核栈(svc mode)的栈指针SP_svc进行的,如果发生FIQ的时候,processor处于FIQ mode,那么其sp(更准确的说是SP_fiq)指向哪里?要知道,SP_fiq和SP_svc是不同的。
      • ayeu0425

        @linuxer:谢谢博主的热心回答!解析真的很透彻,我花了快一个星期才找出来的问题,博主三两句话就解释的如此清楚。最近做这个网卡的fiq总是时不时来查阅下博主的文章。 现在已经放弃了在fiq中call tasklet的任何东西,在想另一种方案: 大概就是用软件去触发一个硬中断,比如用软件去置位一个中断的标志位,进而产生一个硬件中断。还想问下博主, arm的gic支持这种方法吗? 会不会还和平台有关? 我现在用的是飞思卡尔的imx6q.
        • ayeu0425

          @ayeu0425:再补充一下,并不确定这个方法可行,只是听bsp部门的大牛这样提了一下,说他以前用过,但很久没有试过了。
        • linuxer

          @ayeu0425:从CPU设计者的角度看,ARM处理器的FIQ似乎看起来对应X86的NMI中断,实际上它们的设计目标还是有差别的。PC的NMI中断被设计出来主要用于debug、诊断或者watch dog这样的特殊用途场景。而ARM的FIQ主要用于那些有快速响应要求的外设。为此,ARM cpu的设计者还有一些特别的设计: 1、更多的该mode下的寄存器。除了lr和sp之外,R8~R12也是特有的。这时候,如果仅仅使用FIQ mode下的寄存器而不访问R0~R7的话,进出该mode都不需要保存现场和回复现场的动作,节约了cpu指令。 2、FIQ vector处于vector table的最后一项,其他的异常向量都需要一个跳转指令,但是FIQ handler可以直接是handler而不需要跳转指令 然而,来到linux这个场景,事情又变得不一样了。linux本身没有支持ARM处理器的这个特性,毕竟不是每一个CPU都实现了类似ARM这样快速中断的机制。因此,在linux中,FIQ的使用更像是NMI的中断。因此,在linux kernel中实现FIQ本来的设计目标:快速中断,还是比较困难的。 对于FIQ在linux kernel中的使用,我的观点是这样的:不要用于一般的外设功能。如果系统的确有对某个中断的快速响应的要求,建议使用IRQ,辅以interrupt thread这样的特性还是可以达到很好的效果的。 另外,你说的那个方案我没有听说过,我觉得GIC似乎不支持这种特性。
          • ayeu0425

            @linuxer:谢谢博主的解答,你绝对是我见过的最热心和最有耐心的博主,没有之一。我会认真的考虑你的建议。多谢多谢!
            • RobinHsiang

              @ayeu0425:Linuxer和Wowo太好了。现在论坛人气越来越高了。 估计里面也有不少我这样的菜鸟,在这里面学习,关键是遇到疑难问题可以问两位博主,两位博主还帮忙耐心解答~~~ 呵呵,希望两位博主能收徒弟,将来发扬光大成立一大门派,两位也成为一代宗师啊~~~
              • linuxer

                @RobinHsiang:不敢当,RobinHsiang同学过奖了,我们只是中国众多普通的软件工程师中的两个,希望可以分享、整理自己的一些技术心得而建立了这样一个网站,如果能够给广大人民群众一些帮助当然更好。希望常来,大家一起讨论技术。
        • bsp

          @ayeu0425:1、arm的GIC一般是GIC一般支持,软件操作GIC的对应寄存器来 使能对应中断的pending位; 2、针对FIQ下 不能调用相应变量,可以从FIQ模式下切换到SVC模式,再去调用。这点可参考google的glue-fiq,其作用是用于记录系统 咬狗时的临终遗言。
  • passerby

    @linuxer 我在看高通的64位代码,在64位的secondary_start_kernel函数中并没有看到调用cpu_init做irq und和abt的栈初始化,是不是64位相比32位的这方面有了变化?
    • passerby

      @passerby:armv8好像不是用 irq und abt svc这些模式了,而是使用了el0 el1等表示特权等级,感觉向X86靠拢了。
      • wowo

        @passerby:ARMv8的异常模式已经很不一样了,忘掉之前的吧……
        • passerby

          @wowo:linuxer有空的时候可以分析下armv8的异常向量表建立过程吗?刚看了他分析32位的时候的情况,但是现在armv8有了很大变化,汇编指令也变了很多。坐等大神授道解惑。
          • linuxer

            @passerby:好的,我马上就有时间了,可以考虑一下这个主题
  • passerby

    @linuxer,中断发生在内核时会有 blne svc_preempt进行进程切换,那么在用户时为什么没有看到相应的代码?原来看到内核抢占发生时机是schedule显示调用和中断返回时,而用户抢占发生在系统调用返回和中断返回时。那么这里怎么没有看到用户抢占的发生时机呢?
    • linuxer

      @passerby:在bne work_pending 这里
      • passerby

        @linuxer:谢谢,真的在这里。
  • Andy

    博主,这条汇编有些没看明白:THUMB( adr r0, 1f )----根据当前PC值,获取lable 1的地址 ;能否讲解一下呢,主要有些不明白的地方时怎么将1f的地址赋值给r0,就获取了lable 1的地址?
    • Andy

      @Andy:这个1f是不是就是下面的 1: .endm 这里这个label 1的地址?如果是,1f中的f是什么意思?
      • 郭健

        @Andy:f表示forward,1f表示前面的“1”这个lable b表示backword,1b表示后面的“1”这个lable
        • Andy

          @郭健:之前没怎么看过汇编,明白了。感谢~~
    • 郭健

      @Andy:adr是获取1f这个符号的运行时地址,我们知道adr这条指令和1f这个符号之间的offset是固定的,实际上这里的r0实际上就等于PC + offset,从而获取了lable 1的运行时地址。如果还是不明白,可以看看ARM手册,那里说的更清楚的
  • harryxp

    __vectors_start: W(b) vector_rst W(b) vector_und W(ldr) pc, __vectors_start + 0x1000 W(b) vector_pabt W(b) vector_dabt W(b) vector_addrexcptn W(b) vector_irq ---------------------------IRQ Vector W(b) vector_fiq 博主,linux2.6上述代码后面有加stubs_offset,这个值与early_trap_init函数的0x200有关,现在变成0x10000,而且stubs_offset没有定义,是怎么回事呢?希望能帮解答下!
    • passerby

      @harryxp:现在申请了2个page来做异常向量表,0x1000就是4096bytes就是一个4k的页。而没有用stubs_offset,应该是在编译脚本中指定了的吧 __vectors_start = .; .vectors 0 : AT(__vectors_start) { *(.vectors) } . = __vectors_start + SIZEOF(.vectors); __vectors_end = .; __stubs_start = .; .stubs 0x1000 : AT(__stubs_start) { *(.stubs) } . = __stubs_start + SIZEOF(.stubs); __stubs_end = .;
      • harryxp

        @passerby:是的,这里指定了,谢谢。
  • jyzxlsw@126.com

    博主,请教一个问题,在中断后,irq模式向svc模式跳转时,觉得THUMB( ldr lr, [r0, lr, lsl #2] )已经完成地址计算了,应该跳转的地址已经在lr了里面了,为啥后面还要用pc再去计算一次ARM( ldr lr, [pc, lr, lsl #2] ) 呢? 这个地方看的不是很明白,顺便再问已经异常向量表的W(b),b我知道是跳转,W()是什么意思?望博主指点一下。 and lr, lr, #0x0f---lr保存了发生IRQ时候的CPSR,通过and操作,可以获取CPSR.M[3:0]的值 这时候,如果中断发生在用户空间,lr=0,如果是内核空间,lr=3 THUMB( adr r0, 1f )----根据当前PC值,获取lable 1的地址 THUMB( ldr lr, [r0, lr, lsl #2] )-lr根据当前mode,要么是__irq_usr的地址 ,要么是__irq_svc的地址 mov r0, sp------将irq mode的stack point通过r0传递给即将跳转的函数 ARM( ldr lr, [pc, lr, lsl #2] )---根据mode,给lr赋值,__irq_usr或者__irq_svc movs pc, lr @ branch to handler in SVC mode-----(4) ENDPROC(vector_\name)
    • hello_world

      @jyzxlsw@126.com:这是为了兼容ARM和THUMB两种处理器状态,代码如下: ----------------------------- #ifdef CONFIG_THUMB2_KERNEL #if __GNUC__ < 4 #error Thumb-2 kernel requires gcc >= 4 #endif /* The CPSR bit describing the instruction set (Thumb) */ #define PSR_ISETSTATE PSR_T_BIT #define ARM(x...) #define THUMB(x...) x #ifdef __ASSEMBLY__ #define W(instr) instr.w #define BSYM(sym) sym + 1 #else #define WASM(instr) #instr ".w" #endif #else /* !CONFIG_THUMB2_KERNEL */ /* The CPSR bit describing the instruction set (ARM) */ #define PSR_ISETSTATE 0 #define ARM(x...) x #define THUMB(x...) #ifdef __ASSEMBLY__ #define W(instr) instr #define BSYM(sym) sym #else #define WASM(instr) #instr #endif #endif /* CONFIG_THUMB2_KERNEL */ ---------------------------- ARM()和THUMB()中的指令只会执行其一。
  • david

    @bsp,谢谢,smp情况下的中断还是各种flow还是比较多啊,中断线source N 触发硬中断,在source N 的GIC EOI被操作之前,即使source N硬件产生中断时间,GIC N不会assert irq给任何cpu core的吧。 看了high level handler函数handle_fasteoi_irq() 470 if (desc->istate & IRQS_ONESHOT) 471 mask_irq(desc); 472 473 preflow_handler(desc); 474 handle_irq_event(desc); 475 476 if (desc->istate & IRQS_ONESHOT) 477 cond_unmask_irq(desc); 478 479 out_eoi: 480 desc->irq_data.chip->irq_eoi(&desc->irq_data); 只有IRQS_ONESHOT情况下才会mask 中断source,我的问题是在EOI之前,同一中断source的外设又有中断产生的话GIC还会assert 中断并分配给CPU吗?如果分配的话,就有可能分配到正在处理中断的CPU了?
    • bsp

      @david:1、硬中断到达CPU时,cpu硬件会关闭local-irq(CPSR_I置1)、更新LR、设置PC到相应异常向量,然后才会进入entry-armv.S中的汇编代码中。除非代码将本地中断打开,否则硬中断流程中不会响应其它任何中断的(3.10内核)。 2、GIC的中断状态: Inactive,中断尚未激活或挂起; Pending,中断已经由硬件或者软件产生,正等待目标CPU处理; Active,CPU已经应答(Acknowledge)来自GIC的中断,该中断正在被CPU处理但尚未完成; Active&Pending,CPU正在处理该中断,此时GIC又收到来自该中断源的中断; 第四种是你说的情况,CPU处理中断过程中(本地中断关闭,禁止中断抢占)、同一中断又产生时,GIC会分配其给相应CPU,只不过CPU要等到 本中断处理完成 才会处理下一个中断。 cpu-core和GIC,是SOC上的两个独立的不同单元。 3、有的关注实时性的定制内核,在处理硬中断时 会将本地中断打开、允许中断抢占;这时只有高优先级中断到来时、才会打断本中断流程,中断嵌套次数也有控制(一般3次)。 硬中断流程被打断时,cpu处理完高优先级的中断后 又返回到这个原来本打断的地方 继续原来的硬中断流程。 这个第3点不用关注哈,仅作了解。
      • david

        @bsp:@bsp.看了一下GIC spec. 比较清楚了,GIC不会把正在active/active and pending的 IRQ在signal给任何的CPU的。所以同一个irq在GIC这边也只能串行的分配给CPU的。 A CPU interface never signals to the connected processor any interrupt that is active and pending. It only signals interrupts that are pending and have sufficient priority: • For PPIs and SGIs, the active status of particular interrupt ID is banked between CPU interfaces. This means that if a particular interrupt ID is active or active and pending on a CPU interface, then no interrupt with that same ID is signaled on that CPU interface. • For SPIs, the active status of an interrupt is common to all CPU interfaces. This means that if an interrupt is active or active and pending on one CPU interface then it is not signaled on any CPU interface.
  • Harm

    楼主对ARM很熟悉啊 赞一个
  • Harm

    有个问题请教下:如果把HCR.IMO bit置为1去拦截特殊的中断,当中断发生时,cpu就会进入HYP mode,在hyp irq hanlder里面需要怎么做才能把其余(非特殊)的中断交还给kerenl处理?
  • xie1230

    有一个地方不理解的是,中断控制器不管怎么级联,最终是CPU接收到了中断信号,这个应该是属于硬件中断,获取到的也应该是HW ID,cpu进入中断模式,然后又进入SVC模式,可是一直不解的是,接下来怎么获取到的相应的irq num了? 这个转换过程是在哪里做的,是直接把转换后的irq num作为寄存器参数传入的吗?还是在handle_arch_irq里面才开始做的转换?具体是怎么的转换的呢
    • linuxer

      @xie1230:建议看看/irq_subsystem/irq-domain.html,这份文档描述了从HW interrupt ID到IRQ number之间的转换过程。
      • lamaboy

        @linuxer:下面的代码看看就明白了 set_handle_irq(gic_handle_irq); static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs) { u32 irqstat, irqnr; struct gic_chip_data *gic = &gic_data[0]; void __iomem *cpu_base = gic_data_cpu_base(gic); do { irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK); irqnr = irqstat & GICC_IAR_INT_ID_MASK; if (likely(irqnr > 15 && irqnr < 1020)) { if (static_key_true(&supports_deactivate)) writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI); handle_domain_irq(gic->domain, irqnr, regs); continue; } if (irqnr < 16) { writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI); if (static_key_true(&supports_deactivate)) writel_relaxed(irqstat, cpu_base + GIC_CPU_DEACTIVATE); #ifdef CONFIG_SMP handle_IPI(irqnr, regs); #endif continue; } break; } while (1); }
  • borman

    有一个疑问,在bottom half执行的过程中是开中断的,如果这个过程中又发生了外设中断,那么之前那个中断的现场怎么恢复
    • linuxer

      @borman:当调用bottom half的时候,上一次中断的上下文(我们称之A中断)保存在了内核栈上,如果这个过程中又发生了外设中断(我们称之B中断),那么该中断的上下文依然保存在内核栈上,当B中断的handler处理完毕的时候,首先返回B中断的现场,也就是返回了被B中断打断执行的bottom half的现场,恢复其执行,当bottom half执行完毕,从内核栈恢复A中断的现场。
  • qkhhyga_2016

    Hi,wowo 想请问一下,如果cpu处于休眠状态,被外部中断唤醒后,是在何时跳到异常向量表的IRQ向量的?如果等CPUINIT 之后再跳转,那是否可认为中断控制器有pending的功能?谢谢!
    • linuxer

      @qkhhyga_2016:系统唤醒之后是处于关中断状态的,因此,中断处理一定会先等待resume处理到了一个合适的点,然后打开中断,这时候,pengding的中断立刻被按照正常的中断流程处理,完成之后,继续resume的过程。
      • qkhhyga_2016

        @linuxer:还有个问题请教一下,gicc_iar 寄存器的低9位是否自动清位的?我的意思是当cpu去读这个寄存器时,读完后自动清位? 因为我在gic代码里看到如下逻辑 static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs) { ... do { irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK); irqnr = irqstat & GICC_IAR_INT_ID_MASK; if (likely(irqnr > 15 && irqnr < 1021)) { handle_domain_irq(gic->domain, irqnr, regs); continue; } ... break; } while (1);
        • linuxer

          @qkhhyga_2016:呵呵~~我推荐你去GIC的文档中自己去找答案,GIC datasheet中往往可以获得对各个寄存器的详细解释。
  • star

    Hi 博主, 我最近在看 svc handler,有一个问题不明白,用户模式进入 svc 模式的过程中,为什么需要保存 用户模式下的 sp 和lr,按照文档上的描述,用户模式下的 sp 和 lr 是banked 应该不需要保存才对。
    • linuxer

      @star:进程A在欢快的运行,发生了中断,陷入内核,完成处理之后,如果中断返回是回到进程A继续执行,那么你说的逻辑是通的,但是,中断返回一定是返回进程A的现场吗?
      • star

        @linuxer:理解了,最近重新看中断异常,竟然看到之前留过言。
  • Jialing Fu

    关于中断返回时PC的恢复问题,这个是一个硬件设计向前兼容软件设计的结果。 一开始ARM的设计比较简单,只有3级流水线:取指,译码,执行。所以一条指令结束时,CPU已经预先读取了两条指令,中断跳转是一条指令执行结束时,这样PC = address + 4(Thumb,一条指令2Byte) 或 + 8。所以中断返回时,PC值需回退一条指令。 后续ARM增加了更多流水线级数,但为了软件设计的方便和向前兼容,ARM硬件做了一定的支持:就是软件还是可以把PC值回退一条指令即可。不然软件要根据不同流水线级数做不同的处理,就太复杂了。
  • randyqiu

    请教一个问题:“SVC模式的stack准备”中,比如在用户态陷入内核态,使用的svc stack应该就是各task自己的kernel stack(2 page frame size)吧?此时的sp_svc是如何切换到各task的kernel stack中的呢?
    • randyqiu

      @randyqiu:好吧,我知道了,arm维护了几个sp寄存器,内核态比如spL1,用户态spL0,当task调度时会把当前task的sp指针放到spL1中,这样从用户态陷入内核态时只需要直接用spL1即可
  • 风露

    蜗窝大神,求教一个问题: 进入IRQ mode后,中断会被自动禁掉,正常情况下,什么时候会再开启中断?
  • passerby

    在arm64的do_signal()中,如果系统调用需要restart,则要修改PC值。 但是这里restart_addr = continue_addr - (compat_thumb_mode(regs) ? 2 : 4); 依然与arm32一样 按道理应该是-4或者-8才对呢
  • Physh

    大侠,求教一个问题:我用perf top分析一个arm-linux进程,结果top symbols如下: 4.27% [vectors] [.] 0x00000fc4 3.84% [kernel] [k] _raw_spin_unlock_irqrestore 2.30% [kernel] [k] _raw_spin_unlock_irq 1.94% libc-2.20.so [.] 0x0007c35c 1.91% [vectors] [.] 0x00000fd8 1.56% libGLESv2.so.1.9.6.0 [.] 0x0003a5e0 1.34% libGLESv2.so.1.9.6.0 [.] 0x0003a5cc 其中,第一行 [vectors] 这行尤其显得奇怪,这行有可能是代表irq handlers吗?我看到_raw_spin_unlock_irqrestore 占比很重,系统中应该是interrupt-heavy,但perf profile是基于PMU,ARM平台PMU应该还不是不可屏蔽中断,不太可能可以落到irq handlers内才对。 疑惑不已,求教。
    • Physh

      @Physh:研究了下,已经理清这个东西,希望有助后者: 4.27% [vectors] [.] 0x00000fc4 3.84% [kernel] [k] _raw_spin_unlock_irqrestore 2.30% [kernel] [k] _raw_spin_unlock_irq 1.94% libc-2.20.so [.] 0x0007c35c 1.91% [vectors] [.] 0x00000fd8 1.56% libGLESv2.so.1.9.6.0 [.] 0x0003a5e0 1.34% libGLESv2.so.1.9.6.0 [.] 0x0003a5cc 上文中,4.27% [vectors] [.] 0x00000fc4 确实表明这个地址位于进程空间,是[verctors]中偏移量为0x00000fc4的func,这个地方是用户kuser_helper,一般是用来实现原子操作等用户空间无法实现需要内核提供帮助实现的函数,在本例中0x00000fc4 是kuser_cmpxchg64。
  • 太空的树懒

    蜗窝大侠,你好,请问一下: 保存中断现场那一段里有一句: “一旦进入SVC mode,ARM处理器看到的寄存器已经发生变化,这里的sp已经变成了sp_svc了。因此,后续的压栈操作都是压入了发生中断那一刻的进程的(或者内核线程)内核栈(svc mode栈)” 这一句里怎么能看出来 sp_svc 指向的是 发生中断时的那个进程的内核栈呢
    • 明天

      @太空的树懒:讲的很清楚了 (4)很多异常处理的代码返回的时候都是使用了stack相关的操作,这里没有。“movs pc, lr ”指令除了字面上意思(把lr的值付给pc),还有一个隐含的操作(movs中‘s’的含义):把SPSR copy到CPSR,从而实现了模式的切换。 模式的切换是靠修改cpsr寄存器实现的
  • zgq

    中断发生在user mode下的退出过程: add sp, sp, #S_FRAME_SIZE - S_PC----现场已经恢复,移动svc mode的sp到原来的位置 --看了下代码,这个是thumb2指令集下恢复SP的代码, --ARM指令集恢复SP的代码为: add sp, sp, #\offset + S_FRAME_SIZE
  • andreaji

    ``` .macro svc_entry, stack_hole=0 sub sp, sp, #(S_FRAME_SIZE + \stack_hole - 4)----sp指向struct pt_regs中r1的位置 stmia sp, {r1 - r12} ------寄存器入栈。 ldmia r0, {r3 - r5} add r7, sp, #S_SP - 4 ------r7指向struct pt_regs中r12的位置 mov r6, #-1 ----------orig r0设为-1 add r2, sp, #(S_FRAME_SIZE + \stack_hole - 4)----r2是发现中断那一刻stack的现场 str r3, [sp, #-4]! ----保存r0,注意有一个!,sp会加上4,这时候sp就指向栈顶的r0位置了 mov r3, lr ----保存svc mode的lr到r3 stmia r7, {r2 - r6} ---------压栈,在栈上形成形成struct pt_regs .endm ``` ------------------------------------------------------------------------------- linuxer 您好,我认为这里的 `add r7, sp, #S_SP - 4` 应该是让 r7 指向 ARM_sp 。如果是指向 ARM_r12 , 应该是 add r7, offsetof(ARM_r11) 即可。 这里之所以是 add r7, offsetof(ARM_SP)-4 ,我个人理解是因为此前 sp_svc 指向 ARM_r1 , 在此基础上 + offsetof(ARM_SP) 会让 r7 指向到 ARM_lr ,所以需要 -4 以修复偏差。请您看看我的理解对否。
  • fengeryi

    能讲这么清楚真的太难得了,非常感谢
  • Uchiha.tan

    “3、mask IRQ exception。也就是设定CPSR.I = 1” 这里有错误吧?这个位设为1,就表示禁止了中断,那之后的中断岂不是就没法进行了?硬件应该没设定CPSR.I = 1
  • 明天

    有两种情况会恢复:1退出中断的时候,因为会恢复cpsr寄存器;2、软中断soft_irq中会打开中断
  • ecjtusbs 主页

    2022年12月14日 读了好几遍,每次感觉受益匪浅! 前人栽树后人乘凉。感谢前辈的无私分享。