MMU-AArch32
MMU即内存管理单元,是操作系统对内存进行分段分页管理的硬件基础,它是一个或者一组芯片,一般位于协处理器中,它的重点任务是完成虚拟地址到物理地址的转化,同时还会对页面进行权限管理,它与CPU Core之间通信方式是通过PageFault-“缺页异常”。
PageTable即页表,OS内核中供MMU做内存寻址使用的软件数据结构,是虚拟地址与物理地址的转换关系表,同时记录了页面访问权限。
TLB即页表缓存,又被称为快表,减少MMU访问内存页表,加快MMU寻址速度。
MMU的作用
从一个简单的例子说起
下面这幅图描述了一个抽象出来的最简单的MMU与OS内核交互过程,二者配合完成CPU对内存寻址。
对该图说明如下:
① 这是一个简单的16位的CPU通过MMU查询页表做内存寻址的过程概要,并不是某架构/某OS上的精确描述,比如页表项等不同OS内核实现是并不一致的;
② 16bit的CPU能寻址虚拟内存范围是2^16=64KB,如上图左边“虚拟内存”显示; 依旧按照最常用的4KB分页对内存进行分页管理;
③ 中间紫色分割线:以上部分,是正常寻址过程;以下部分,是触发缺页异常后做页换出、更新页表寻址过程;
④ 物理内存只有32KB,显然不能满足对64KB虚拟内存进行地板式的一一映射(flat mapped),因此需要OS有缺页异常处理能力、以及对页面的swap能力;
⑤ 这里暂时并未考虑存在TLB的情况,只是一个裸的MMU从内存取PageTable并完成页转换;
⑥ 虚拟地址==>物理地址映射 是以Page页(这里是4KB)作为最小单位的,即每次最小映射1页
⑦ 正常寻址过程:
- MMU从TTBR0/1寄存器读入PGD物理地址
- MMU从虚拟地址最高4位读入一级页表的页表项偏移PageIndex
- MMU将PGD+PageIndex找到该Page页对应的物理地址,也即一级页表项的内容
- MMU对该Page页物理地址进行权限检查(检查最低bit位P是否==1,相等表示有效不等表示无效),满足后,根据页框物理地址找到对应物理页框起始(或取最高4bit找到对应的物理页框号,根据页框号和物理地址起始值找到对应物理页框)
- MMU用物理页框对应的物理地址+虚拟地址记录的PageInsideOffset页内偏移,找到该虚拟地址对应的物理地址,并将该物理地址发到地址总线上,内存控制器按照该物理地址进行访存操作
⑧ 触发缺页异常过程:
- MMU前几个操作与⑦中正常流程一样
- MMU从权限检查开始,当检查到最低位P!=1说明页表中记录的页框物理地址无效,MMU触发PageFault缺页异常
- CPU接收到该异常后,跳转到PageFault异常处理流程,调用OS内核的handler进行PageFault处理:OS内核进行权限检查看vma虚拟地址空间描述符中记录的权限是否能够满足,能够满足则根据LRU算法(最近最少使用原则)选择一个物理页将该页swap换出到磁盘,然后修改与该物理页对应的虚拟页页表,将P位置为0标识该页表项无效;最后修改触发PageFault的虚拟页对应的页表,将该物理页地址写入表项,并将P置为1标识表项有效,到此旧物理页换出、旧页表失效、新页表建立均已经完成。MMU已经可以按照新页表项进行正常寻址了。
- MMU后面寻址的过程与⑦中又一致了
以上就是抽象出来的一个简单的MMU功能描述,它与OS内核配合完成CPU的内存寻址过程。
硬件位置
在ARM架构中,一般MMU位于CP15协处理器中,一般CP15协处理器中与MMU相关的寄存器如下:
Register Name | Description | Usage |
---|---|---|
寄存器1 | 它的“某些位”用于配置MMU中一些操作 | 系统控制 |
寄存器2 | 保存内存中页表的基地址 | 页表基址 |
寄存器3 | 设置域的访问控制属性 | 内存访问权限 |
寄存器4 | 保留 | 用于GIC<->CPU交互接口 |
寄存器5 | 内存访问失效状态指示 | PageFault时错误原因 |
寄存器6 | 内存访问失效时失效的地址 | PageFault时访问的错误地址 |
寄存器8 | 控制与清除TLB内容相关的操作 | TLB控制 |
寄存器10 | 控制与锁定TLB内容相关的操作 | 内存模型与TLB控制 |
寄存器13 | 进程上下文与线程PID寄存器 | 用于线程切换 |
如下是ARMv8 SPEC中描述CP15的32-bit控制寄存器的截图
功能
- 地址转换
- 权限检测
- 异常中止(PageFault)
- 刷新TLB,锁定TLB中某些页表项
MMU地址转换的过程就是通过PageTable以虚拟地址查找物理地址的过程。
① MMU支持存储访问基于段(sections)或页(pages)
- 段(Sections) 大小为1MB的存储块
- 大页(Large pages) 大小为64KB的存储块
- 小页(Small pages) 大小为4KB的存储块
- 极小页(Tiny pages) 大小为1KB的存储块
段和大页支持允许在TLB中只使用一个条目来映射一大块存储区域。通过采用额外的访问控制机制,可以将小页分成大小为1KB的子页,将大页分成大小为16KB的子页,极小页不能再分了,只能以1KB大小的整页为大小进行访问控制;
② 通常情况,段式地址转换过程只需要一级页表,而页式地址转换过程需要二级以上页表
Linux内核,按照其实现并不需要段式映射,只需要完成页式映射,而且最多能支持4级页表,工程中一般会选择如下组合:
- AArch32平台:使用二级页表+4KB Page
- AArch64平台:使用三级页表/四级页表+4KB Page,或者二级页表/三级页表+64KB Page
③ 无论如何选择映射方式、页大小、内存模型、以及CPU位数,MMU均根据OS选择的页表实现来完成“虚拟地址”=>“物理地址”的转换
页表实现见下一节<PageTable的作用>
根据页表具体实现,MMU寄存器设置:
- C1 - S(System)和R(ROM)位来控制二级页目录项中AP bit(段/页)的访问权限
- C2 - bit[31:14]存放一级页表基地址,bit[13:0]为0。因此,一级转换表的基地址必须是16KB对齐的;
Tips:
这也间接决定了32bit虚拟地址中“一级页表内offset”所占长度是12bit(12=32-18-2)
而虚拟地址高12bit就是一级页表内offset,也即决定了:每个“一级页目录项”索引“1M虚拟地址空间”(这时因为它每加1,相当于整体地址空间增加2^20)
如下图所示:
参考代码
TODO...
- C3 - TODO...
- C4 - TODO...
- C5 - TODO...
- C6 - TODO...
- C8 - TODO...
- C10 - TODO...
- C13 - TODO...
权限控制
① 通过对一级页目录项中“bit[8:5]域字段”去索引到CP15的“DACR寄存器(32bit)”中对应的域上,查看权限:
Tips:
域是指段、大页和小页的集合,AArch32支持16个域。
DACR寄存器是一个32bit的,被分成了16个Domain,每个Domain占2个bit,对应一个域,每个域在寄存器初始化时会被赋予一个默认值,如下:
bit[8:5]这4个bit是以上16个Domain的索引,通过该值可以知道该一级页目录项索引的这1M虚拟空间属于那个Domain,然后查看对应bit[n+1:n]来确认该Domain的访问权限。
DACR寄存器每个Domian控制权限描述如下:
Value | Meaning | Note |
---|---|---|
0b00 | 没有访问权限 | 任何访问将产生域错误 |
0b01 | 客户类型 | 检查段或页描述符的访问权限位来决定访问权限 |
0b10 | 保留 | 使用这个值会产生不可预知结果 |
0b11 | 管理员权限 | 不考虑段或页描述符的访问权限位,不会产生访问失效 |
以上字段标记了允许对整个域的访问进行使能和禁止,这样可以将整个内存空间包含在虚拟存储空间中,或排除其外。支持两种形式的域访问:
- 客户类型 - 是域的用户(执行程序,访问数据),受限于组成域的各个段和页的访问权限。
- 管理员类型 - 控制域的行为(域的当前段和页以及域访问),不受限于组成域的各个段和页的访问权限
一个程序可以是其中一些域的客户,以及另一些域的管理员,对其他域没有访问权限。这允许需要访问不同存储资源的程序具备非常灵活的存储保护措施。
② 通过检查二级页目录项中各个“权限访问控制位”-APx(2bit)以及CP15 C1寄存器的S/R bit来实现对各个段、页的权限控制:
(二级页目录项中这些bit详见下面一节<PageTable的作用>)
AP | C1.S | C1.R | Supervisor Mode | User Mode | Notes |
---|---|---|---|---|---|
0b00 | 0 | 0 | 没有访问权限 | 没有访问权限 | 访问将产生PageFault |
0b00 | 1 | 0 | 只读 | 没有访问权限 | 管理模式只读,管理模式写/用户模式访问将产生PageFault |
0b00 | 0 | 1 | 只读 | 只读 | 写将产生PageFault |
0b01 | X | X | 读/写 | 没有访问权限 | 用户模式访问将产生PageFault |
0b10 | X | X | 读/写 | 只读 | 用户模式写将产生PageFault |
0b11 | X | X | 读/写 | 读/写 | |
0bXX | 1 | 1 | 不可预知 | 不可预知 | 保留字段 |
有2种因为存储器访问限制而导致ARM处理器中止执行的机制:MMU失效、外部中止
Tips:
由MMU检测到的访问中止将发生在任何外部存储器访问之前,而引起外部中止的外部访问将由外部存储系统来停止。
如果存储访问中止发生在指令提取阶段,那么如果当处理器试图去执行非法访问对应的指令时,将产生指令预取中止异常.
如果存储访问中止是在数据访问阶段,就会产生一个数据中止异常。数据访问同时引起多种类型的数据中止时,它们是由优先级的.
① MMU失效:
MMU检测到存储访问失效,并向CPU发送PageFault缺页异常,分为如下4种类型
- 对齐失效
- 地址转换失效
- 域失效
- 权限检查失效
② 外部中止:
外部存储系统报告一个非法存储访问,分为如下3种类型
- 行提取
- 存储器访问(uncached/unbufferd访问)
- 页表访问
失效地址寄存器(FAR):引起数据异常的“虚拟地址”
失效状态寄存器(FSR):包含一个4位失效状态(FS[3:0])和所属域编号
Tips:
指令预取中止时,这些寄存器不会发生变化。只有发生数据访问异常时,才会发生改变。
失效状态标识表:
Priority | Coming | PageTable Position | FS[3:0] | 域字段 | FAR |
---|---|---|---|---|---|
最高 | 极端异常 | - | 0b0010 | 无效 | 实现定义 |
- | 中断向量异常 | - | 0b0000 | 无效 | 有效 |
- | 地址对齐 | - | 0b00x1 | 无效 | 有效 |
- | 转换时外部存储异常 | 一级页目录 | 0b1100 | 无效 | 有效 |
- | 转换时外部存储异常 | 二级页目录 | 0b1110 | 有效 | 有效 |
- | 地址转换 | 段 | 0b0101 | 无效 | 有效 |
- | 地址转换 | 页 | 0b0111 | 有效 | 有效 |
- | 域 | 段 | 0b1001 | 有效 | 有效 |
- | 域 | 页 | 0b1011 | 有效 | 有效 |
- | 权限 | 段 | 0b1101 | 有效 | 有效 |
- | 权限 | 页 | 0b1111 | 有效 | 有效 |
- | 行提取时外部存储异常 | 段 | 0b0100 | 有效 | 有效 |
- | 行提取时外部存储异常 | 页 | 0b0110 | 有效 | 有效 |
- | 非行提取时外部存储异常 | 段 | 0b1000 | 有效 | 有效 |
最低 | 非行提取时外部存储异常 | 页 | 0b1010 | 有效 | 有效 |
Linux内核异常处理代码
1 | TODO...
|
TODO...
MMU初始化
CP15的寄存器Reg1的bit[0]标识使能/禁止MMU,CPU复位时该位被清0,所以复位后MMU是禁止状态
① 是否支持cache和buffer由各个芯片平台具体实现决定,但访存规则是:
- 如果芯片设计当禁止MMU时也禁止cache和写buffer,C、B控制位的值忽略
- 如果芯片设计当禁止MMU时允许cache和写buffer,那么:
- 数据访问被视为uncacheable和unbufferable(C=0,B=0)
- 使用同一的TLB时,指令访问视为uncachable(C=0);使用分开的TLB时,指令访问视为cachable(C=1)
② CPU访存不进行权限控制,MMU也不会生成存储访问中止信号
③ 所有的物理地址和虚拟地址相同(即平板地址映射模式)
在设置CP15的Reg1[0]=1前,即“开启MMU”之前:
① CP15各个寄存器必须已经完成初始化
② OS内核必须已经完成了页表的建立
在“开启MM”U之后,MMU会改变地址映射关系,因此:
① 使能/禁止MMU的代码必须是位置无关的(物理地址==虚拟地址),防止跑飞
② 开后要立即对cache做一次invalid
③ 开启后要立即对TLB做一次invalid
Linux内核中对MMU初始化参考代码如下
1 | TODO...
|
PageTable的作用
PageTable,页表又被称为translation table,按照分页机制又被分成二级、三级、四级这些具体实现。
页表结构
不同类型页表各级结构中的项,定义了1KB/4KB/16KB/64KB/1MB/16MB等大小区域属性,主要包含以下三个方面:
- 地址转换关系(base addr/offset)
- 内存页访问权限(域/AP)
- Cachability和Bufferability控制(C位和B位)
Tips:
以二级页表为例,页表完整结构包含:
* 全局页目录地址PGD => 保存了索引的一级页表基址(物理地址)、权限控制bit
* 一级页表 => 一块保存了N个一级页目录项的区域,每个项保存了索引的二级页表基址(物理地址)、权限控制bit
* 二级页表 => 一块保存了N个二级页目录项的区域,每个项保存了索引的物理页框号(物理地址)、权限控制bit
在32位架构上,地址线理论宽度是32bit的,也就是理论上能寻址最大物理地址范围是0GB~4GB,在实际实现用由于寄存器/IO空间等都需要占用地址范围,存储器自身实际物理大小一定是达不到4GB的。只是这应该也不会影响到我们对虚拟地址空间0GB~4GB划分的理解。
AArch32 Linux memory layout with 4KB pages + 2 levels
Start | End | Size | Use |
---|---|---|---|
00000000 | bfffffff | 3GB (32-bit) | user |
c0000000 | ffffffff | 1GB (30-bit) | kernel |
MMU支持多种地址转换模式组合
- 段式(段/超级段) + 一级页表 + 1M段
- 页式 + 二级页表(粗粒度) + 64KB大页/4KB小页
- 页式 + 二级页表(细粒度) + 64KB大页/4KB小页/1KB极小页
① 无效路径:(缺页)
比特位 | 取值 | 描述 |
---|---|---|
bit[1:0] | 0b00 | 该一级页表项无效,MMU触发PageFault,OS内核kernel panic掉 |
bit[31:2] | - | IGN,硬件忽略位,软件可以考虑根据情况使用 |
② 超级段式寻址路径:这个与段式只是寻址大小不同,其它类似见下面段式路径
③ 段式寻址路径:
比特位 | 取值 | 描述 |
---|---|---|
bit[1:0] | 0b10 | 段描述符 |
bit[2] | 据实际 | B位,控制该内存区域是否经过Write-Buffer |
bit[3] | 据实际 | C位,控制该内存区域是否经过Cache |
bit[4] | 据实际 | IMP,根据具体实现定义 |
bit[8:5] | 据实际 | 域位,指定该内存区域所在的“域”,域号0~15 |
bit[9] | 0b0 | SBZ,未使用应该为0 |
bit[11:10] | 据实际 | 访问权限,控制段访问是否合法 |
bit[19:12] | 0b00000000 | SBZ,未使用应该为0 |
bit[31:20] | 据实际 | 段区域基址,是1MB对齐的,区域起始“物理地址” |
对应32bit虚拟地址布局:(能寻址的每个物理段大小是1MB=2^20Bytes)
④ 页式粗粒度页表(二级)路径:
比特位 | 取值 | 描述 |
---|---|---|
bit[1:0] | 0b01 | 粗粒度描述符 |
bit[4:2] | 据实际 | IMP,根据具体实现定义 |
bit[8:5] | 据实际 | 域位,指定该内存区域所在的“域”,域号0~15 |
bit[9] | 0b0 | SBZ,未使用应该为0 |
bit[31:10] | 据实际 | 二级页表区域基址,是1KB对齐的,区域起始“物理地址” |
对应32bit虚拟地址布局:(能寻址的每个物理页大小是4KB=2^12Bytes)
⑤ 页式细粒度页表(二级)路径:
比特位 | 取值 | 描述 |
---|---|---|
bit[1:0] | 0b11 | 细粒度描述符 |
bit[4:2] | 据实际 | IMP,根据具体实现定义 |
bit[8:5] | 据实际 | 域位,指定该内存区域所在的“域”,域号0~15 |
bit[11:9] | 0b000 | SBZ,未使用应该为0 |
bit[31:12] | 据实际 | 二级页表区域基址,是4KB对齐的,区域起始“物理地址” |
对应32bit虚拟地址布局:(能寻址的每个物理页大小是1KB=2^10Bytes)
① 无效页:
比特位 | 取值 | 描述 |
---|---|---|
bit[1:0] | 0b00 | 该二级页表项无效,MMU触发PageFault,OS内核酌情处理(kernel panic/分配物理页) |
bit[31:2] | - | IGN,硬件忽略位,软件可以考虑根据情况使用 |
② 64KB大页:
比特位 | 取值 | 描述 |
---|---|---|
bit[1:0] | 0b01 | 64KB大页描述符 |
bit[2] | 据实际 | B位,控制该内存区域是否经过Write-Buffer |
bit[3] | 据实际 | C位,控制该内存区域是否经过Cache |
bit[11:4] | 据实际 | AP3~0,权限访问控制位。大页分为4个子页,分别对应AP3~0访问权限 |
bit[15:12] | 0b0 | SBZ,未使用应该为0 |
bit[31:16] | 据实际 | 大页基址,是64KB对齐的,区域起始物理页框号 |
对于粗粒度页表(虚拟地址布局见上面),因为页框内offset==12,因此该描述符需要在二级页目录项中重复16次(64KB=4KB×16);
对于细粒度页表,因为页框内offseet==10,因此该描述符需要在二级页目录中重复64次(64KB=1KB×64);
③ 4KB小页:
比特位 | 取值 | 描述 |
---|---|---|
bit[1:0] | 0b10 | 4KB小页描述符 |
bit[2] | 据实际 | B位,控制该内存区域是否经过Write-Buffer |
bit[3] | 据实际 | C位,控制该内存区域是否经过Cache |
bit[11:4] | 据实际 | AP3~0,权限访问控制位。小页分也为4个子页,分别对应AP3~0访问权限 |
bit[31:12] | 据实际 | 小页基址,是4KB对齐的,区域起始物理页框号 |
对于粗粒度页表(虚拟地址布局见上面),因为页框内offset=12,因此该描述符需要在二级页目录项中只需要1次(4KB=4KB×1);
对于细粒度页表,因为页框内offset=10,因此该描述符需要在二级页目录中重复4次(4KB=1KB×4);
④ 1KB极小页:
比特位 | 取值 | 描述 |
---|---|---|
bit[1:0] | 0b11 | 1KB小页描述符 |
bit[2] | 据实际 | B位,控制该内存区域是否经过Write-Buffer |
bit[3] | 据实际 | C位,控制该内存区域是否经过Cache |
bit[5:4] | 据实际 | AP0,权限访问控制位 |
bit[9:6] | 0b0 | SBZ,未使用应该为0 |
bit[31:10] | 据实际 | 小页基址,是1KB对齐的,区域起始物理页框号 |
对于粗粒度页表(虚拟地址布局见上面),因为二级页目录数量不够用(256个=1K/4Bytes)无法完成寻址,无法使用;
对于细粒度页表,因为页框内offset=10,因此该描述符需要在二级页目录只需要1次(1KB=1KB×1);
Tips:
页表的大小 - 以4KB页+二级粗粒度页表为例
虚拟地址范围bit[31:20][19:12][11:0],分别标识一级页表内offset、二级页表内offset、物理页内offset
bit[31:20]:范围是0 ~ 4096(4K=2^12),可以查找4K个一级页目录项,即:每个一级页表大小是16KB(4B×4K),寻址范围是1M(4G/4×4K)
bit[19:12]:范围是0 ~ 256(256=2^8),可以查找256个二级页目录项,即:每个二级页表大小是1KB(4B×256),寻址范围是4KB(1M/256)
bit[11:0]: 范围是0 ~ 4096(4K=2^12),可以查找4K个物理页框内地址,即:每个物理页框大小是4KB(1B×4K),对应地址是1Byte(4KB/4K)
32bit内核选择了“页式映射 + 二级页表(粗粒度)+ 4KB小页”转换模式,页表转换过程
其它OS选择其它类型转换过程,基本都是类似的。
初始化
Linux内核PageTable初始代码
1 | TODO...
|
存储
swapper_pg_dir保存了Linux内核页表首地址,它是内核在编译时静态初始化的临时页全局目录,内核运行起来后就把它当做了内核页表起始。
正是因为编译时静态初始化的,因此我们在内核C代码中搜索定义时,只在/kernel/arch/arm/include/asm/pgtable.h中找到了一条:
1 | extern pgd_t swapper_pg_dir[PTRS_PER_PGD];
|
其实它真正定义和初始化的地方在kernel/arch/arm/kernel/head.S中:
1 2 | .globl swapper_pg_dir
.equ swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE
|
定义全局变量,并且它设置到“Kernel虚拟地址-页表大小”的位置
TODO...
TODO...
工程实践
MetalDown漏洞
TODO...(是否只64bit上有meltdown漏洞??)
TLB的作用
Translation Lookaside Buffer,又称为快表,用于缓存页表项,集成在MMU内部。
分类
从硬件上讲,通常每个存储器接口中有一个TLB:
- 只具备单个存储器接口的系统一般只有一个统一的TLB
- 若一个系统具有独立的"指令存储器接口"和"数据存储器接口",则通常具有分开的指令TLB(指令MicroTLB)和数据TLB(数据MicroTLB)
- 而在有的平台上,TLB也会被分成L1 TLB(一级TLB)和L2 TLB(二级TLB)等,比如在main TLB下还存在micro TLB专门给icache/dcache使用的,典型的micro TLB是8个条目、main TLB是64个条目;
Tips:
CPU对于“指令”和“数据”的区分是根据”时序“判断的:
在“取指周期”中从内存读出的信息流是指令流,流向控制器;
在“执行周期”中从内存取出的信息时数据流,流向运算器;
作用
包含TLB的MMU访存示意图:
增加TLB之后,CPU寻址流程大致变为:CPU => MMU => TLB => Memory PageTable => TLB => Memory
结构
其中:
- ASID是address space id,用于标记这一条也表记录是属于哪个进程
- VA Tag是valid tag,标记该条也表记录是否有效,只有它有效时这个TLB项才会记录虚拟地址、物理地址、页属性等等
更新
从软件角度,对于TLB的操作基本就是各种invalid方法,因为没有条目存在于TLB却不存在于PageTable的情况,因此也就不需要从TLB向PageTable去做flush动作。
TODO...
CPU内存寻址过程
这里就描述一个32bit的用户态进程,从通过malloc分配堆内存到真正拿到物理内存的全过程,如下图表述:
TODO...
参考文档