ARM 官方针对 ARMv7-A 处理器的描述再:
ARMv7-A_and_R_Architecture_Reference_Manual
ARMv7架构支持安全扩展,如果使能了安全扩展,ARMv7-A架构分为安全模式(Secure State)和非安全模式(Non-secure State)两个世界。
在非安全模式下,存在三种运行特权 PL0,PL1和 PL2(Privilege level)。(这里仅仅讨论非安全 State)
这里的 PL0~PL2 指的是特权等级,不同的特权等级,访问资源的权限不一样,操作系统运行再 PL1 的特权等级下,具有较高的访问权限,用户态运行再 PL0 的特权等级下(也叫非特权等级),只有最最基本的访问权限;
上面说的是处理器的特权等级,那么处理器真正的运行的模式有几种呢?如下所示:
可以看到,ARMv7-A 的处理器模式有 User、FIQ、IRQ、Supervisor、Monitor、Abort、Hyp、Undefined、System 模式:
User:用户模式,运行再 PL0 这个特权等级上,也就是没有特权等级,他是OS上运行应用程序时候的等级,他不可以访问系统资源(诸如 MMU 等),在这个模式下,无法主动切换模式,除非遇到中断或者异常(诸如 SWI 触发系统调用);
FIQ:快中断模式,发生 FIQ 快中断的时候处理器模式;
IRQ:中断模式,发生 IRQ 快中断的时候处理器模式;
Supervisor:管理员模式,复位后的默认模式,运行再 PL1 特权等级,可以通过 SWI(SVC) 系统调用呼叫产生 Supervisor Call 异常,进入 Supervisor 模式,操作系统常用的模式;
Monitor:监视模式,针对 Security 扩展,不详细讨论;
Abort:停止模式,当发生 Data Abort exception 或者 Prefetch Abort exception 异常时候进入这个模式;
Hyp:当支持虚拟化扩展的时候模式,不详细讨论;
Undefined:这是执行和指令相关的模式,当企图执行 UNDEFINED 指令的时候进入这个模式;
System:系统模式,也是 PL1 特权等级,和 Supervisor 的区别是,System 模式具有和 User 模式一样的寄存器,目前大多数系统未使用;
ARMv7-A 处理器有 16 个通用寄存器:R0~R15,其中:
R13:通常用做堆栈指针 SP;
R14:通常用作链接寄存器 LR;
R15:通常用作程序计数器 PC;
前面说了处理器有特权等级,每种特权等级访问系统资源的权限不一样,而处理器又有几种模式,每种模式对应的特权等级有一定区别;
每一种处理器模式对应的寄存器也有一定区别:
从上图可以看出:
1、R0~R7,PC是所有模式下共享的;
2、FIQ 模式下,R8~R12、SP、LR 都是有专门的寄存器,有的材料上,称之为“影子寄存器”,什么意思呢?这个模式下,有他专用的 R8~R12、SP、LR;
3、同样道理,Supervisor、Abort、Undefined、IRQ 等,都有他们自己模式下专用的 SP 和 LR,也就是说,从其他模式进来的时候,不需要针对这两个寄存器进行恢复现场;
4、FIQ 之所以称之为 FIQ,从软件上也看得出来,他专用的寄存器要多于 IRQ 的,所以也的确是要 Fast 一些;
从这里,我们也可以看出,exception 发生的时候,我们其实是有必要手动保存一些现场的;
ARMv7-A 还有一个特殊寄存器叫:程序状态寄存器 CPSR(Current Program Status Register),再进入异常之前,当前的 CPSR 被保存到 SPSR (Saved Program Status Register)中;
当然 CPRS 再用户层叫做 APSR,APSR 只是 CPSR 寄存器中被截取的一部分,因为在用户层,并不是所有的 CPSR 的位都可以访问;
CPSR 的组成如下所示:
这里的 M[4:0] 就是直接对应到了前面讲到的模式,还记得在前面那个处理器模式的列表中,每一个模式都对应了一个 Encoding 吗?这个 Encoding 就是这个 M 位的值;
ARMv7-A 支持 32bit ARM 指令集的同时,还支持 16bit 的 Thumb 指令集,它具有更好的代码密度,处理器可以在这两种指令集之间切换;
所有的Cortex-A系列处理器实现了Thumb-2技术,它扩展了Thumb指令集。混合使用32位和16位指令,以Thumb指令集的代码密度和接近ARM指令集的性能。自从所有的Cortex-A系列处理器支持这一扩展,针对它们的软件常被编译成Thumb指令集;
ARM 处理是加载/存储体系结构的典型的RISC处理器,对存储器的访问只能使用加载和存储指令实现。ARM 的加载/存储指令是可以实现字、半字、无符/有符字节操作;批量加载/存储指令可实现一条指令加载/存储多个寄存器的内容,大大提高效率;
基本格式为:
<opcode>{<cond>}{S} <Rd>,<Rn>{,<opcode2>}
<>内的项是必须的,{}内的项是可选的,如<opcode>是指令助记符,是必须的,而{<cond>}为指令执行条件,是可选的,如果不写则使用默认条件AL(无条件执行)
opcode:指令助记符,如 LDR,STR 等
cond:执行条件,如EQ,NE 等
S:是否影响CPSR 寄存器的值,书写时影响CPSR,否则不影响
Rd :目标寄存器
Rn:第一个操作数的寄存器
operand2:第二个操作数
LDR指令用于从内存中读取数据放入寄存器中;STR 指令用于将寄存器中的数据保存到内存。指令格式如下:
LDR{cond}{T} Rd,<地址>; 加载指定地址上的数据(字),放入Rd中
STR{cond}{T} Rd,<地址>; 存储数据(字)到指定地址的存储单元,要存储的数据在Rd中
LDR/STR 指令寻址是非常灵活的,由两部分组成,一部分为一个基址寄存器,可以为任一个通用寄存器,另一部分为一个地址偏移量。地址偏移量有以下3种格式:
(1) 立即数。立即数可以是一个无符号数值,这个数据可以加到基址寄存器,也可以从基址寄存器中减去这个数值。指令举例如下:
讯享网
(2)寄存器。寄存器中的数值可以加到基址寄存器,也可以从基址寄存器中减去这个数值。指令举例值。指令举例如下:
讯享网
(3)寄存器及移位常数。寄存器移位后的值可以加到基址寄存器,也可以从基址寄存器中减去这个数值。指令举例如下:
一组代码示例:
讯享网
批量加载/存储指令可以实现在一组寄存器和一块连续的内存单元之间传输数据。LDM 为加载多个寄存器,STM 为存储多个寄存器。允许一条指令传送16 个寄存器的任何子集或所有寄存器。指令格式如下:
LDM{cond}<模式> Rn{!},reglist{^}
STM{cond}<模式> Rn{!},reglist{^}
LDM /STM 的主要用途是现场保护、数据复制、参数传送等。其模式有8种,如下所列:(前面4 种用于数据块的传输,后面4 种是堆栈操作)。
(1) IA:每次传送后地址加4
(2) IB:每次传送前地址加4
(3) DA:每次传送后地址减4
(4) DB:每次传送前地址减4
(5) FD:满递减堆栈
(6) ED:空递增堆栈
(7) FA:满递增堆栈
(8) EA:空递增堆栈
其中,寄存器Rn 为基址寄存器,装有传送数据的初始地址,Rn 不允许为R15;后缀“!”表示最后的地址写回到Rn中;寄存器列表reglist 可包含多于一个寄存器或寄存器范围,使用“,”分开,如{R1,R2,R6-R9},寄存器排列由小到大排列;“^”后缀不允许在用户模式呈系统模式下使用,若在LDM 指令用寄存器列表中包含有PC 时使用,那么除了正常的多寄存器传送外,将SPSR 拷贝到CPSR 中,这可用于异常处理返回;使用“^”后缀进行数据传送且寄存器列表不包含PC时,加载/存储的是用户模式的寄存器,而不是当前模式的寄存器。
地址对准――这些指令忽略地址的位[1:0]。
批量加载/存储指令举例如下:
LDMIA R0!,{R3-R9} ;加载R0 指向的地址上的多字数据,保存到R3~R9中,R0 值更新
STMIA R1!,{R3-R9} ;将R3~R9 的数据存储到R1 指向的地址上,R1值更新
STMFD SP!,{R0-R7,LR} ;现场保存,将R0~R7、LR入栈
LDMFD SP!,{R0-R7,PC}^;恢复现场,异常处理返回
值得注意的是一些诸如原子操作的指令:STREX/LDREX;
特殊寄存器 CPSR 通过 MRS 和 MSR 指令进行读写操作:
MRS:读状态寄存器指令。在ARM 处理器中,只有 MRS 指令可以状态寄存器CPSR或SPSR读出到通用寄存器中。
MRS{cond} Rd ,psr
Rd 目标寄存器。Rd 不允许为R15
举例:
讯享网
MRS 指令读取CPSR,可用来判断ALU 的状态标志,或IRQ、FIQ中断是否允许等;在异常处理程序中,读SPSR 可知道进行异常前的处理器状态等。MRS 与MSR 配合使用,实现CPSR 或SPSR 寄存器的读—修改---写操作,可用来进行处理器模式切换(),允许/禁止IRQ/FIQ中断等设置。另外,进程切换或允许异常中断嵌套时,也需要使用MRS 指令读取SPSR 状态值。保存起来
举例:
MSR:写状态寄存器指令。在ARM 处理器中。只有MSR 指令可以直接设置状态寄存器CPSR或SPSR。指令格式如下:
MSR{cond} psr_fields,#immed_8r
MSR{cond} psr_fields,Rm
psr CPSR 或 SPSR,
fields 指定传送的区域。Fields 可以是以下的一种或多种(字母必须为小写):
c 控制域屏蔽字节(psr[7…0])
x 扩展域屏蔽字节(psr[15…8])
s 状态域屏蔽字节(psr[23。…16])
f 标志域屏蔽字节(psr[31…24])
immed_8r 要传送到状态寄存器指定域的立即数,8 位。
Rm 要传送到状态寄存器指定域的数据的源寄存器。
MSR 指令举例如下:
讯享网
注意:只有在特权模式下才能修改状态寄存器!
程序中不能通过MSR 指令直接修改CPSR 中的 T 控制位来实现ARM 状态/Thumb状态的切换,必须使用 BX 指令完成处理器状态的切换(因为BX 指令属转移指令,它会打断流水线状态,实现处理器状态切换)。MRS 与MSR 配合使用,实现CPSR或SPSR 寄存器的读-修改-写操作,可用来进行处理器模式切换、允许/禁止IRQ/FIQ 中断等设置。
更多指令集 相关的详细内容,参考 ARM 官方文档,或者:
https://blog.csdn.net/u0/article/details/
更多关于协处理器 Cp15 以及 Cache 和 MMU 相关的,在下一章介绍;
ARMv7-A 处理器除了标准的 R0~R15,CPSR,SPSR 以外,由于引入了 MMU、TLB、Cache 等内容,ARMv7-A 使用协处理器来对这些扩展来进行管理,ARMv7-A 支持 16 个协处理器,编号从 CP0~CP15,其中的 CP15 协处理器称之为系统控制协处理器,CP15 协处理器下的寄存器包含了 MMU、TLB、Cache等关键组件,其余的 CP0~CP14 有的控制Debug功能,有的控制 SIMD,有的控制浮点,咱们暂时只关注关键的 CP15;
CP15 协处理器由16个子寄存器组成,分别为 c0~c15,所以 CP15 的层次关系为:
这里的 c0~c15 不是寄存器的含义,而是主(Primary Register)寄存器的意思,也就是每个 c0~c15 中,包含很多寄存器组:
比较常用的 c0~c15 的寄存器组织如下
所以,针对 CP15 协处理器相关的层次结构总结下来为(这里以c0为例,c1~c15同样有很多寄存器,每个寄存器都是 32bits 的):
与 CPSR 类似,协处理器的访问是通过指定的汇编指令进行访问;常用的有 MCR/MRC 两条:
MRC: 将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中。
MCR: 将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中。
使用这两条指令,外加一些标准的指令,就可以实现读改写;
MCR 指令的格式如下:
MCR<c> <coproc>, <opc1>, <Rt>, <CRn>, <CRm>{, <opc2>}
其中:
coproc:访问协处理器的名字,取值范围从 p0~p15;
opc1:协处理器要执行的操作码,取值范围从 0~7;
Rt:ARM 的寄存器(比如 R0),要写入到指定协处理器寄存器的数据就保存在此寄存器中;
CRn:指定协处理器的目标寄存器;
CRm:协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将 CRm 设置为 C0,否则结果不可预测;
opc2:可选的协处理器特定操作码,取值范围从 0~7,当不需要的时候要设置为 0;
MRC 指令的格式如下:
MRC<c> <coproc>, <opc1>, <Rt>, <CRn>, <CRm>{, <opc2>}
其中:
coproc:访问协处理器的名字,取值范围从 p0~p15;
opc1:协处理器要执行的操作码,取值范围从 0~7;
Rt:ARM 的寄存器(比如 R0),将指定协处理器寄存器的数据读在此 ARM Core 寄存器中;
CRn:指定协处理器的目标寄存器;
CRm:协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将 CRm 设置为 C0,否则结果不可预测;
opc2:可选的协处理器特定操作码,取值范围从 0~7,当不需要的时候要设置为 0;
访问协处理器寄存器的指令已经写得很清楚,接下来就是如何来填这指令中的变量,这涉及到具体的协处理器的寄存器组织,我们使用最常用的 CP15 为例进行讲述;
不管是 MCR/MRC 指令,如果针对到具体的 Cp15 协处理器,coproc 字段填 p15,Rt 字段可以随便指定,暂时不管,那么就剩下 4 个变量:
{opc1、CRn、CRm、opc2};
Cp15 协处理器的所有寄存器访问,都要依赖这几个值的组合达到访问的目的,前面说过整个 Cp15 的寄存器,分为 c0~c15,一个 16 个 Primary Regiser,再在每个 Primary Regiser 下面,又细分了很多具体的每个长度为 32bits 的寄存器,他们的整体组织结构为:
可以看到,Cp15 协处理器的 CRn 编号从 c0~c15,每个 Primary Regiser 都标记得有访问权限;后面的 opc1、CRm、opc2 的取值访问也都标记得清清楚楚;
那么下面将 c0~c16 每个展开看一下便可以清清楚楚看到他们怎么组织起来的,这样就可以知道如何使用汇编进行编码配置;
c0 的寄存器组成如下:
访问每个寄存器的 {opc1、CRn、CRm、opc2} 写的清清楚楚;
主要是和 ID 相关的内容;
c1 的寄存器组成如下:
主要是和系统控制相关的配置;
c2&c3 的寄存器组成如下:
主要是内存保护和控制相关的(MMU);
Cp15 协处理器的 c4 寄存器为 Not used;
c5&c6 的寄存器组成如下:
主要是和 memory fault 相关的;
c7 的寄存器组成如下:
主要是和 Cache 相关的部分;
c8 的寄存器组成如下:
主要是和 TLB 相关的;
c9 的寄存器是为 cache 和 TCM 预留的;
c10 的寄存器是为内存重映射和 TLB 控制:
c11 的寄存器是为 TCM DMA 预留:
c12 的寄存器是为安全扩展的:
c13 的寄存器是为进程,上下文和线程 ID 的:
c14 的寄存器是为通用 Timer 预留的:
ARM体系架构—ARMv7-A指令集:协处理器指令_liyuewuwunaile的博客-CSDN博客
ARMv7 CP15协处理器详解_Deep_l_zh的博客-CSDN博客_cp15协处理器
本文参考:
《ARMv7-A_and_R_Architecture_Reference_Manual》 中的 {A3.5 Memory types and attributes and the memory order model}
ARMv7-A 处理器中,将 Memory定义为几种类型(Memory Type):
1、Strongly-ordered;
2、Normal;
3、Device;
它的定义如下所示:
注意:这里的 Memory 指的不是内存,可以翻译成储存器,是地址空间的概念;
普通的内存(RAM),只读的内存(ROM),这些都属于 Normal Type 的范畴;
外设和I/O,这些属于 Device 和 Strongly-ordered 范畴;
对于地址空间来说,每种地址空间,ARMv7-A 使用 attribute 来描述这个存储器地址的属性,总的属性分为两种:
1、shareability;共享属性
2、cacheability;缓存属性
它们描述了存储器是否可以共享,或者是否可以过cache;
Shareability 共享属性用于描述存储器地址空间是否可以共享;只对 Normal Type 类型和 Device 类型有效;
Normal Type 是比较常见的存储器模型,它可以支持读/写或只读,它可以被配置成为 Shareable 或者 Non-Shareable;
在访问 Normal 存储器的时候(比如 DDR),一定要小心内存一致性的问题,因为系统中,常常会开启 cache,并在多核系统中,存在多 CPU 核心访问内存,内存屏障能够起到访问的保序作用,原子操作指令,可以起到访问互斥(LDREX/STREX);
对于 Normal Type 的内存属性描述如下:
当被配置成为 Non-Shareable 的时候,意味着在多核系统中,它只能被一个核心访问;
当配置为 Inner-Shareable 的时候,意味着只能够被单个 CPU cluster 集群访问;
当配置为 Outer-Shareable 的时候,意味着只能够被多个 CPU clusters 集群访问;
Device 类型存储器地址空间,可以配置为 Shareable(比如多核 CPU 中,UART 外设地址空间是对所有 CPU 共享的);当然,也可以配置为 Non-Shareable;
但是具体实际的设计实现中,不管是配置为 Non-Shareable,还是 Inner-Shareable,还是 Outer-Shareable,都一概视为 Outer-Shareable(毕竟是外设);
Normal 类型的存储器(比如 DDR)除了有 Shareable 的属性以外,还可以携带 cacheable 的属性:
Write-Through Cacheable:写透型 Cache 属性;
Write-Back Cacheable:回写型 Cache 属性;
Non-cacheable:不带 Cache 属性;
Non-Cacheable 很好理解,就是不带 Cache,直接与存储器交互,这样不会带来内存一致性问题,但是效率不高;
其余两种都是带 Cache 的访问,不过 Cache 的策略略有区别;写透也可以在一定程度上保证内存一致性问题,但要发起内存访问时序,降低性能;回写只是将数据写到了 Cache 中,并通过 Dirty 标记等方式来记录数据的有效性,从而避免直接的内存访问,可以提高性能;
Device 和 Strongly-ordered 类型的存储器(可以理解为外设)区域,都是 Non-cacheable 的,即,不缓存的;这个很好理解,写过驱动的朋友应该都能够知道,对外设的访问和对普通内存的访问是完全不一样的两个概念;
所以,最后总结起来就是:
ARMv7-A 中,Device 类型的存储器和 Strongly-ordered 类型存储器,都是不可 Cacheable 的,并且他们的访问是不可被优化的,他们的唯一区别是在 Shareability 属性:
1、对 Strongly-ordered 存储器的写入,只有在写访问到外设或者存储器组件的时候,才算完成;
2、对 Device 存储器的写入,允许在写访问到达外设之前就完成;
上面这个说法是官方的意思,我理解一下,应该是 Strongly-ordered 不惜牺牲性能,去做保序的要求,一定要实际访问到外设,而 Device 类型的访问指令,可能还在路上(流水线中);
而且,Strongly-ordered 和 Device 类型要求,对他们的访问必须是对齐访问;
存储器模型除了有各种属性以外,还需要支持访问权限(这个是必须的,否则访问非法空间,那还得了);
ARMv7-A 定义了扩展的存储器区域访问属性,也就是这个权限访问;访问权限分为两部分:
1、基于特权等级的限制数据的访问;
2、基于特权等级的指令访问;
PL0 的时候,也就是 User 模式,相当于非特权等级下,禁止访问结构体系下的某些特性,它不能更改许多配置设置。 在PL0上执行的软件只允许非特权内存访问;
当在 PL1 上执行的软件(通常操作系统都执行在 PL1 特权模式下)可以访问体系结构的所有特性,并且可以更改这些特性的配置设置;
值得注意的是,fa权限的配置以及更改,只能够在 PL1 模式下进行;
针对数据的访问来说,在 PL1 模式下,可以定义某存储器区域的访问权限为:
1、不可访问;
2、只有 PL1 特权才能访问;
3、特权和非特权模式都可以访问;
如果处理器尝试访问权限不允许的数据访问,则会生成数据中止异常(Data Abort)。 例如,如果处理器位于 PL0,并且试图访问被标记为仅特权模式可访问的内存区域,则会生成数据中止异常 Data Abort;
针对指令的执行来说,PL1 模式下,以定义某存储器区域的执行权限为:
1、不允许执行;
2、在处理器实现了 Large Physical Address Extension 情况下,PL1 不允许执行;
3、只能在 PL1 执行;
4、特权和非特权模式都可以执行;
访问权限的配置,在 CP15 的 MMU 相关的寄存器,他们的层次结构为:
一些 Cache 基本的内容,比如 “为什么需要 Cache”,“Cache 的组织形式”,“Cache 的映射形式”等,我已经在之前的文章《Cache 原理浅析》中叙述得比较清楚了,这里不再赘述,有兴趣的同学可以跳转观看一下,这里主要是补充一些 ARMv7-A 上的一些细节;
《Cache 原理浅析》可以知道,Cache Line 是 Cache 的最小单位,为了寻找特定 Cache,在 Cache 结构中,ARM 将地址分为了几段,比如 32bits 的地址总线,ARM 将其分为了 3 段:
最高的一段叫做 Tag,中间的叫 Index,最后叫 Offset;
在一个多路组相关的 Cache 结构中,它的结构如下:
针对这个图,可以做如下理解:
1、这里的 Line,指的是一个 Cache Line,最小的缓存单位,可能很多字节(比如,64 Bytes);
2、可以看到,一个 Way,包含了 N 条 Cache Line(这里包含了 4 条),图中有 4 个 Way,也就是 4-Way associate 的含义;
3、每一个 Way 的同样位置的 4 条 Cache Line 组成一个 Set,具体分为了多少个 Set,这个要看 Cache 总共有多大,这里画了 4 个 Set;
4、因为 Cache 进行数据缓存,并不是按照 一个地址+一个数据(32bits 地址+数据),如果按照这样的结构来缓存,因为这样效率太低;实际上,它缓存的方式是一组一组来缓存,每一组用一个 Tag 进行表征;所以,每个 Cache Line 就有一个 Tag;
5、Index 用于表征一个 Way 中
5、每个 Way 中,都对 Cache Line 进行编号,有 Index=1,2,3..n(Index=1表示 Cache Line 的编号即 line1,index=2表示 line2), 我们将在不同 way 中,Index 相同的叫成 set。
上面的结构如果用 C 语言来表示的话:
Way = Cache[tag];
Cache Line = Way[Index];
Data = Cache Line[Offset]
举个真实的例子:
假如我们有一个 Cache Line = 32Bytes,4路 Way 的 组织方式,Cache=32KBytes。
Per Way = (32KBytes / 32KBytes) / 4 Way= 256 Cache Line/Way;
前面说过,一个 Way 中的 Cache Line 是 Index 来索引的,256 个 Cache Line/Way 的话,就要 5 个 bits 来表示;
一个 Cache Line 是 32Bytes,那么就是 8 个 Word,使用 3 个 bits 表示即可;
剩余的高位,作为 Tag 的形式存在:
既然以地址来进行查找 Cache 的,那么我们到底是用虚拟地址还是物理地址呢?三种方式:
1、早期的 ARM 处理器,如 ARM720T 或 ARM926EJ-S 处理器使用虚拟地址提供 Index 和 Tag。 这有一个优点,即 CPU 不需要虚拟到物理地址转换就可以进行缓存查找。 缺点是,每当进行进程切换(虚拟地址映射表发生改变),Cache 中的虚拟地址就不能再用了,导致性能下降,现在这种方式已经淘汰。
2、使用物理地址来查找 Cache(我们叫它 PIPT),那么这么做很明显解决了第一种的缺点(因为是以物理地址进行 Cache 的,不管映射表怎么变,物理地址不会变)。但是由引入了一个缺点:每次进行查表的时候,都需要到MMU去进行地址转换,这样增加了查找cache需要的时间,效率明显没有采用虚拟地址的高。注:这种方法,依赖MMU,即MMU关闭,Cache 就必须关闭;
3、是第 1 种和第 2 种的折中处理,将这个查找过程分为两步,Tag 用物理地址表示,Index 用虚拟地址的,我们叫它 VIPT(Virtually Indexed, Physically Tagged)。那么怎么实现呢?首先,由于 Cache 控制器和 MMU 是两个独立模块,因此通过 MMU 去查找 Tag 和通过 Index 去 Cache 查找 Way 是相互独立的即可以同时运行。即当用 Index 去 Cache 查找 Set(上面有解释,即 Index 相同,但处于不同 Way 的一组集合)的同时也在用虚拟地址去 MMU 找物理地址的 Tag,当从cache找到一组set(line[way])的时候(因为只提供了index,因此cache control不知道到底是哪个way,所以返回每个包含index的way),此时MMU中也查到了物理tag,然后再用该物理tag去匹配返回的set,最后获取到对应的 cache line。常用CPU情况如下。
可能到这里有人会问了,混合使用物理地址和虚拟地址不会有问题吗?毕竟虚拟地址在进程发生变动的时候是会不断变化的。不不不,理论上是不会有问题的,为什么呢?我们知道我们的虚拟映射表了,我们的映射表一般是以4K为一个page,即4k对齐,不管虚拟地址怎么发生变化,一个page内的偏移是不变的。要寻址一个4k大小我们需要[11:0]共12个bit来提供支持,即在MMU当中,虚拟地址的低12位和物理地址的低12位是相同的。假如我们用的是一个16kb大小,含有4个way,每个line 32bytes的cache,那么通过计算[4:0]用于cache的offset定位,[11:5]则用于cache的index定位。如上所说,虚拟地址和物理地址的[11:0]是相同的,因此index用虚拟地址就不会有影响。但是话又说回来,如果我们的cache大小超过了16k,加入为32kb呢?那么我们以32KB,含有4个way,每个line 32bytes的cache来说,[4:0]用于offset定位,[12:0]用于index定位,那么问题来了,由于虚拟地址和物理地址仅仅是[11:0]相同,那么第13位在发生切换后,就可能会出现0/1两个值,意味着一个物理地址可能会同时占用2个cache line,即两个副本, 这样就会容易引发cache一致性的问题。针对于这种cache alias问题,目前的方案是由操作系统来保证,对于同一物理地址在不同进程空间的虚拟地址,他们的虚拟地址的差一定是cache way大小的整数倍,也就是说他们的第13位一定是相同的。同时已经有些cpu厂商在开发监视模块,试图在硬件层面解决类似的同步问题。同理对于64kb的cache也采用同样的方法。
在 Cache 操作策略中可以做出许多不同的选择。
第一种,CPU读数据时。只有当读取的时候,发现cache miss,才从cache中申请一个line去缓存该数据。写的时候,不申请,直接写入下一级。
第二种,写和读时。只要访问时,不管读或者写,发生了cache miss都去申请一个cache line。
当 Cache miss 的时候,Cache 替换策略:
第一种,Round-robin 或者循环替换策略;
第二种,Random 替换,Cache 存满并且出现 Cache miss,如果来了一条新的,则随机找一个 Cache Line 被替换;
第三种,LRU(Least Recently Used) 算法替换,方法如名字,当 Cache 存满了后,如果来了一条新的,则选择最近最少使用的被替换;
第一种,Write-Back 模式:写数据时,只向 Cache写入数据,并标记 Cache 为 Dirty,然后在合适的时机将数据更新到主存;
第二种,Write-Through 模式:写数据时,Cache 和主存都要写一份;
和 Cache 相关的寄存器控制,首先就是 CP15 的 SCTLR 系统控制寄存器:
bit[2] 是控制 Cache enable 的
bit[12] 是控制指令 Cache enable 的
获取 Cache 的 Type,刷 Cache 等操作寄存器,等在分析 Linux Kernel 的时候,在对着代码解读;
这里需要注意的是,访问 CP15 协处理器的寄存器,通过 MRC/MCR 特殊指令;
参考:
linux系统之arm架构的CPU与Cache_eleven_xiy的博客-CSDN博客
关于L1 L2 Cache_忧郁天蝎-CSDN博客
之前写过 MMU 的一些入门和基础的分析《初探 MMU》和《ARMv7-A 的 MMU 浅析》,有基于概念掌握和基本入门的一些理解,这里打算在针对 ARMv7-A 的处理器再次稍微深入一点研究一下他的 MMU 和 TLB;
这一版同样基于 ARM 官方文档:
ARMv7-A_and_R_Architecture_Reference_Manual
DEN0013D_cortex_a_series_PG
的一个是完整版的 ARMv7-A 的处理器架构文档,第二个是 Cortex-A 系列的 Programmer Guide;
由于有前面的两篇文章垫底(《初探 MMU》和《ARMv7-A 的 MMU 浅析》),这里就不再介绍一些基础的内容了,直接单刀直入;
前面知道,MMU 用作虚拟地址和物理地址的相互转换,是为了能够给 OS 提供统一视角的虚拟地址空间;
TLB 的作用是作为 MMU 的 Cache,以提高 MMU 的性能,他们之间的关系如下:
1、ARM 处理器发出地址访问(虚拟地址),首先过 MMU 地址翻译单元的 TLB,如果 TLB 命中,那么直接返回真实的物理地址;
2、如果 TLB Miss,那么就要靠 Table Walk 单元去主存中查找表,以获取物理地址,然后通过 Cache,去访问;
3、Cache 如果命中,那么直接返回实际物理地址的数据,否则,也就是最糟糕的情况,会去访问主存的数据;
上面的过程呢,软件要做的,只有配置并放好这个 Transliation Tables,其他的过程,全部是硬件行为;下面马上仔细的过这部分的细节;
使能 MMU 的参考代码(因为是 CP15 的系统控制寄存器,所以使用 MRC/MCR 指令):
讯享网
这里要注意的一点是,可能要用到内存屏障指令,因为这里就开启了 MMU,即将进入虚拟内存的世界,要确保在这之前,流水线干净,所以执行已经完毕;
TLB 的全称是:Translation Lookaside Buffer;从第一节的那个图可以看出来,MMU 做 Table Walk 的这个 Transliation Tables 是放到主存中,主存访问速度很慢(加 Cache 的根本原因),所以,这里每次都去再主存中做 Table Walk,显然效率非常低,所以,这里就为这个 Table Walk 定制了一个属于他的 “Cache”,称之为 TLB;
但是与 真是的 Cache 不一样(详见《ARMv7-A 处理器窥探(4) —— Cache》),这个 TLB 是专门缓存 Transliation Tables 的,典型的情况,他的组成如下:
由 VA、ASID、PA、Attributes 组成,即:
VA:虚拟地址;
PA:物理地址;
ASID:Address Space ID;
Attributes:属性;
TLB 既然扮演的 Transliation Tables Cache 的角色,那么也会有一致性问题,最典型的就是再 OS 中,上下文切换的时候,上一个进程的虚拟地址对应的物理地址表,肯定是和另一个不一样,导致 TLB 一致性问题;此刻,OS 必须处理这种情况,使得上一个进程的 TLB 对下一个失效,也可以直接通过 CP15 控制寄存器,来 flush 掉 TLB(代价太大);
这里,抛开大物理地址扩展和 section 和 supersection 的分析,暂时就看最最常用的两段查找;两段页表查找,我们称第一级页表为 L1,第二级为 L2;
前面知道,软件需要负责构建这个虚拟地址到物理地址的转换表:Transliation Tables,当软件构件完毕这个表后,只需要告诉硬件,这个 Transliation Tables 放到了那个首地址即可,这个配置通过写 ARM 的 TTBR 寄存器实现(Translation Table Base Address );这里其实有两个 TTRB 寄存器,分别叫 TTBR0 和 TTBR1,为啥两个,后面解释;
和这个 TTBR0、TTBR1 勾肩搭背的,还有一个 TTBCR 寄存器,他们直接什么关系呢,看寄存器说明:
TTBCR:
PD0 和 PD1 是和 Security Extensions 相关的,不管他;
EAE 是和 Large Physical Address Extension 相关的,不管他;
主要关注这里的 N[2:0],指示TTBR0页表基址寄存器基址位宽,同时指示使用 TTBR0 还是 TTBR1 作为页表基址寄存器,以及 TTBR0 页表尺寸:
如果 N = 0 的话,则在做 Table Walk 的时候使用 TTBR0 指定的基地址作为 Transliation Tables 入口的地址;
如果 N > 0 的话:指示TTBR0页表基址寄存器基址位宽,同时指示使用 TTBR0 还是 TTBR1 作为页表基址寄存器,以及 TTBR0 页表尺寸;
- N==0,使用 TTBR0。
- N>0,如果虚拟地址[31:32-N]为0,则使用 TTBR0;其他情况使用TTRB1。这种情况下,N 指示了TTBR1的页表地址,也指示了 TTBR0 的页表大小。
- TTRB0的页表大小由TTBCR.N控制,TTRB1的页表大小为16KB。
我换句话来说,当 N>0 的时候,比如 N=1,那么按照这种说法,VA [31:31] 也就是 VA 的 bit[31] 为 0 的时候,使用 TTBR0 否则使用 TTRB1,按照地址空间来划分,即,32bits 地址,当最高位为 0,即虚拟地址为 0x0000_0000 ~ 0x7FFF_FFFF 这个区间的时候,使用 TTBR0 作为 Transliation Tables 入口的地址,从 0x8000_000 ~ 0xFFFF_FFFF 的虚拟地址空间,使用 TTBR1;
ARM 官方举了个例子,当 TTBCR.N=3'b111 的时候,VA [31:25] 全部为 0 的时候,使用 TTBR0,按照地址空间来划分就是,虚拟地址为:0x0000_0000 ~ 0x01FF_FFFF 这段区间使用 TTBR0 作为 Transliation Tables 入口的地址;
0x0200_0000 ~ 0xFFFF_FFFF 的虚拟地址空间,使用 TTBR1;
OK,现在可以理解为,配置 TTBCR.N 这个值,可以实现将虚拟地址切割成为两部分,一部分使用 TTBR0 指定的 Transliation Tables 进行 Table Walk,另一部分使用 TTBR1 指定的 Transliation Tables 进行 Table Walk,这个有什么好处呢?比如,内核的页表,是不会改变的,而进程上下文的页表是会改变的,有了这个的话,就可以考虑用一个 TTBR 来专门为内核服务,另一个 TTBR 为进程服务,这样避免进程和内核使用同一个页表,每次都要进行内核页表的拷贝;
由于 TTBCR 是 CP15 的寄存器,访问 TTBCR 的指令为:
上面说了 TTBCR,下面来看 TTRB0、TTRB1 寄存器描述:
TTBR0
在带有多核处理器扩展的情况下 TTBR0 由一个可变的长度构成 Transliation Tables Base Address,这个 x 就是上面的 (14 - (TTBCR.N));
Bits[31:x]:x=(14-(TTBCR.N))。一级页表地址;
Bits[x-1:7]:Reserved;
NOS:Not Outer Shareable bit,指示了做 Table walk 的那个内存的属性,是 Outer Shareable 还是 Inner Shareable.
- 0 Outer Shareable.
- 1 Inner Shareable.
TTBR0.S == 0 时,该bit无效;
S:Shareable bit. 指示内存共享属性与页表转换的关系;
- 0 Non-shareable.
- 1 Shareable.
RNG:Region bits,指示 Outer Cache 属性与页表转换的关系;
- 0b00 Normal memory, Outer Non-cacheable.
- 0b01 Normal memory, Outer Write-Back Write-Allocate Cacheable.
- 0b10 Normal memory, Outer Write-Through Cacheable.
- 0b11 Normal memory, Outer Write-Back no Write-Allocate Cacheable.
IRGN[6,0]:Inner region bits,指示 Inner Cache 属性与页表转换的关系;
- 0b00 Normal memory, Inner Non-cacheable.
- 0b01 Normal memory, Inner Write-Back Write-Allocate Cacheable.
- 0b10 Normal memory, Inner Write-Through Cacheable.
- 0b11 Normal memory, Inner Write-Back no Write-Allocate Cacheable.
访问 TTBR0 的指令为:
讯享网
TTBR1
它的位域和 TTBR0 几乎一样,唯一不一样的地方在于,配置的地址区间在于 bit[31:14],这意味着,配置进 TTBR1 的 Transliation Tables Base Address 的物理地址,必须 16KB 对齐;
访问 TTBR1 的指令为:
现在我们知道了合理的配置 TTBCR/TTBR0/TTBR1 可以分配并指定 Transliation Tables,而这个 Transliation Tables 位于内存中,用作 MMU 来做 Table Walk;那么接下来我们需要知道页表的结构,这样我们才能够在内存中创建页表,并将页表配置给 TTBR 寄存器,完成 MMU 的配置;
不考虑大地址扩展和 SuperSection 以及 Section 的情况下,针对 Transliation Tables,ARMv7-A 的手册给出了如下的图解
图中,我们暂时只考虑 Page Table 的情况,即红色部分(其余的可以照着推);
蓝色的部分,可以理解为之前寄存器里面配置的那个 N 值;这里为了说明情况,我们暂时将 N 定为 0;
我们先暂时不管使用 TTBR0 还是 TTBR1,其实过程是一样的;此刻当 N = 0 的时候,一级页表以虚拟地址(后面简称 VA,即 Virtual Address)VA[31:20] 作为 L1 Index,一共 12bits,最大能够表征 4K 的 Index:
每个入口是 4 Bytes 也就是一个 Word,32bits 的入口,L1 Index 从 0~4095,一共 4K,在内存上,每个入口 4Bytes,那么 L1 页表占用内存 4K x 4 Bytes = 16KB;
每一个入口是什么样子的呢,我们放大来看:
可以看到,这个入口,根据不同的配置,内容有所区别,一共有 4 种类型,这 4 种类型,通过 32bits 的尾部 2 bits 来区分,即,绿色部分(Section 和 SuperSection 的区分,还靠 bit[18]);
这里我们暂时不关心 Section 和 SuperSection,关注于红色部分和那个 Fault;
Level 2 Descriptor Base Address:指向的是 L2 页表的物理地址的基地址;可以看到他是 bit[31:10],是 1KB 边界对齐的;
这个 Domain 指的是 ARM 支持将内存标记为最多 16 个 domain,并以 Domain ID 作为区分,每个 Domain 可以支持配置成为不同的访问权限(通过配置 CP15 的 C3 的 Domain Access Control Register (DACR) 寄存器):
配置指令为:
讯享网
针对这个 DACR 寄存器,ARM 官方建议配置成为 Client;
The use of domains is deprecated in the ARMv7 architecture, and will eventually be removed,
but in order for access permissions to be enforced, it is still necessary to assign a domain number
to a section and to ensure that the permission bits for that domain are set to client. Typically, you
would set all domain ID fields to 0 and set all fields in the DACR to 'Client’.
介绍完 L1 Address Translation 后,下面是二级页表!与 L1 页表不一样,二级页表不和 N 值挂钩,它直接采用 VA[19:12] 作为 L2 Index 索引,一共 8 bits,最大能够表征 256 的 L2 Index;
加入 L2 页表后结合 L1,通过一个给定的 VA 进行索引的第二步为(图中表示的 N 值为 0):
这样,一个 VA 通过高地址部分[31:20] 索引到了 L1,再从 L1 指向的 L2 加上 VA[19:12] 作为 L2 Index,索引到 L2 表的固定位置;
L2 也是每条由 4 Bytes 构成,即一个 32bits 的数,那么一个 L2 表大小为 256 x 4 Bytes = 1024 Bytes = 1KB;一共有 4096 个这样的 L2,那么 L2 表总的大小为:4096 x 1KB = 4MB;
我们放大每一条 L2 的入口:
我们只关心红色部分!可以看到,这个 Small Page Base Address 有 bit[31:12] 也就是 4KB 边界对齐!接下来我们看剩余几个位的含义:
AP/APX:Access Permission 即访问权限,每个内存区域 都有自己的权限,不符合访问权限的 内存访问都会引发 异常。如果是 数据访问 则引发 数据异常。如果是 指令访问,且该指令在执行前没有被 flush,将引发 预取指异常。引发的 异常原因将会被设置在 CP15 的 the fault address and fault status registers;
内存区域类型 可以通过 TEX字段、C字段 和 B字段 来进行设置:
XN:指的是 Execute Never,不允许执行,如果往这里取地址执行,那么会导致异常发生;通常,Device memory 类型的区域会配置成为 XN;
S:指的是是否具有 Shareable 属性;
nG:non-Global,这个标记告诉 MMU,这个页表是否是一个全局的,什么意思呢?看下面:
当 nG 为 0 的时候,说明此区域是全局可见的,换句话来说,就是任何时候都生效;
当 nG 为 1 的时候,说明此区域不是全局的,要联合这个 ASID 来确认;
每一个 nG=1 的区域,都会和 ASID 来关联,ASID (Address Space Identifier),这代表,TLB 可以存在多个不同进程的页表缓存,后面说 ASID 的时候会仔细说;
自此,L1/L2 分析完毕,那么整个 Table Walk 的流程为:
VA 的 4K 页内偏移,直接对应到 PA 的 4KB 页内偏移;
通常情况下,在使用 Cortex-A 系列处理器的时候,典型场景是跑多任务 OS;每一个任务(或者成为应用),都有它独立的虚拟地址空间,以及他的独立的 Translation Table;但是对于 OS 来说,Kernel 的 Translation Tables 其实是固定的,只是进程之间的 Translation Tables 不一样而已;
当一个进程启动的时候,OS 负责为他 code 和 data 段建立映射表(Translation Tables);当进程调用诸如 malloc 之类分配内存的行为,OS 负责修改 Translation Tables(Linux 中,实际访问分配的内存的时候,才去修改页表),进程生命周期消亡,OS 负责回收它的资源和页表,并可以为下一个新的进程提供资源;每一个进程都有自己的独立的页表,这便可以保证进程之间不会相互干扰;
在操作系统中,多进程是一种常态。那么多进程 的情况下,每次切换进程都需要进行 TLB 清理。这样会导致切换的效率变低。为了解决问题,TLB 引入了 ASID(Address Space ID) 。ASID 的范围是 0-255。ASID 由操作系统分配,当前进程的ASID值 被写在 ASID 寄存器 (使用CP15 c3访问)。TLB 在更新页表项时也会将 ASID 写入 TLB。
如果设置了如果 当前进程的ASID,那么 MMU 在查找 TLB 时, 只会查找 TLB 中具有 相同ASID值 的 TLB行。且在切换进程是,TLB 中被设置了 ASID 的 TLB行 不会被清理掉,当下次切换回来的时候还在。所以ASID 的出现使得切换进程时不需要清理 TLB 中的所有数据,可以大大减少切换开销。
有了这个 ASID + nG 的机制,那么 TLB 中就可以缓存不同进程的页表,不用每次都去 flush TLB,导致性能的损失:
前面我们说了 TTBR0、TTBR1 是根据 TTBCR.N 来进行划分的,典型场景下 OS 跑多任务,如果处理器只能够支持一个 TTBR 的话,也就意味着用户空间和内核空间使用同一个 TTBR,由于内核空间的 code 和 data 几乎是不变的,但是多任务的用户空间都是不一样的,这样就会存在两个问题:
1、多个任务的页表里面,都有同样一部分内核映射的拷贝副本;
2、要修改内核映射的时候,所有任务的页表都要修改;
加入两个 TTBR 的原因,是因为希望内核和用户空间使用两套 TTBR,这样就可以避免上面的尴尬;内核空间固定使用一组,用户空间不断的切换(比如,配合 TTBR0 + ASID 进行性能的提升)
参考文献:
https://www.jianshu.com/p/ef1e93e9d65b
https://www.cs.rutgers.edu/~pxk/416/notes/10-paging.html
https://blog.csdn.net/liyuewuwunaile/article/details/?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&spm=1001.2101.3001.4242


版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/171275.html