ARM 学习之内存管理单元 MMU

先问个问题,为啥要有内存管理单元?

本篇目标:

  • 了解虚拟地址和物理地址的关系
  • 掌握如何通过设置 MMU 来控制虚拟地址到物理地址的转化
  • 了解 MMU 的内存访问权限机制
  • 了解 TLB、Cache、Write Buffer 的原理,使用时的注意事项
  • 通过实例深刻掌握上述要点

内存管理单元 MMU 介绍

S3C2410/S3C2440 MMU 特性

内存管理单元(Memory Management Unit)简称 MMU,它负责虚拟地址到物理地址的映射,并提供硬件机制的内存访问权限检查。

这句话应该说明了 MMU 的作用了吧!

现代的多用户多进程操作系统通过 MMU 使得各个用户进程都有自己独立的地址空间:

  • 地址映射功能使得各进程拥有 “看起来” 一样的地址空间;
  • 内存的访问权限检查可以保护每个进程所用的内存不会被其他进程破坏。

S3C2410/S3C2440 有如下特性:

  • 与 ARM V4 兼容的硬件长度、域、访问权限检查机制。
  • 4 种映射长度:段(1MB)、大页(64KB)、小页(4KB)、极小页(1KB)。
  • 对每个段都可以设置访问权限。
  • 大页、小页的每个子页(sub-page,即被映射页的 1/4)都可以单独设置访问权限。
  • 硬件实现的 16 个域。(域是啥?
  • 指令 TLB(含有 64 个条目)、数据 TLB(含有 64 个条目)。
  • 硬件访问页表(地址映射、权限检查由硬件自动进行)。
  • TLB 中条目的替换采用 round-robin 算法(也称 cyclic 算法)。
  • 可以使无效整个 TLB。
  • 可以单独使无效某个 TLB 条目。
  • 可以在 TLB 中锁定某个条目,指令 TLB、数据 TLB 相互独立。

重点学习地址映射:页表的结构与建立、映射的过程,对于访问权限、TLB、Cache 只粗略介绍。

S3C2410/S3C2440 MMU 地址变换过程

地址的分类

以前的程序是非常小的,可以全部装入内存中。随着技术的发展,出现了以下两种情况。

(1)有的程序很大,它所要求的内存空间超过了内存总容量,不能一次性装入内存;

(2)多道系统中有很多程序需要同时执行,它们要求的内存空间操过了内存总容量,不能把所有程序全部装入内存。

实际上,一个程序在运行之前,没有必要全部装入内存,而仅需要将那些当前要运行的部分先装入内存,其余部分在用到时再从磁盘调入,而当内存耗光时在将暂时不用的部分调出到磁盘。

这使得一个大程序可以在较小的内存空间中运行,也使得内存中可以同时装入更多的程序并发执行,从用户角度看,该系统所具有的内存容量将比实际内存容量大得多,人们把这样的存储器称为虚拟存储器。

哇,这个思想还是挺厉害的,内存总归是有限的,如果在有限的内存中运行无限的程序呢?

我觉得操作嵌入式可以把计算机组成原理和操作系统这两门课一起上了,还可以动手实践。

虚拟存储器从逻辑上对内存容量进行了扩充,用户看到的大容量只是一种感觉,是虚的,在 32 位的 CPU 系统中,这个虚拟内存地址范围为 0~0xFFFFFFFF,我们把这个地址范围称为虚拟地址空间,其中的某个地址称为虚拟地址。与虚拟地址空间、虚拟地址对应的是物理地址空间、物理地址,它们对应实际的内存。

虚拟地址最终需要转换为物理地址才能读写实际的数据,这将通过将虚拟地址空间、物理地址空间划分为同样大小的一块块小空间(称为段或页),然后为这两类小空间建立映射关系。由于虚拟地址空间远大于物理空间,有可能多块虚拟地址空间映射到同一块物理地址空间,或者有些虚拟地址空间没有映射到具体的物理地址空间上去(可以在使用到的时候再映射)。

ARM CPU 上的地址转换过程涉及 3 个概念:

  • 虚拟地址(VA,Virtual Address)
  • 变换后的虚拟地址(MVA,Modified Virtual Address)
  • 物理地址(PA,Physical Address)

没启动 MMU 时,CPU 核、cache、MMU、外设等所有部件使用的都是物理地址。

启动 MMU 后,CPU 核对外发出虚拟地址 VA;VA 被转化为 MVA 供 cache、MMU 使用,在这里 MVA 被转换为 PA;最后使用 PA 读写实际设备(S3C2410/S3C2440 内部寄存器或外接的设备):

(1)CPU 核看到的、用到的只是虚拟地址 VA,至于 VA 如何最终落实到物理地址 PA 上,CPU 核是不理会的。

(2)cache 和 MMU 也是看不见 VA 的,它们利用由 MVA 转换得到 PA。

(3)实际设备看不到 VA、MVA,读写它们时使用的是物理地址 PA。

MVA 是除 CPU 核外的其他部分看见的虚拟地址,VA 与 MVA 之间的变换关系如下图:

如果 VA < 32M,需要使用进程标识号 PID(通过读 CP15 的 C13 获得)来转换为 MVA。

VA 与 MVA 的转换方法如下(这是硬件自动完成的):

利用 PID 生成 MVA 的目的是为了减少切换进程的代价:不使用 MVA 而直接使用 VA 的话,当两个进程所用的虚拟地址空间(VA)有重叠时,在切换进程时为了把重叠的 VA 映射到不同的 PA 上去,需要重建页表、使无效 caches 和 TLBs 等,代价非常大。

使用 MVA 后,进程切换就省事多了:假设两个进程 1、2 运行时的 VA 都是 0~(32M-1),但是它们的 MVA 并不重叠,分别是 0x02000000~0x03FFFFFF、0x04000000~0x05FFFFFF,这样就不必进行重建页表等工作了。

有点懵逼了,VA -> MVA -> PA

下面说到的虚拟地址,如果没有特别指出,就指 MVA。

虚拟地址到物理地址的转换过程

重点来了!!!

将一个虚拟地址转换为物理地址,一般有两个方法:用一个确定的数学公式进行转换或用表格存储虚拟地址对应的物理地址。这类表格称为页表(Page Table),页表由一个个条目(Entry)组成;每个条目存储了一段虚拟地址对应的物理地址及其访问权限,或者下级页表的地址。

在 ARM CPU 中使用第二种方法。S3C2410/S3C2440 最多会用到两级页表:以段(Section,1MB)的方式进行转换时只用到一级页表,以页(Page)的方式进行转换时用到两级页表。

页的大小有 3 种:大页(64KB)、小页(4KB)、极小页(1KB)。

条目也称为 “描述符”(Descriptor),有:段描述符、大页描述符、小页描述符、极小页描述符——它们保存段、大页、小页或极小页的起始物理地址;粗页表描述符、细页表描述符——它们保存二级页表的物理地址。

大概的转化过程如下(一个通用的转换过程,一个是针对 ARM CPU 细化的转换过程):

(1)根据给定的虚拟地址找到一级页表中的条目;

(2)如果此条目是段描述符,则返回物理地址,转换结束;

(3)否则如果此条目是二级页表描述符,继续利用虚拟地址在此二级页表中找到下一个条目;

(4)如果这第二个条目是页描述符,则返回物理地址,转换结束;

(5)其他情况出错。

 “TTB base” 代表一级页表的地址,将它写入协处理器 CP15 的寄存器 C2(称为页表基址寄存器)即可,如下图所示,一级页表的地址必须是 16K 对应的(位 [14:0] 为 0,这里没写错吗?)。

先介绍一级页表。32 位的 CPU 的虚拟地址空间达到 4GB,一级页表中使用 4096 个描述符来表示这 4GB 空间——每个描述符对应 1MB 的虚拟地址,要么存储了它对应的 1MB 物理空间的起始地址,要么存储了下一级页表的地址。(所以到底存了啥?

使用 MVA[31:20] 来索引一级页表,得到一个描述符,每个描述符占据 4 字节,格式如下图:

根据一级描述符的最低两位,可以分为以下 4 种。

(1)0b00:无效

(2)0b01:粗页表(Coarse page table)

位 [31:10] 称为粗页表基址(Coarse page table base address),此描述符的低 10 位填充 0 后就是一个二级页表的物理地址。此二级页表含 256 个条目(所以大小为 1KB),称为粗页表(Coarse page table)。其中每个条目表示大小为 4KB 的物理地址空间,所以一个粗页表表示 1MB 的物理地址空间。

(3)0b10:段(Section)

位 [31:20] 称为段基址(Section base),此描述符的低 20 位填充 0 后就是一块 1MB 物理地址空间的起始地址。MVA[19:0] 用来在这 1MB 空间中寻址。所以,描述符的位 [31:20] 和 MVA[19:0] 就构成了这个虚拟地址 MVA 对应的物理地址。

以段的方式进行映射时,虚拟地址 MVA 到物理地址 PA 的转换过程如下:

1. 页表基址寄存器位 [31:14] 和 MVA[31:20] 组成一个低两位为 0 的 32 位地址,MMU 利用这个地址找到段描述符。

2. 取出段描述符位 [31:20] —— 即段基址,它和 MVA[19:0] 组成一个 32 位的物理地址 —— 这就是 MVA 对应的 PA。

(4)0b11:细页表(Fine page table)

位 [31:12] 称为细页表基址(Fine page table base address),此描述符的低 12 位填充 0 后就是一个二级页表的物理地址。此二级页表含 1024 个条目(所以大小为 4KB),称为细页表(Fine page table)。其中每个条目表示大小为 1KB 的物理地址空间,所以一个细页表表示 1MB 的物理地址空间。

以大页(64KB)、小页(4KB)或极小页(1KB)进行地址映射时,需要用到两级页表。

二级页表有粗页表、细页表两种,“Coarse page table” 和 “Fine page table” 就是这两种页表。

二级页表中描述符的格式如下图:

根据二级描述符的最低两位,可以分为 4 中情况。

(1)0b00:无效

(2)0b01:大页描述符

(3)0b10:小页描述符

(4)0b11:极小页描述符

细节先不敲了,太多了

从段、大页、小页、极小页的地址转换过程可知:

(1)以段进行映射时,通过 MVA[31:20] 结合页表得到一段(1MB)的起始物理地址,MVA[19:0] 用来在段中寻址。

(2)以大页进行映射时,通过 MVA[31:16] 结合页表得到一个大页(64KB)的起始物理地址,以 MVA[15:0] 用来在大页中寻址。

(3)以小页进行映射时,通过 MVA[31:12] 结合页表得到一个小页(4KB)的起始物理地址,MVA[11:0] 用来在小页中寻址。

(4)以极小页进行映射时,通过 MVA[31:10] 结合页表得到一个极小页(1KB)的起始物理地址,MVA[9:0] 用来在极小页中寻址。

内存访问权限检查

TLB 的作用

Cache 的作用

S3C2410/S3C2440 MMU、TLB、Cache 的控制指令

MMU 使用实例:地址映射

程序设计

开发板的 SDRAM 的物理地址范围处于 0x30000000~0x33FFFFFF,S3C2410/S3C2440 的寄存器地址范围都处于 0x48000000~0x5FFFFFFF。

在学习 GPIO 的时候,是通过 GPBCON 和 GPBDAT 这两个寄存器的物理地址 0x56000010、0x56000014 写入特定的数据来驱动 4 个 LED。

寄存器的地址似乎没有对应上,尼玛我这个板子只有 3 个 LED,骗子,赔我一个 LED。

这个实例将会开启 MMU,并将虚拟地址空间 0xA0000000~0xA0100000 映射到物理地址空间 0x30000000~0x33FFFFFF 上,并在连接程序时将一部分代码的运行地址指定为 0xB0004000(这个数值有点奇怪,看下去就会明白),看看能否令程序跳转到 0xB0004000 处执行。

程序将只使用一级页表,以段的方式进行地址映射。

32 位 CPU 的虚拟地址空间达到 4GB,一级页表中使用 4096 个描述符来表示这 4GB 空间(每个描述符对应 1MB 的虚拟地址),每个描述符占 4 字节,所以一级页表占 16KB。

本实例使用 SDRAM 的开始 16KB 来存放一级页表,所以剩下的内存开始物理地址为 0x30004000。

将程序代码分为两个部分:

  • 第一部分的运行地址设为 0,它用来初始化 SDRAM、复制第二部分代码到 SDRAM 中(存放在 0x30004000 开始处)、设置页表、启动 MMU,最后跳转到 SDRAM 中(地址 0xB0004000)去继续执行;
  • 第二部分的运行地址设为 0xB0004000,它用来驱动 LED。

程序流程图如下:

值得注意的是,在开启了 MMU 之后,无论是 CPU 取值还是 CPU 读写数据,使用的都是虚拟地址。

copy_2th_to_sdram 函数用来将第二部分代码(即由 leds.c 编译的来的代码)从 Steppingstone 中复制到 SDRAM 中。

在连接程序时,第二部分代码的加载地址被指定为 2048,重定位地址为 0xB0004000。

所以系统从 NAND Flash 启动后,第二部分代码就存储在 Steppingstone 中地址 2048 之后,需要把它复制到 0x30004000 处(此时尚未开启 MMU,虚拟地址 0xB0004000 对应的物理地址在后面设为 0x30004000)。Steppingstone 总大小为 4KB,不妨把地址 2048 之后的所有数据复制到 SDRAM 中,素以源数据的结束地址为 4096。

剩下的 create_page_table、mmu_init 就是重点了,前者用来设置页表,后者用来开启 MMU。

先看看 create_page_table 函数。它用于设置 3 个区域的地址映射关系。

(1)将虚拟地址 0~(1M-1) 映射到同样的物理地址去,Steppingstone(从 0 地址开始的 4KB 内存)就处于这个范围中。

是虚拟地址等于物理地址,可以让 Steppingstone 中的程序(head.S 和 init.c)在开启 MMU 前后不需要考虑太多的事情。

(2)GPIO 寄存器的起始物理地址范围为 0x56000000,将虚拟地址 0xA0000000~(0xA0000000+1M-1) 映射到物理地址 0x56000000~(0x56000000+1M-1)。

(3)开发板的 SDRAM 的物理地址范围为 0x30000000~0x33FFFFFF,将虚拟地址 0xB0000000~0xB3FFFFFF 映射到物理地址 0x30000000~0x33FFFFFF。

连接脚本 mmu.lds 将程序分为两个段:first 和 second。

前者由 head.o 和 init.o 组成,它的加载地址和运行地址都是 0,所以运行前不需要重新移动代码。

后者由 leds.o 组成,它的加载地址为 2048,重定位地址为 0xB0004000,这表明段 second 存在编译器所得映像文件地址 2048 处,在运行前需要将它复制到地址 0xB0004000 处,这由 init.c 中的 copy_2th_to_sdram 函数完成(注意,此函数将代码复制开始地址为 0x30004000 的内存中,这是开启 MMU 后虚拟地址 0xB0004000 对应的物理地址)。

卧槽,这个过程有点复杂啊!!

不过能使用  MMU 也算一个进阶了,我没说我啊!!

还要消化一下,再分析代码。

猜你喜欢

转载自www.cnblogs.com/tuhooo/p/11183817.html