SylixOS 下的中断嵌套
本文以 ARMv7 架构为例,讲解 SylixOS 操作系统下的中断嵌套。
本篇文章阅读的前提,需要读者对 ARM 架构有一定的了解
建议先阅读:ARM 学习笔记(一)
文章部分摘自:这可能是最通俗易懂的方式讲解ARM中断原理以及中断嵌套
1、概念
1.1 网络上的概念
中断嵌套指中断系统正在执行一个中断服务 L 时,有另一个优先级更高的中断 H 触发,这时中断系统会暂时中止当前正在执行低优先级中断服务程序 L,而去处理高级别中断 H,待处理完毕,再返回被中断了的中断服务程序 L 继续执行。
所谓中断嵌套,就是中断抢占机制,允许高优先级中断源抢占正在执行的低优先级中断。
上面这种大的宽的话,在互联网上很常见。只能模糊的理解,一旦找工作、面试被问到了相关问题,就是尴尬
1.2 更详细的概念
我们还是以 ARM 架构为例,ARM 有七种模式,我们这里只讨论 SVC、IRQ 和 FIQ 模式。我们可以假设 ARM 核心有两根中断引脚(实际上是看不见的),一根叫 irq pin, 一根叫 fiq pin。在 ARM 的 cpsr 中,有一个 I 位和一个 F 位,分别用来禁止 IRQ 和 FIQ。
先不说中断控制器,只说 ARM 核心。正常情况下,ARM 核都只是机械地随着 pc 的指示去做事情,当 CPSR 中的 I 和 F 位为 1 时,IRQ 和 FIQ 全部处于禁止状态。无论你在 irq pin 和 fiq pin上面发什么样的中断信号,ARM 不会理你,你根本不能打断它,因为它“耳聋”,"眼瞎"了。
当 I 位和 F 位为 0 时,irq pin上有中断信号过来时,就会打断 ARM 的当前工作,并且切换到 IRQ 模式下,跳到相应的异常向量表(vector)位置去执行代码。这个过程是自动的,但是返回到被中断打断的地方就得您亲自动手。
当你跳到异常向量表,处于 IRQ 的模式的时候,此时如果 irq pin上面又来中断信号,此时 ARM 是不会理你的,irq pin 就像秘书,ARM 核心就像老板,老板本来在做事,然后来了一个客户,秘书打断它,让客户进去。而此时再来一个客户,要么秘书不断去敲门问,要么客户走人。老板第一个客户没有会见完,不会理你。 但是有一种情况例外,当 ARM 处在 IRQ 模式,这个时候 fiq pin 来了一个中断信号,fiq pin 是什么?快速中断,好比公安局的来查刑事案件,才不管老板是不是在会见客户,直接打断,进入到fiq 模式,跳到相应的 fiq 的异常向量表处去执行代码。那如果当 ARM 处理 FIQ 模式,fiq pin 又来中断信号,也就是又一批公安来了,那没戏,都是执法人员,你打不断我。如果此时 irq pin 来了呢?来了也不理,正在办案,还敢来妨碍公务。 所以得出一个结论: IRQ 模式只能被 FIQ 模式打断,FIQ 模式下谁也打不断。 在打不断的情况下,irq pin 或 fiq pin 随便你怎么发中断信号,都是白发。 所以除了 fiq 能打断 irq 以外,根本没有所谓中断嵌套的情况。
看到这里,你会不会有疑问,上面得出的结论,怎么和 1.1 章节有冲突?
实际上,众多的 RTOS,为了达到中断嵌套的目的,通常会在软件上,进入 IRQ 中断处理后,手动将当前核的中断打开,也就是置 CPSR 的 I 位为 0。这时,就会存在 IRQ 打断 IRQ 的情况了
其实对于 ARM CPU 核来说,它是不知道所谓的中断优先级、中断号的概念的。CPU 会做的事,只是 “irq pin上有中断信号过来时,就会打断 ARM 的当前工作,并且切换到 IRQ 模式下,跳到相应的异常向量表(vector)位置去执行代码”。对于中断号、中断优先级的概念,完全是由中断控制器决定的。关于中断控制器,后面会单独出一个章节详解。
1.3 中断嵌套的现状
现在很多的操作系统,通常都不会使用中断嵌套(虽然支持,但是很少使用)。例如 Linux 内核,从某一个版本开始,就不再支持中断嵌套,给出的理由是,为了防止栈溢出。
kernel/git/torvalds/linux.git - Linux kernel source tree
我们在使用中断嵌套时,要考虑到中断嵌套实现的复杂度、多级中断上下文切换带来的时间开销、栈溢出问题等。
很多 RTOS 实现的抢占式内核,也就是抢占式调度,已经可以满足实时性要求。例如:快速中断上下文 + 软中断(softirq)/tasklet 工作机制,完全可以替代中断嵌套带来的优势
- 硬中断中只做最小处理(例如:确认、屏蔽、记录)
- 剩下的耗时处理在软中断或工作队列中完成
- 由优先级调度器来完成对高优先级任务的响应、处理
以上这些,大大降低了操作系统对中断嵌套依赖。
2、实现
下面以 SylixOS 操作系统为例,讲解中断嵌套的实现。完整的代码放在最后,本章节进行详细的代码拆解。
2.1 中断上下文保存
;/*********************************************************************************************************
; 中断入口
;*********************************************************************************************************/FUNC_DEF(archIntEntry)SUB LR , LR, #4 ;/* 调整用于中断返回的 PC 值 */STMFD SP!, {LR} ;/* 保存返回地址 */STMFD SP!, {R0-R12} ;/* 保存寄存器 */MOV R1 , SPMSR CPSR_c, #(DIS_INT | SYS32_MODE) ;/* 回到 SYS 模式 */STMFD R1!, {SP} ;/* 保存 SP_sys */STMFD R1 , {LR} ;/* 保存 LR_sys */MSR CPSR_c, #(DIS_INT | IRQ32_MODE) ;/* 回到 IRQ 模式 */SUB SP , SP , #(2 * 4) ;/* 调整 SP_irq */MRS R2 , SPSRSTMFD SP!, {R2} ;/* 保存 CPSR_sys */
注意,这里为什么要保存 SP_sys 和 LR_sys? 首先有一个大的前提,要知道,SP 和 LR 这两个寄存器在每种模式下都是独立的寄存器,决定着当前模式正在使用的一个栈空间。
因为中断处理函数通常在 SYS 模式运行。而在进行中断处理的过程中,势必会破坏之前的栈。2.3 章节中,详细的提到了,切换 SYS 模式栈的工作。所以这里提前保存,所有中断处理结束后,再去恢复。
即便是中断嵌套的情况下,也是需要保存 SP_sys 和 LR_sys。因为当第一个中断正在处理的过程中,第二个中断来了,所以需要保存上一次中断处理的现场,包括栈信息。
以上代码执行结束,IRQ 模式栈情况如下:
2.2 中断上下文拷贝
;/*; * API_InterEnter(SP_irq), 如果是第一次中断, 会将 IRQ 模式栈空间的 ARCH_REG_CTX; * 拷贝到当前任务 TCB 的 ARCH_REG_CTX 里; */MOV R0 , SPLDR R1 , =API_InterEnterMOV LR , PCBX R1
API_InterEnter
函数实现的主要功能是,将刚刚保存到 IQR 模式栈空间的这些中断上下文ARCH_REG_CTX
,再重新保存一遍到当前任务 TCB 中。当所有中断处理结束时,需要恢复中断上下文时,使用的就是 TCB 中保存的中断上下文。
需要注意的是,仅仅是第一次进入中断时,才把 IQR 模式栈空间的这些上下文,保存到当前任务 TCB 中!!!
VOID archIntCtxSaveReg (PLW_CLASS_CPU pcpu,ARCH_REG_T reg0,ARCH_REG_T reg1,ARCH_REG_T reg2,ARCH_REG_T reg3)
{if (pcpu->CPU_ulInterNesting == 1) {archTaskCtxCopy(&pcpu->CPU_ptcbTCBCur->TCB_archRegCtx, (ARCH_REG_CTX *)reg0);}
}LW_API
ULONG API_InterEnter (ARCH_REG_T reg0,ARCH_REG_T reg1,ARCH_REG_T reg2,ARCH_REG_T reg3)
{PLW_CLASS_CPU pcpu;pcpu = LW_CPU_GET_CUR();pcpu->CPU_ulInterNesting++;#if !defined(__SYLIXOS_ARM_ARCH_M__) || (LW_CFG_CORTEX_M_SVC_SWITCH > 0)archIntCtxSaveReg(pcpu, reg0, reg1, reg2, reg3);
#endif......return (pcpu->CPU_ulInterNesting);
}
这里实际上使用的是 IRQ 模式的栈。
;/*********************************************************************************************************
; 拷贝任务上下文
; 参数 R0 为目的 ARCH_REG_CTX 指针, R1 为源 ARCH_REG_CTX 指针
;*********************************************************************************************************/FUNC_DEF(archTaskCtxCopy)STMFD SP! , {R4-R10} //寄存器 r4~r10 共 7 个寄存器,存储到 SP 指向的栈中,相当于入栈LDMIA R1! , {R2-R10} //从 R1 指向的内存地址开始,依次将内容加载到 R2–R10STMIA R0! , {R2-R10} //将刚才加载的寄存器值存储到 R0 指向的目标地址,并写回更新 R0LDMIA R1! , {R2-R9} //从 R1 当前地址(已偏移 36 字节)继续读取 8 个寄存器值(R2–R9)STMIA R0! , {R2-R9} //然后写入到 R0 当前地址(也偏移了36字节)LDMFD SP! , {R4-R10} //SP 指向的栈中,恢复到 r4~r10 共 7 个寄存器,相当于出栈BX LRFUNC_END()FILE_END()
2.3 清栈 & 调整 SYS 模式栈
;/*; * 如果不是第一次进入中断, 那么上一次中断(工作在 SYS 模式)已经设置 SP_sys, 只需要回到 SYS 模式; */CMP R0 , #1BNE 1f;/*; * 第一次进入中断: 因为已经将 IRQ 模式栈空间的 ARCH_REG_CTX 拷贝到当前任务 TCB 的 ARCH_REG_CTX 里; * 调整 SP_irq; */ADD SP , SP , #(ARCH_REG_CTX_SIZE);/*; * 第一次进入中断: 获得当前 CPU 中断堆栈栈顶, 并回到 SYS 模式, 并设置 SP_sys; */LDR R0 , =API_InterStackBaseGetMOV LR , PCBX R0MSR CPSR_c, #(DIS_INT | SYS32_MODE) ;/* 回到 SYS 模式 */MOV SP , R0 ;/* 设置 SP_sys */
这里注意,在第一次进入中断的情况下,因为上面已经把 ARCH_REG_CTX
上下文保存到 TCB 中,所以 IRQ 模式栈中的上下文数据实际上已经不需要了,所以修改 IRQ 模式的 SP 指针(可以理解为清栈)。同时,为了后面的中断处理函数 bspIntHandle
—— 这样一个 C 函数能够顺利执行,需要重新给 SYS 模式切换栈空间。
这里给 SYS 模式切换的栈,是操作系统分配的栈空间。为什么不直接使用 SYS 模式下的异常栈?
因为系统刚上电后执行的初始化操作,设置 ARM 异常栈时,通常不会设置的很大。但是这里需要调用一个 C 函数去做中断处理,可能会有多级调用、需要很大的栈空间
在上面的操作过后, IRQ 模式栈空间情况如下:
而对于非第一次进入中断、也就是中断嵌套的情况下,则无需切换 SYS 模式的栈空间。同时,也不能修改 IRQ 模式的 SP。因为中断嵌套情况下,是不会保存 IRQ 上下文到当前 TCB 中的。
2.4 中断处理
1:MSR CPSR_c, #(DIS_INT | SYS32_MODE) ;/* 回到 SYS 模式(不是多余的) */;/*; * bspIntHandle(),中断处理过程; */LDR R1 , =bspIntHandleMOV LR , PCBX R1;/*; * API_InterExit(); * 如果没有发生中断嵌套, 则 API_InterExit 会调用 archIntCtxLoad 函数, SP_irq 在上面已经调整好; */LDR R1 , =API_InterExitMOV LR , PCBX R1
这里要注意,实际上中断处理的过程,是在 SYS 模式操作的,并不是 IRQ 模式。bspIntHandle
最终会调用到 archIntHandle
/*********************************************************************************************************
** 函数名称: archIntHandle
** 功能描述: bspIntHandle 需要调用此函数处理中断 (关闭中断情况被调用)
** 输 入 : ulVector 中断向量
** bPreemptive 中断是否可抢占
** 输 出 : NONE
** 全局变量:
** 调用模块:
** 注 意 : 此函数退出时必须为中断关闭状态.
*********************************************************************************************************/
LW_WEAK VOID archIntHandle (ULONG ulVector, BOOL bPreemptive)
{
......if (bPreemptive) {VECTOR_OP_LOCK();__ARCH_INT_VECTOR_DISABLE(ulVector); /* 屏蔽 vector 中断 */VECTOR_OP_UNLOCK();KN_INT_ENABLE_FORCE(); /* 允许中断 */}irqret = API_InterVectorIsr(ulVector); /* 调用中断服务程序 */
......
}
由上面代码可以看出,为了达到中断嵌套的目的,在刚进 archIntHandle
函数时,就需要调用 KN_INT_ENABLE_FORCE
强制打开当前核的中断,否则当前核无法产生 IRQ 中断。
而 API_InterExit
函数中,如果是第一次进入中断,则会进行中断上下文切换;如果不是,则直接返回;
关于中断上下文切换,请看:SylixOS armv7 任务切换
LW_API
VOID API_InterExit (VOID)
{
......if (pcpu->CPU_ulInterNesting) { /* 查看系统是否在中断嵌套中 */ return; /* LW_CFG_INTER_DSP > 0 */}......#if !defined(__SYLIXOS_ARM_ARCH_M__) || (LW_CFG_CORTEX_M_SVC_SWITCH > 0)archIntCtxLoad(pcpu); /* 中断返回 (当前任务 CTX 加载)*/
#endif
}
2.5 中断嵌套的处理
;/*; * 来到这里, 说明发生了中断嵌套; */MSR CPSR_c, #(DIS_INT | IRQ32_MODE) ;/* 回到 IRQ 模式 */MOV R0 , SPLDMIA R0!, {R2-R4} ;/* 读取 CPSR LR SP */ADD SP , SP , #(ARCH_REG_CTX_SIZE) ;/* 调整 SP_irq */MSR CPSR_c, #(DIS_INT | SYS32_MODE) ;/* 回到 SYS 模式 */MOV SP , R4 ;/* 恢复 SP_sys */MOV LR , R3 ;/* 恢复 LR_sys */MSR CPSR_c, #(DIS_INT | IRQ32_MODE) ;/* 回到 IRQ 模式 */MSR SPSR_cxsf , R2LDMIA R0 , {R0-R12, PC}^ ;/* 恢复包括 PC 的所有寄存器, */;/* 同时更新 CPSR */FUNC_END()
如果发生了中断嵌套,且当前中断已经处理完毕,则可以返回到上一层的中断现场。即恢复 IRQ 栈中保存的 ARCH_REG_CTX
上下文,这其中,包括 PC。
3、完整的代码片段
;/*********************************************************************************************************
; 中断入口
;*********************************************************************************************************/FUNC_DEF(archIntEntry)SUB LR , LR, #4 ;/* 调整用于中断返回的 PC 值 */STMFD SP!, {LR} ;/* 保存返回地址 */STMFD SP!, {R0-R12} ;/* 保存寄存器 */MOV R1 , SPMSR CPSR_c, #(DIS_INT | SYS32_MODE) ;/* 回到 SYS 模式 */STMFD R1!, {SP} ;/* 保存 SP_sys */STMFD R1 , {LR} ;/* 保存 LR_sys */MSR CPSR_c, #(DIS_INT | IRQ32_MODE) ;/* 回到 IRQ 模式 */SUB SP , SP , #(2 * 4) ;/* 调整 SP_irq */MRS R2 , SPSRSTMFD SP!, {R2} ;/* 保存 CPSR_sys */;/*; * API_InterEnter(SP_irq), 如果是第一次中断, 会将 IRQ 模式栈空间的 ARCH_REG_CTX; * 拷贝到当前任务 TCB 的 ARCH_REG_CTX 里; */MOV R0 , SPLDR R1 , =API_InterEnterMOV LR , PCBX R1;/*; * 如果不是第一次进入中断, 那么上一次中断(工作在 SYS 模式)已经设置 SP_sys, 只需要回到 SYS 模式; */CMP R0 , #1BNE 1f;/*; * 第一次进入中断: 因为已经将 IRQ 模式栈空间的 ARCH_REG_CTX 拷贝到当前任务 TCB 的 ARCH_REG_CTX 里; * 调整 SP_irq; */ADD SP , SP , #(ARCH_REG_CTX_SIZE);/*; * 第一次进入中断: 获得当前 CPU 中断堆栈栈顶, 并回到 SYS 模式, 并设置 SP_sys; */LDR R0 , =API_InterStackBaseGetMOV LR , PCBX R0MSR CPSR_c, #(DIS_INT | SYS32_MODE) ;/* 回到 SYS 模式 */MOV SP , R0 ;/* 设置 SP_sys */1:MSR CPSR_c, #(DIS_INT | SYS32_MODE) ;/* 回到 SYS 模式(不是多余的) */;/*; * bspIntHandle(); */LDR R1 , =bspIntHandleMOV LR , PCBX R1;/*; * API_InterExit(); * 如果没有发生中断嵌套, 则 API_InterExit 会调用 archIntCtxLoad 函数, SP_irq 在上面已经调整好; */LDR R1 , =API_InterExitMOV LR , PCBX R1;/*; * 来到这里, 说明发生了中断嵌套; */MSR CPSR_c, #(DIS_INT | IRQ32_MODE) ;/* 回到 IRQ 模式 */MOV R0 , SPLDMIA R0!, {R2-R4} ;/* 读取 CPSR LR SP */ADD SP , SP , #(ARCH_REG_CTX_SIZE) ;/* 调整 SP_irq */MSR CPSR_c, #(DIS_INT | SYS32_MODE) ;/* 回到 SYS 模式 */MOV SP , R4 ;/* 恢复 SP_sys */MOV LR , R3 ;/* 恢复 LR_sys */MSR CPSR_c, #(DIS_INT | IRQ32_MODE) ;/* 回到 IRQ 模式 */MSR SPSR_cxsf , R2LDMIA R0 , {R0-R12, PC}^ ;/* 恢复包括 PC 的所有寄存器, */;/* 同时更新 CPSR */FUNC_END()