ChinOS
MMU
MMU在操作系统中起到了至关重要的作用,通过MMU我们可以实现地址空间的隔离,把操作系统分为用户态和内核态,使用不同的地址空间来隔离用户和内核,同时在用户态下的多进程也是通过地址空间的隔离来实现进程彼此之间互不干扰的。
相比传统的RTOS,RTOS基本上运行于MCU上,只有MPU没有MMU,无法做到地址空间的隔离,所有地址都是真实物理地址,用户所能用到的地址空间也受限于物理内存。
AArch64-MMU
虚拟地址空间
在64位地址空间中地址位有64位,如果全部用完64bit,那可以映射2^64 = 16777216TB大小的物理内存,现实中不可能有这么大的物理内存,也没有这么大物理内存的需求,而且在现有的处理机架构中也不支持64bit的完全寻址。在实际情况中,最大的虚拟地址位宽位52bit,当然更加常用的处理器总线位宽一般为48bit/39bit,39bit最大能容量512G的内存,48bit最大能容量256T的内存。
Linux虚拟地址空间布局
回顾一下32bit Linux下的虚拟地址空间布局,地址范围为(0x0000 0000 — 0xFFFF FFFF):
- Linux用户空间3G(0x0000 0000 — 0x7FFFF FFFF)
- Linux内核空间1G(0x8000 0000 — 0xFFFF FFFF)
由于32bit总线限制,最大只能访问4G物理内存,而且在用户态下可使用的内存区域也只有3G而已
VA = 48bits
在64bit Linux下的VMA布局,使用48bit总线宽度,用户空间和内核空间都有最大256TB的区域,但是通过高16bit
来区分是用户还是内核,通常情况下我们会使用最高位(bit:63)来表示OS的内存布局,高位为1代表内核区域,高位为0代表用户区域
ARM的TTBRx_ELx,有如下描述:当虚拟地址最高位为0时使用TTBR0_ELx,最高位位1时使用TTBR1_ELx,
对于一个虚拟地址高16位[63:48]必须全是0或1,否则会触发故障
VA = 39bits
在64bit Linux下的VMA布局,使用39bit总线宽度,用户空间和内核空间都有最大512G的区域,但是通过高25bit
来区分是用户还是内核,通常情况下我们会使用最高位(bit:63)来表示OS的内存布局,高位为1代表内核区域,高位为0代表用户区域
ARM的TTBRx_ELx,有如下描述:当虚拟地址最高位为0时使用TTBR0_ELx,最高位位1时使用TTBR1_ELx,
对于一个虚拟地址高25位[63:39]必须全是0或1,否则会触发故障
配置虚拟地址空间
通过配置TCR.TxSZ来限制不同区域(TTBRx_EL1)的虚拟地址空间的大小(2^(64-TxSZ)bytes),最大为52bit,在Linux中虚拟地址空间为48bit那么就需要把TxSZ设置为16(48bit = 64 - TxSZ),如果位39位则需要将TxSZ设置为25(39bit = 64 - TxSZ)
- TCR.T0SZ
- 用户虚拟地址空间大小
- TCR.T1SZ
- 内核虚拟地址空间大小
这个TxSZ可以根据你自己的OS来具体设计布局,不一定为固定的16
物理地址空间的有效位
具体每一个Core的物理地址的位数,在设计的时候是固定的,是不能修改的,具体支持的物理地址位数可以通过手册查找
物理地址范围
AAch64下可通过ID_AA64MMFR0_EL1.PARange来查找Core支持的物理地址范围
TCR.EL1.IPS
配置MMU输出的物理地址的位数,这个不能超过Core支持的物理地址范围(ID_AA64MMFR0_EL1.PARange)
颗粒度
AArch64支持4/16/64KB不同颗粒度,不同颗粒度会影响到转换表的级数和大小,它们定义了最低级别转换表的块大小,并控制正在使用的转换表的大小。更大的颗粒大小减少了所需页表级数,这在使用hypervisor提供虚拟化的系统中可能成为一个重要的考虑因素。
颗粒度支持
处理器支持的粒度是自定义的并由 ID_AA64MMFR0_EL1.TGran4/16/64保存,所有 Arm Cortex-A 处理器都支持 4KB 和 64KB
页表描述符格式(Descriptor Format)
可以在表的所有级(从第0级到第2级)中使用描述符格式。
第0级描述符只能输出第1级表的地址,所以第0级描述符只能是Table entry,剩下的第1,2级既可以是Table也可以是Block,在4页页表的映射中,存在level3【PTE】,PTE的type设置为table
在4KB颗粒度,4级页表下,level 0/1/2/都为Table,level 3为table
Table descriptors
Table描述符表示此描述符指向下一级描述符
在配置Table描述符时,一般无需配置attributes
Block descriptors
Block描述符,表示此描述符直接指向一段内存基地址,包含的地址就是真实的物理基地址,再加上offset就可以得到真正的物理地址
在配置Block/Page描述符时,涉及到具体Page的属性,需要对nG/AF/SH/AP/AttIndx进行配置:
- PBHA, bits[62:59] :for FEAT_HPDS2
- XN or UXN, bit[54] : Execute-never or Unprivileged execute-never
- PXN, bit[53] :Privileged execute-never
- Contiguous, bit[52] : translation table entry 是连续的,可以存在一个TLB Entry中
- DBM, bit[51] :Dirty Bit Modifier
- GP, bit[50] :for FEAT_BTI
- nT, bit[16] :for FEAT_BBM
- nG, bit[11] :缓存在TLB中的翻译是否使用ASID标识
- AF, bit[10] : Access flag, AF=0后,第一次访问该页面时,会将该标志置为1. 即暗示第一次访问
- SH, bits[9:8] :shareable属性
- AP[2:1], bits[7:6] :Data Access Permissions bits,
- NS, bit[5] :Non-secure bit
- AttrIndx[2:0], bits[4:2] : Memory attributes index field, for the MAIR_ELx,
AttrIndx
AttrIndex只是一个索引,AArch64下有8组Attr,用户可以自己定义和选择组合,并通过MAIR_ELx来配置
着重关注0b0000dd00 和 0booooiiii
0b0000dd00(设备内存)
设备内存的属性
0booooiiii(普通内存)
Linux预定义的6种内存属性
AttrIndx | bits[8n+7:8n] | values | MAIR | meaning |
---|---|---|---|---|
0 | [7:0] | nGnRnE | 00000000 | 设备内存 |
1 | [15:8] | nGnRE | 00000100 | |
2 | [23:16] | GRE | 0x00001100 | |
3 | [31:24] | NORMAL_NC | 0x01000100 | |
4 | [39:32] | NORMAL | 0x11111111 | |
5 | [47:40] | NORMAL_WT | 0x10111011 | |
6 | [55:48] | |||
7 | [63:56] |
两种常见的页表映射
Page = 4K,vabits = 48,4级映射
Page = 4K,vabits = 39,3级映射
在Linux中我们用PGD/PUD/PMD/PTE来描述每一级页表
Level 0(PGD)
一共有(2^9 = 512)个entry,每一个entry包含512G的内存空间
Level 1(PUD)
一共有(2^9 = 512)个entry,每一个entry包含1G的内存空间
Level 2(PMD)
一共有(2^9 = 512)个entry,每一个entry包含2M的内存空间
Level 3(PTE)
一共有(2^9 = 512)个entry,每一个entry包含4K的内存空间
当vabits = 39时,只有3级映射,此时没有PUD页表,PGD页表中,一个entry包含1G内存空间,PMD和PTE不变
/* PGD */
/* 每个ENTRY包含512G内存区域 */
typedef struct page_table
{
unsigned long entry[PTRS_PER_PGD];
} aligned_data(PAGE_SIZE) pgtable_pgd_t;
#if CONFIG_PGTABLE_LEVELS > 3
/* PUD */
/* 每个ENTRY包含1G内存区域 */
typedef struct pgtable_pud
{
unsigned long entry[PTRS_PER_PUD];
} aligned_data(PAGE_SIZE) pgtable_pud_t;
#endif
#if CONFIG_PGTABLE_LEVELS > 2
/* PMD */
/* 每个ENTRY包含2M内存区域 */
typedef struct pgtable_pmd
{
unsigned long entry[PTRS_PER_PMD];
} aligned_data(PAGE_SIZE) pgtable_pmd_t;
#endif
/* PTE */
/* 每个ENTRY包含4K内存区域 */
typedef struct pgtable_pte
{
unsigned long entry[PTRS_PER_PTE];
} aligned_data(PAGE_SIZE) pgtable_pte_t;
AArch64-MMU寻址过程
我们来模拟一下开启mmu后,AArch64是如何从虚拟地址找到物理地址的
当CPU接收到虚拟地址(0xFFFF_0000_1000_0000)MMU如何将他转换为真正的物理地址
采用4K,VABITS=48,4级映射
第一次寻址(0xFFFF_0000_1000_1000)
CPU识别到地址高16位为1,那么认为这是内核的地址空间,MMU会去找TTBR1_ELx,没有hypervisor时,访问TTBR1_EL1,TTBR1_EL1中保存了一级页表(PGD)的地址:pgd
#define VA_MASK ((1UL << 48) - 1) // VA_MASK = 0x0000_ffff_ffff_ffff
#define INDEX_MASK ((1UL << 9) - 1) // INDEX_MASK = 0x1ff
#define PAGE_MASK ((1UL << 12) - 1) // PAGE_MASK = 0xfff
unsigned long pgd_addressing(unsigned long *pgd, unsigned long va)
{
va &= VA_MASK; // va = 0x0000_0000_1000_1000
// 获取L0索引
unsigned int L0_index = (va >> 39) & INDEX_MASK; //L0_index = 0x0000_0000_1000_1000 >> 39 & 0x1ff = 0
// 获取PGD页表描述符
unsigned long pgd_desc = pgd[L0_index];
// 查看Table Descriptor Type位
unsigned char desc_type = pgd_desc & 0x3;
if (desc_type == 3) {
// Type位为3代表,此描述符保存了下一级页表的地址,也就是PUD的地址,PUD的地址必须4K对齐
unsigned long *pud = (unsigned long *)(pgd_desc & (~ 0x3));
return pud_addressing(pud, va);
} else if(desc_type == 1) {
// 一级页表type必须为3
abort();
} else {
abort();
}
}
第二次寻址(0xFFFF_0000_1000_1000)
static unsigned long pud_addressing(unsigned long *pud, unsigned long va)
{
unsigned long pa;
// 获取L1索引
unsigned int L1_index = (va >> 30) & INDEX_MASK; //L1_index = 0x0000_0000_1000_1000 >> 30 & 0x1ff = 0
// 获取PUD页表描述符
unsigned long pud_desc = pud[L1_index ];
// 查看Table Descriptor Type位
unsigned char desc_type = pud_desc & 0x3;
if (desc_type == 3) {
// Type位为3代表,此描述符保存了下一级页表的地址,也就是PMD的地址,PMD的地址必须4K对齐
unsigned long *pmd = (unsigned long *)(pud_desc & (~ 0x3));
return pmd_addressing(pmd, va);
} else if(desc_type == 1) {
// Type位为1代表,此描述符保存的物理地址,PA地址必须1G对齐
pa = pud_desc & (~ PAGE_MASK);
unsigned long offset = va & ((1UL << 30) - 1);
pa += offset;
} else {
abort();
}
return pa;
}
第三次寻址(0xFFFF_0000_1000_1000)
static unsigned long pmd_addressing(unsigned long *pmd, unsigned long va)
{
unsigned long pa;
// 获取L2索引
unsigned int L2_index = (va >> 21) & INDEX_MASK; //L2_index = 0x0000_0000_1000_1000 >> 21 & 0x1ff = 0x80 & 0x1ff = 128
// 获取PMD页表描述符
unsigned long pmd_desc = pmd[L2_index];
// 查看Table Descriptor Type位
unsigned char desc_type = pmd_desc & 0x3;
if (desc_type == 3) {
// Type位为3代表,此描述符保存了下一级页表的地址,也就是PTE的地址,PTE的地址必须4K对齐
unsigned long *pte = (unsigned long *)(pmd_desc & (~ 0x3));
return pte_addressing(pte, va);
} else if(desc_type == 1) {
// Type位为1代表,此描述符保存的物理地址,PA地址必须2M对齐
pa = pmd_desc & (~ PAGE_MASK);
unsigned long offset = va & ((1UL << 21) - 1);
pa += offset;
} else {
abort();
}
return pa;
}
第四次寻址(0xFFFF_0000_1000_1000)
static unsigned long pte_addressing(unsigned long *pte, unsigned long va)
{
unsigned long pa;
// 获取L3索引
unsigned int L3_index = (va >> 12) & INDEX_MASK; //L3_index = 0x0000_0000_1000_1000 >> 12 & 0x1ff = 1
// 获取PMD页表描述符
unsigned long pte_desc = pte[L3_index];
// 查看Table Descriptor Type位
unsigned char desc_type = pte_desc & 0x3;
if (desc_type == 3) {
// Type位为3代表,此描述符保存的物理地址,PA地址必须4K对齐
// PTE的Type位虽然为3,但并不代表还指向下一级,PTE已经是最后一级,描述的就是物理地址,当我map时把Type设置为1会出现错误,Type=1只有在PUD和PMD阶段起作用
pa = pte_desc & (~ PAGE_MASK);
unsigned long offset = va & ((1UL << 12) - 1);
pa += offset;
abort();
} else {
abort();
}
return pa;
}