首发:嵌入式客栈
作者:逸珺
导读
对于驱动开发而言,中断机制是一个无法绕开的主题,翻看了很多资料书籍,读来读去总觉得没明白,所以尝试自底向上的分析一下Linux中断子系统的内在设计以及运行机制。将陆续分享相关的学习原创笔记,敬请关注期待。
代码分析基于内核5.4.31
啥是中断/异常
处理器的典型任务是处理一系列预定的程序。为了通知处理器某些事件,有时需要中断当前正在处理的任务。中断可以由程序触发,也可以从外设异步触发。如果发生中断请求IRQ,则处理器将执行预定的中断服务程序ISR。CPU需要能处理软件错误,例如除零或显式中断调用(例如syscalls)。硬件中断由中断控制器路由到指定的处理器(如x86系统的IO-APIC)。
操作系统的总体运行机制属于事件驱动(event-driven)机制。
中断:
- 由外部硬件生成的异步事件。
- 中断控制器芯片将每个IRQ输入映射到一个中断向量,该中断向量定位相应的中断服务程序
一个 IRQ 是来自某个设备的一个中断请求。可能的中断请求源:来自一个硬件引脚,或来自一个数据包。多个设备可能连接到同一个硬件引脚,从而共享一个 IRQ。抽象成中断请求事件,有哪些可能的事件呢:
- GPIO 中断请求,包括边沿、电平模式
- 外设 I2C的start/stop事件
- USB枚举,报文
- 网口......
异常(陷阱):由软件生成的同步事件,比如前面提到的除零,页错误
ARM32/ARM64硬件架构对比
ARM32
这里以ARMv7 为例进行描述,对于ARM32而言,CPU具有9大处理模式:
其中encoding是指的CPSR程序状态寄存器中的M[4:0],对于ARMv7而言,权限等级如下:
- PL0-适用于用户应用程序供应商,例如从App Store下载的应用程序。
- PL1-丰富的OS供应商,例如Android使用的Linux内核。
- PL2-虚拟机监控程序供应商。
- (Secure PL0)安全PL0-受信任的OS供应商提供的受信任的OS应用程序。
- (Secure PL1)安全PL1-受信任的操作系统。
- (Secure PL)安全PL1-提供安全固件的OEM。
ARMv7采用通用中断控制器GIC V2进行中断分发控制。
AArch64
自《Programmer’s Guide for ARMv8-A》,armv8 引入64位ARM架构,向后兼容。
- Cortex-A53处理器是一个中档、低功耗处理器,在单个集群中有一个到四个核,每个核都有一个L1缓存子系统、一个可选的集成GICv3/4(通用中断控制器Generic Interrupt Controller)接口和一个可选的L2缓存控制器。
- Cortex-A57处理器针对移动和企业计算应用,包括计算密集型64位应用,如高端计算机、平板电脑和服务器产品。它可以与Cortex-A53处理器一起采用big.LITTLE技术(有的也称为异步多核架构)成为一个处理器。
ARMv8-A体系结构引入了许多更改,这些更改使得可以设计性能明显更高的处理器实现:
- 更大的物理地址寻址能力:处理器可以访问超过4GB的物理内存。
- 64位虚拟寻址:这样可以使虚拟内存超过4GB的限制。这对于使用内存映射文件I / O或稀疏寻址的现代台式机和服务器软件很重要。
- 自动事件信号:利于实现低功耗、高性能的自旋锁。
- 更大寄存器文件:31个64位通用寄存器可提高性能并减少栈开销。
- 高效的64位立即数生成:较少对文字池的依赖。
- 更大的PC指针相对寻址空间:+/- 4GB寻址范围,可在共享库和与位置无关的可执行文件中进行有效的数据寻址。
- 额外的16KB和64KB翻译颗粒:这样可以减少转换后备缓冲区(TLB)的丢失率和页面遍历的深度。
- 新的异常模型: 降低了操作系统和管理程序软件的复杂性。
- 高效的缓存管理: 用户空间缓存操作提高了动态代码生成效率。使用数据高速缓存零指令快速清除数据高速缓存。
- 硬件加速密码器:提高3倍至10倍较好的软件加密性能。这对于小颗粒的解密和太小而无法有效地卸载到硬件加速器上的加密非常有用,例如https。
- Load-Acquire, Store-Release指令:针对C++11,C11,Java内存模型而设计。它们通过消除显式的内存屏障指令来提高线程安全代码的性能。
- NEON双精度浮点高级SIMD:这使SIMD矢量化可以应用于更广泛的算法集,例如科学计算,高性能计算(HPC)和超级计算机。
上面谈到了新的异常模型,这里来看一下具体是指什么:
在ARMv8中,程序运行总是处于四个异常级别之一。在AArch64中,异常级别确定特权级别,类似于ARMv7中定义的特权级别。异常级别确定特权级别,因此在ELn程序对应于特权PLn。类似地,n值大于另一个值的异常级别处于较高的异常级别。数量比另一个少的异常级别被描述为处于较低的异常级别。
异常级别提供了适用于ARMv8架构所有运行状态的软件执行特权的逻辑隔离。它类似于计算机科学中常见的分层保护域概念。
- EL0:普通用户应用程序。
- EL1:操作系统内核通常描述为特权。
- EL2:管理程序(Hypervisor)。
- EL3:底层固件,包括安全监视器。
通常,一个软件(例如应用程序,操作系统的内核或系统管理程序)占据一个异常级别。该规则的一个例外是内核内虚拟机管理程序,例如KVM,它们在EL2和EL1上都可运行。
可运行在AArch32以及AArch64模式下:
由图可见:在AArch32模式下,EL0相对于usr 模式,而EL1则相当于Svc、Abt、Und、FIQ、IRQ、Sys模式,而EL2则相当于Hyp模式。
ARMv8-A提供两种安全状态,即安全和非安全。非安全状态也称为正常模式(normal world)。这使操作系统(OS)与受信任的OS在同一硬件上并行运行,并提供了针对某些软件攻击和硬件攻击的保护。ARM TrustZone技术使系统可以在普通和安全环境之间进行分区。与ARMv7-A架构一样,安全监视器充当在普通和安全环境之间移动的网关。
ARMv8-A 在正常模式下还提供了对虚拟化的支持。从而虚拟机监控程序或虚拟机管理器(VMM)代码可以在系统上运行,并承载多个客户操作系统。每个客户操作系统本质上都运行在一个虚拟机上。每个操作系统就不会意识到它正在与其他客户操作系统共享CPU。
ARMv8-A采用通用中断控制器GIC V3进行中断分发控制。
在AArch64中,异常可以是(synchronous exception)同步的,也可以是( asynchronous exception)异步的。同步异常:如果异常是在执行或试图执行指令流时产生,并且返回地址提供导致该异常指令的详细信息,则将其描述为同步异常。
异步异常:异步异常不是由执行指令生成的,而返回地址可能并不总是提供导致异常的细节。异步异常的来源是IRQ(正常优先级中断),FIQ(快速中断)或SError(系统错误)。系统错误有许多可能的原因,最常见的是异步数据中止(例如,由将脏数据从缓存线写到外部内存而触发的中止)。
啥是GIC?
GIC是一种先进的微控制器总线架构(AMBA)和ARM架构兼容的系统片上(SoC)外设。它是一种高性能的、区域优化的中断控制器,具有芯片上的AMBA总线接口,根据配置,它符合AMBA高级可扩展接口(AXI)协议或AMBA AHB-Lite协议。
这里仅仅参考ARM官方文档将其中一部分从概念上加以介绍,过多细节比较枯燥就不做介绍了,个人认为仅需要从概念上去理解一下大致原理即可,没有必要去进行更深层的挖掘。除非你需要去修改这一层次的代码。
具体一点来看,GIC的总体框架如下:
- 处理单元Processing element (PE),也即是核
- 中断翻译服务组件Interrupt Translation Service components (ITS)
- 中断路由基础设施Interrupt Routing Infrastructure (IRI)
比如一个SPI的中断分发路由机制如下:
分发器 Distributor:分发服务器执行中断优先级排序,并将spi和SGIs分发到连接到系统中PEs,从而进入CPU处理。如果我们将这些都看成黑盒子,可以简化理解一下:
对于GICv2与GICv3的主要显著区别是GICv3可以支持更多核,GICv3可支持超过8核的处理器。
异常/中断来了咋办?
- 对于ARM处理器而言,异常/中断到来后,处理器对应进入不同的处理器模式(FIQ/IRQ/UND/ABT).
- 对于ARM64处理器而言,因为处理器已经没有处理器模式机制了,因此对应变成进入何种异常级别(exception level)。处理器复位时默认进入最高级别的exception level,例如如果处理器最高支持的EL是EL2,复位后系统将处于EL2。对于那些正常通过system call产生的异常,处理器会切换到哪一个exception level这个问题也很好回答,SVC、HVC和SMC将进入处理器设定的异常级别。
然而对于异常/中断的处理,说到底还是需要进入相应的异常/中断句柄(process handler)进行处理,那么怎么进入的呢?这里自然就引入了异常向量表了,这里来看看异常向量表在哪里实现的。这里个人认为理解一下大致概念也就可以了。
ARM
对于ARMv7-M而言,,位于./arch/arm/kernel/entry-v7m.s
ENTRY(vector_table)
.long 0 @ 0 - Reset stack pointer
.long __invalid_entry @ 1 - Reset
.long __invalid_entry @ 2 - NMI
.long __invalid_entry @ 3 - HardFault
.long __invalid_entry @ 4 - MemManage
.long __invalid_entry @ 5 - BusFault
.long __invalid_entry @ 6 - UsageFault
.long __invalid_entry @ 7 - Reserved
.long __invalid_entry @ 8 - Reserved
.long __invalid_entry @ 9 - Reserved
.long __invalid_entry @ 10 - Reserved
.long vector_swi @ 11 - SVCall
.long __invalid_entry @ 12 - Debug Monitor
.long __invalid_entry @ 13 - Reserved
.long __pendsv_entry @ 14 - PendSV
.long __invalid_entry @ 15 - SysTick
.rept CONFIG_CPU_V7M_NUM_IRQ
.long __irq_entry @ External Interrupts
.endr
.align 2
.globl exc_ret
exc_ret:
.space 4
对于其他的ARM架构而言,位于./arch/arm/kernel/entry-armv.S,贴上部分代码
*
* Interrupt dispatcher
*/
vector_stub irq, IRQ_MODE, 4
.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
.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
/*
* Data abort dispatcher
* Enter in ABT mode, spsr = USR CPSR, lr = USR PC
*/
vector_stub dabt, ABT_MODE, 8
.long __dabt_usr @ 0 (USR_26 / USR_32)
.long __dabt_invalid @ 1 (FIQ_26 / FIQ_32)
.long __dabt_invalid @ 2 (IRQ_26 / IRQ_32)
.long __dabt_svc @ 3 (SVC_26 / SVC_32)
.long __dabt_invalid @ 4
.long __dabt_invalid @ 5
.long __dabt_invalid @ 6
.long __dabt_invalid @ 7
.long __dabt_invalid @ 8
.long __dabt_invalid @ 9
.long __dabt_invalid @ a
.long __dabt_invalid @ b
.long __dabt_invalid @ c
.long __dabt_invalid @ d
.long __dabt_invalid @ e
.long __dabt_invalid @ f
/*
* Prefetch abort dispatcher
* Enter in ABT mode, spsr = USR CPSR, lr = USR PC
*/
vector_stub pabt, ABT_MODE, 4
.long __pabt_usr @ 0 (USR_26 / USR_32)
.long __pabt_invalid @ 1 (FIQ_26 / FIQ_32)
.long __pabt_invalid @ 2 (IRQ_26 / IRQ_32)
.long __pabt_svc @ 3 (SVC_26 / SVC_32)
.long __pabt_invalid @ 4
.long __pabt_invalid @ 5
.long __pabt_invalid @ 6
.long __pabt_invalid @ 7
.long __pabt_invalid @ 8
.long __pabt_invalid @ 9
.long __pabt_invalid @ a
.long __pabt_invalid @ b
.long __pabt_invalid @ c
.long __pabt_invalid @ d
.long __pabt_invalid @ e
.long __pabt_invalid @ f
/*
* Undef instr entry dispatcher
* Enter in UND mode, spsr = SVC/USR CPSR, lr = SVC/USR PC
*/
vector_stub und, UND_MODE
.long __und_usr @ 0 (USR_26 / USR_32)
.long __und_invalid @ 1 (FIQ_26 / FIQ_32)
.long __und_invalid @ 2 (IRQ_26 / IRQ_32)
.long __und_svc @ 3 (SVC_26 / SVC_32)
.long __und_invalid @ 4
.long __und_invalid @ 5
.long __und_invalid @ 6
.long __und_invalid @ 7
.long __und_invalid @ 8
.long __und_invalid @ 9
.long __und_invalid @ a
.long __und_invalid @ b
.long __und_invalid @ c
.long __und_invalid @ d
.long __und_invalid @ e
.long __und_invalid @ f
.align 5
.....................
/*=============================================================================
* FIQ "NMI" handler
*-----------------------------------------------------------------------------
*/
vector_stub fiq, FIQ_MODE, 4
.long __fiq_usr @ 0 (USR_26 / USR_32)
.long __fiq_svc @ 1 (FIQ_26 / FIQ_32)
.long __fiq_svc @ 2 (IRQ_26 / IRQ_32)
.long __fiq_svc @ 3 (SVC_26 / SVC_32)
.long __fiq_svc @ 4
.long __fiq_svc @ 5
.long __fiq_svc @ 6
.long __fiq_abt @ 7
.long __fiq_svc @ 8
.long __fiq_svc @ 9
.long __fiq_svc @ a
.long __fiq_svc @ b
.long __fiq_svc @ c
.long __fiq_svc @ d
.long __fiq_svc @ e
.long __fiq_svc @ f
.globl vector_fiq
.section .vectors, "ax", %progbits
.L__vectors_start:
W(b) vector_rst
W(b) vector_und
W(ldr) pc, .L__vectors_start + 0x1000
W(b) vector_pabt
W(b) vector_dabt
W(b) vector_addrexcptn
W(b) vector_irq
W(b) vector_fiq
.data
.align 2
.globl cr_alignment
cr_alignment:
.space 4
ARM64
位于./arch/arm64/kernel/entry.S,贴上部分代码
.pushsection ".entry.text", "ax"
.align 11
ENTRY(vectors)
kernel_ventry 1, sync_invalid // Synchronous EL1t
kernel_ventry 1, irq_invalid // IRQ EL1t
kernel_ventry 1, fiq_invalid // FIQ EL1t
kernel_ventry 1, error_invalid // Error EL1t
kernel_ventry 1, sync // Synchronous EL1h
kernel_ventry 1, irq // IRQ EL1h
kernel_ventry 1, fiq_invalid // FIQ EL1h
kernel_ventry 1, error // Error EL1h
kernel_ventry 0, sync // Synchronous 64-bit EL0
kernel_ventry 0, irq // IRQ 64-bit EL0
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
kernel_ventry 0, error // Error 64-bit EL0
#ifdef CONFIG_COMPAT
kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_compat, 32 // Error 32-bit EL0
#else
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
#endif
END(vectors)
总结一下
对于做嵌入式开发,尤其需要做底层驱动开发的小伙伴们,较深入的理解一下更为底层异常/中断运行的机制,对于具体驱动开发而言是非常有帮助的。本文参考内核代码,以及ARM官方规格书大致梳理了Linux中断子系统的基础概念,以及异常/中断如何进入到CPU,以及相应的入口在哪里实现定义的。后续为逐步深入分析中断底层如何建模抽象的,采用自底向上的分析策略进而分析用户空间的调用接口,敬请关注期待。
本文辛苦原创分享,水平所限,文中估计也有蛮多错误,希望看到的同学帮忙指正,如果觉得有价值也请帮忙点赞转发支持,不胜感激!