Mini2440裸机开发之MMU
一、linux中的内存管理
1.1 虚拟内存的引出
我们都知道linux是一个多进程的操作系统,既然是多进程操作系统,那就会存在一个问题。
假设我们有两个进程A和B、并且A和B都是直接访问的物理地址,如果程序A使用了物理地址0x40000,B程序并不知道程序A使用了物理地址0x40000,B程序修改了这个地址的内容,将会影响到A程序的运行,很显然这样是行不通。
在32位操作系统下,我们期望的是每一个进程都有4GB的内存空间,而且在对自己的内存空间进行读写时,并不会影响其他进程的内存空间。
然而,32位操作系统下,我们的物理内存实际上只有4GB,linux操作系统解决这个问题的方法就是有一个虚拟内存的概念,每一个进程都有一个独立的4GB虚拟内存。虚拟地址通过MMU转换成真正的物理地址。
1.2 物理地址和虚拟地址等术语
在linux内存管理中,我们经常听到线性地址、逻辑地址、物理地址和虚拟地址,咋一看很容易混淆,让人云里雾里。
线性地址和逻辑地址都是x86的概念,主要用于内存分段机制,而在ARM平台上,没有分段机制,线性地址/逻辑地址和虚拟地址都是同一个概念,都统称为虚拟地址。
- 物理地址:Physical addresses are those used by the actual hardware system,是硬件真实使用的地址;
- 虚拟地址: Virtual addresses are those used by you, and the compiler and linker, when placing code in;
1.3 内存管理方式
在多任务操作系统中,内存管理方式一般可以分为:
- 页式存储结构:将全部物理内存划分为同样大小的页面,每一个页面都有一个页号,有一片存储区域存储"页表"数据结构,页表记录了页面和索引号之间的关系。内存线性地址的前几位的含义变成了“页表索引号“,CPU通过它在“页表”中查到页号,再加上地址后几位的偏移量,得到物理地址的值;
- 段式存储结构:段氏存储和页式存储类似,只是将内存划分的基本单位由页变成了段;
二、MMU
2.1 什么是MMU
我们之前提到过将虚拟地址转换成物理地址是通过MMU完成的。那MMU到底是什么呢?
MMU全称是memory management unit,中文叫做内存管理单元。在ARM的体系结构中,MMU可以使用内存中保存的页表来进行虚拟地址到物理地址的转换,此外MMU还可以控制cache的策略,内存的属性以及访问权限的设置。
ARM920T架构MMU单元具有以下特征:
- 映射大小为1MB(段)、64KB(大页)、4KB(小页)和1KB(极小页);
- 段访问权限;
- 页访问权限;
- 域访问权限;
- TLB(Translation lookaside buffers),又叫快表;
- 清除TLB,当内存中页表的内容改变或者使用新的页表时TLB中的内容需要清空,使用CP15寄存器C8完成该功能;
- 锁定TLB,可以将一个页表项锁定在TLB中,以加快访问速度,使用CP15寄存器C10完成此功能;
- 指令MMU和数据MMU;
2.2 协处理器
CP15,即通常所说的系统控制协处理器(System Control Coprocesssor)。它负责完成大部分的存储系统管理。
CP15包含16个32位寄存器,其编号为0~15。实际上对于某些编号的寄存器可能对应多个物理寄存器,在指令中指定特定的标志位来区分这些物理寄存器。这种机制有些类似于ARM中的寄存器,当处于不同的处理器模式时,某些相同编号的寄存器对应于不同的物理寄存器。
这里我直接截图了,内容太多,自己看吧。
2.3 域访问权限
所有段都有一个关联的域,域是内存区域的访问控制机制。ARM共有16个域,每一个域都可以设定不同的权限,将段分配到某个域,就使得这个段的权限和设定的域的权限一样。
每一个域的访问控制权限定义在CP15域访问控制寄存器C3中:
31:30 | 29:28 | 27:26 | 25:24 | 23:22 | 21:20 | 19:18 | 17:16 | 15:14 | 13:12 | 11:10 | 9:8 | 7:6 | 5:4 | 3:2 | 1:0 |
D15 | D14 | D13 | D12 | D11 | D10 | D9 | D8 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
每个域都有一个 2 位字段来定义对其的访问权限:
- 00:不可访问,任何访问都会产生domain fault;
- 01:client、访问时需要检查访问权限;
- 10:保留;
- 11:manager、访问时不检查访问权限;
一级转换描述符的AP位和CP15控制寄存器C1的位8(S)和位9(R)决定了如何对某块内存进行检查:
AP | SR | R | 特权模式 | 用户模式 |
00 | 0 | 0 | 不可访问 | 不可访问 |
00 | 1 | 0 | 只读 | 不可访问 |
00 | 0 | 1 | 只读 | 只读 |
00 | 1 | 1 | - | - |
01 | x | x | 读写 | 不可访问 |
10 | x | x | 读写 | 只读 |
11 | x | x | 读写 | 读写 |
xx | 1 | 1 | - | - |
2.4 快表
由于页表是存放在内存中的,假设我们使用的是段式存储结构,这使得CPU在每存储一个数据时,都需要两次访问内存。
第一次是访问内存中的转换表(Translation table),从中找到指定段的物理段号,再将段号与段内偏移进行拼接,形成物理地址。
第二次访问内存时,才是从第一次所得地址中获得所需数据。因此采用这种方式会使的计算机的处理速度降低1/2.,可见,以此高昂的代价来换取存储器空间利用率的提高,是得不偿失的。
根据程序访问的局限性,可以使用一个高速容量较小的存储器来存储近期用到的页表条目,避免每次地址转换都到内存中查找,这样就可以大幅度提高性能,而这个高速存储器在这里被被称作快表。
2.5 VA、MVA、PA
上图为ARM920T架构的结构图,从图中我们可以看到指令/数据地址的转换过程:
- IVA->IMVA->IPA;
- DVA->DMVA->DPA;
ARM CPU地址转换涉及三种地址:
- 虚拟地址(VA Virtual address);
- 变换后的虚拟地址(MVA,Modified virtual address);
- 物理地址:(PA, Pyhsical address);
没有开启MMU时,CPU核心、cache、MMU、外设所有部件使用的都是PA。
启动MMU之后,CPU核心对外发出VA,VA转换成MVA供cache、MMU使用,在这里MVA被转换成PA,最后使用PA读取实际数据:
- CPU核心看到和用到的都是VA,至于VA如何去找对应的物理地址PA,CPU核心不关心;
- cache和MMU看不到VA,它们利用MVA转换得到PA;
- 实际设备看不到VA、MVA,读取它们使用的都是物理地址;
VA和MVA的变换关系:
如果VA<32M,需要使用进程标识号PID(通过读CP15寄存器C13获取)来转换为MVA;
if(VA<32M) then MVA = VA | (PID << 25) else MVA = VA
使用MVA而不使用VA的目的,当有若干个进程的时候,并且进程的链接地址都相同时,转换为MVA地址时将不会重叠。
此外每一个进程都是拥有自己的页表的,如果保证每一个进程页表映射到到的物理内存页不冲突呢?操作系统在创建进程的时候,会利用某种分配算法,进行内存分配和回收。具体可以参考操作系统内存动态分区分配算法(Java实现)。
2.6 cache
ARM处理器一般内置 I cache、D cache、writer buffer。
利用程序访问的局部性,在主存和CPU之间建立高速缓存,把正在执行的指令地址附近的一部分指令或数据从主存加载到缓存中。
缓存的写回方式有两种:
- 写穿式(write through):一旦更新cache、立即写回主存、保持cache和主存数据的始终一致;
- 回写式(write back):数据只写到cache、并且dirty位作为标识,一旦数据被强迫换出时,检查该标识位,判断是否要写回主存;
cache有以下两个操作:
- 清空:把cache和write buffer中已经脏(修改过的、但未写回主存)数据写回主存;
- 使无效:使之不能再使用,并不将脏的数据写入主存;
I cache:系统刚上电,关闭的,CP15寄存器C1位12 I;
D cache:系统刚上电,关闭的,CP15寄存器C1位2 D;
write buffer和D cache是密切相连,必须在MMU开启后才能被使用。
三、MMU地址转换(MVA->PA)
下面这张图是从ARM920T手册截取到的,这张图描述了如何将一个MVA转换成物理地址的过程。
3.1 TTB base(Translation Tbale Base Register)
TTB寄存器指向内存中转换表的基地址,TTB寄存器在读取的时候低14位被设置成0。转换表最多有4096个项,每一项长度为32位,并且每一项描述1MB的虚拟内存,从而最多可以描述4GB的虚拟内存。
注意:这里我称作转换表,是直接英语翻译过来的,实际上叫法应该是一级页表。
3.2 Level one fetch
通过MVA[31:20],我们首先得到Table index,然后根据Table index查找转换表中对应的项,转换表中的每一项又叫做转换描述符。
这个转换描述符就是一个存储在内存中的32位数据。通过这个描述符最后两位,可以判断是什么转换:
- 如果是段转换,就按照段转换的格式去转换物理地址(最后两位为10);
- 如果是页转换,就去找第二级的页表,然后再按照二级页表的格式去转换物理地址(最后两位为01:粗页,11:细页);
3.3 一级转换描述符
其中:
- Domain字段用于指定当前段/页的权限和哪一个域的权限一致;
- 最后两位用来指定物理地址的转换方式;
CB作用:
如果一级描述符的后两位为10,那么就是进行的段转换,一级转换描述符的[31:20]存储的就是段转换后的物段理基地址,然后加上MVA的段内地址(位[19:0])就可以得到真实的物理地址。
如果一级描述符的后两位配置为11,那么就是进行细页转换,一级转换描述符的[31:12]以及MVA的[19:10]被用来确定二级页表的位置。对应上图的Fine table page。
3.4 二级转换描述符
二级页表包含1024项,每一项的长度为32位;
- 如果二级描述符后两位为01,通过二级描述符的位[31:16]和MVA的位[15:0]可以确定物理地址;
- 如果二级描述符后两位为10,通过二级描述符的位[31:12]和MVA的位[11:0]可以确定物理地址;
- 如果二级描述符后两位为11,通过二级描述符的位[31:10]和MVA的位[9:0]可以确定物理地址;
3.5 转换过程
通过上面的分析,我们大概已经明白了如何将MVA转换成PA的过程,下面我们展示一个具体的案例,给定一个MVA=0x503200,它将通过MMU的两级分页机制转换成物理地址:
3.6 为什么有多级页表
我们试想一下,如果只有一级页表会怎样?正常来说,一个32位逻辑地址的分页系统,假设页面大小都是4KB,即$2^{12}B$,在操作系统中每个进程都有自己的页表,每个进程页表中的页表项最多可以达到1M个之多,并且一项占用4个字节,也就是4MB的空间,而且还要求是连续的,很显然并不现实。因此现实中的操作系统通常采用以下两个方法解决这一问题:
- 采用离散分配方式来解决难以找到一块连续的大空间的问题;也就是常说的采用两级页表或者多级页表;
- 只将当前需要的部分页表项调入内存,其余的页表项仍然驻留在磁盘,需要时再调用;
这里稍微介绍一下两级页表:
对于要求连续的内存空间来存放页表的问题,可利用将页表进行分页,并离散的将各个页面分别放在不同的物理块中的办法来加以解决,也就是上图中的Coarse page table、或者Fine page table。
四、开启/禁用MMU
下面的代码主要涉及到C15控制寄存器C1,所以这里给出该寄存器的具体位描述:
4.1 开启MMU
如果要使用MMU,需要执行如下步骤:
- 在内存中创建页表,然后将页表的起始地址写入TTB寄存器;
- 开启MMU;
4.2 创建页表
建立页表,这里使用段式映射方式,需要将段映射的描述符写入到相应的内存中。
#define MMU_SECDESC_AP (3<<10) #define MMU_SECDESC_DOMAIN (0<<5) #define MMU_SECDESC_NCNB (0<<2) #define MMU_SECDESC_WB (3<<2) #define MMU_SECDESC_TYPE ((1<<4) | (1<<1)) #define MMU_SECDESC_FOR_IO (MMU_SECDESC_AP | MMU_SECDESC_DOMAIN | MMU_SECDESC_NCNB | MMU_SECDESC_TYPE) #define MMU_SECDESC_FOR_MEM (MMU_SECDESC_AP | MMU_SECDESC_DOMAIN | MMU_SECDESC_WB | MMU_SECDESC_TYPE) #define IO 1 #define MEM 0 void create_secdesc(unsigned int *ttb, unsigned int va, unsigned int pa, int io) { int index; index = va / 0x100000; if (io) ttb[index] = (pa & 0xfff00000) | MMU_SECDESC_FOR_IO; else ttb[index] = (pa & 0xfff00000) | MMU_SECDESC_FOR_MEM; } /* * VA PA CB * 0 0 00 * 0x40000000 0x40000000 11 * * 64M sdram: * 0x30000000 0x30000000 11 * ...... * 0x33f00000 0x33f00000 11 * * register: 0x48000000~0x5B00001C * 0x48000000 0x48000000 00 * ....... * 0x5B000000 0x5B000000 00 * * Framebuffer : 0x33c00000 * 0x33c00000 0x33c00000 00 * * link address: * 0xB0000000 0x30000000 11 * */ void create_page_table(void) { /*1.页表在哪里?0x3200 0000(16KB)只需要指向一块没有用到的地址就可以*/ /*ttb:translation table base*/ unsigned int *ttb = (unsigned int *)0x32000000; unsigned int va, pa; int index; /*2.根据va,pa设置页表条目*/ /*2.1 for sram/nor flash*/ /*nand启动0地址是sram的0地址,nor启动0地址是nor flash的0地址*/ create_secdesc(ttb, 0, 0, IO); /*这里用NCNB模式的原因是之后会通过写数据至0地址然后读出来,从而判定是nor还是nand启动*/ /*如果使用cache和buffer,写数据以及读数据都会写/读到cache,这样就会导致读出的内容就是写进去的内容*/ /*然后cpu就一直认为是nand启动,从而出错*/ /* 2.2 for sram when nor boot */ create_secdesc(ttb, 0x40000000, 0x40000000, MEM); /* 2.3 for 64M sdram */ va = 0x30000000; pa = 0x30000000; for (; va < 0x34000000;) { create_secdesc(ttb, va, pa, MEM); va += 0x100000; pa += 0x100000; } /* 2.4 for register: 0x48000000~0x5B00001C */ va = 0x48000000; pa = 0x48000000; for (; va <= 0x5B000000;) { create_secdesc(ttb, va, pa, IO); va += 0x100000; pa += 0x100000; } /* 2.5 for Framebuffer : 0x33c00000 */ create_secdesc(ttb, 0x33c00000, 0x33c00000, IO); /* 2.6 for link address */ create_secdesc(ttb, 0xB0000000, 0x30000000, MEM); }
由于页表是以1M对齐,所以register 地址需要写成0x5B000000。
4.2 设置TTB
/*把页表基地址告诉cp15*/ ldr r0, =0x32000000 mcr p15, 0, r0, c2, c0, 0 /*将r0的值写进协处理器cp15中的c2(c0和后面的0用区分是哪一个c2,可能有多个c2)*/ /*设置域为0xffffffff,不进行权限检查*/ ldr r0, =0xffffffff mcr p15, 0, r0, c3, c0, 0
4.3 使能I cache、D cache、mmu
/*使能icache,dcache,mmu*/ mrc p15, 0, r0, c1, c0, 0 /* read */ orr r0, r0, #(1<<12) /* enable icache */ orr r0, r0, #(1<<2) /* enable dcache */ orr r0, r0, #(1<<0) /* enable mmu */ mcr p15, 0, r0, c1, c0, 0 /* wite */
4.4 编程注意事项
- 关闭MMU前,清空I/D cache、即将脏数据写回主存上;
- 如果代码有变,使无效I cache、这样CPU取指令时会从新读取内存;
- 开启I/D cache时,要考虑I/D cache中内容与主存保持一致;
- 对于I/O地址空间,不使用cache和write buffer;
- 使用DMA操作可以被cache的内存时,将内存的数据发送时,要清空cache,将内存的数据读入时,使无效cache;
参考文章:
[3]操作系统——页式存储管理
[4]协处理器CP15 - 常见的五大ARM存储器之一:协处理器CP15
[5]ARM920T_TRM1_S.pdf
[7]嵌入式Linux应用开发完全手册