MMU 架构示意
MMU 在 enable 之后, ARM core 对虚拟地址的访问流程
- ARM Core 到 MMU 的 TLB 中查找是否有地址转换缓存,若有则返回给ARM Core;
- TLB 中没有转换缓存,则到Table walk 保存在 DDR/IRAM 的 translation table;
- 在遍历 page table 过程中:
- Translation entry 不存在,则报一个 translation fault;
- 若页表项存在,检查物理地址对应的page 是否在内存中,若在返回地址;
- 若页表项对应物理地址不在内存中,此时产生 page fault, 需要从 disk 中将page 交换到内存
- ARM Core 根据返回地址访问地址时,会根据translation table 中指定的 attribute 来确定如何使用 cache 以及 访问权限检查
- 最后访问具体地址
Translation table entry 格式
所谓translation table entry 也就是通常我们说的 page table 映射格式。它分为一级和二级。主要描述如下:
一级短描述符描述方式
对于 section, super section 映射方式,使用一级短描述符页表项即可,不需要二级描述符;
对于 page 则需要两级描述符进行描述;
各个类型映射如何区分?
First-level short descriptor 的 bit[1:0] 区分
bit[1:0] | 描述 |
0x00 | 无效 |
0x01 | Page table |
0x1 | section/super section table |
0x11 | section/super section 如果 PXN 开启 |
二级短描述符标识
二级短描述区分 large page, small page 方式: 根据 Bit[1:0] 区分。
0x01: large page
0x1x: small page
地址转换流程
arm32:
- 输入一个虚拟地址:
- TTBR0 中在高14-N ~ 31 决定 translation base 地址
- 根据虚拟地址的 bit 10:31-N 和 translation table 的地址决定这个地址在translation table 中偏移量
- 找到对应的 translation entry
内存属性
ARMV8 的 内存属性分为两大类: normal + device
- Normal
主要用于配置内存相关属性,处理器可以对 normal 属性内存 re-order, repeat 和 merge access
- device
如其名,是用于外设寄存器配置,分为:
- Device-nGnRnE: no gathering, no re-order, no early ack, 等同于 Strongly ordered 内存
- Device-nGnRE: no gathering, no re-order, early ack, 因为会有 early ack 可能会带来一些时序上问题,需要作特殊处理
- Device-nGRE:no gathering,这个限制又少了一层,增加了 re-order
- Device-GRE: 最少限制
PageTable
上图是根据 linux 虚拟地址映射的index 架构绘制的ARM64 版本,linux 使用如下几个宏来描述页表信息
pgd
ARM64 使用 低48 bit 来进行描述地址,PGDIR_SHIFT = 39,PTRS_PER_PGD = (1 << 9)
#ifndef pgd_index
/* Must be a compile-time constant, so implement it as a macro */
#define pgd_index(a) (((a) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
#endif
static inline pgd_t *pgd_offset_pgd(pgd_t *pgd, unsigned long address)
{
return (pgd + pgd_index(address));
};
#ifndef pgd_offset
#define pgd_offset(mm, address) pgd_offset_pgd((mm)->pgd, (address))
#endif
pgd_offset : mm->pgd 指向 pgd_t 的数组,它加上 index 即可所引导 va 对应 pgd entry
pgd_index: arm64 使用 va 中 bit[47:39] 这9 个bit 来描述 pgd_index
p4d
/* include/asm-generic/pgtable-nop4d.h */
static inline p4d_t *p4d_offset(pgd_t *pgd, unsigned long address)
{
return (p4d_t *)pgd;
}
pud
ARM64 使用 VA 的 bit[38:30] 这个 9 个 bit 来描述 pud_index, PTRS_PER_PUD = (1 << 9);
#define __phys_to_virt(x) ((unsigned long)((x) - PHYS_OFFSET) | PAGE_OFFSET)
#define __va(x) ((void *)__phys_to_virt((phys_addr_t)(x)))
/* include/linux/pgtable.h */
static inline unsigned long p4d_page_vaddr(p4d_t p4d)
{
return (unsigned long)__va(p4d_page_paddr(p4d));
}
#ifndef pud_index
static inline unsigned long pud_index(unsigned long address)
{
return (address >> PUD_SHIFT) & (PTRS_PER_PUD - 1);
}
#define pud_index pud_index
#endif
#ifndef pud_offset
static inline pud_t *pud_offset(p4d_t *p4d, unsigned long address)
{
return (pud_t *)p4d_page_vaddr(*p4d) + pud_index(address);
}
#define pud_offset pud_offset
#endif
根据 pud 找到指向 pmd 的数组,然后根据 pmd_index 找到索引,最终组合找到 pmd entry.
pte
ARM64 使用 va 的 bit[11:0] 来作为pte index 索引,PTRS_PER_PTE = (1 << 12);
/* include/linux/pgtable.h */
static inline unsigned long pte_index(unsigned long address)
{
return (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
}
#define pte_index pte_index
#ifndef pte_offset_kernel
static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long address)
{
return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address);
}
#define pte_offset_kernel pte_offset_kernel
#endif
#define pte_offset_map(dir, address) pte_offset_kernel((dir), (address))
#define pte_unmap(pte) ((void)(pte)) /* NOP */
初始化阶段的 MMU 映射
pagetable 定义地址
/* arch/arm64/kernel/vmlinux.lds.S */
. = ALIGN(PAGE_SIZE);
init_pg_dir = .;
. += INIT_DIR_SIZE;
init_pg_end = .;
idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
idmap_pg_end = .;
swapper_pg_dir = .;
. += PAGE_SIZE
其中 init_pg_dir 是 pgtable 地址
idmap_pg_dir 是 identity map 地址
swapper_pg_dir 是页表映射地址
创建页表
/* arch/arm64/kernel/head.S */
SYM_CODE_START(primary_entry)
bl preserve_boot_args
bl el2_setup // Drop to EL1, w0=cpu_boot_mode
adrp x23, __PHYS_OFFSET
and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0
bl set_cpu_boot_mode_flag
bl __create_page_tables
bl __cpu_setup // initialise processor
b __primary_switch
SYM_CODE_END(primary_entry)
初始化阶段在 prmary_entry 中创建 page table.
SYM_FUNC_START_LOCAL(__create_page_tables)
mov x28, lr
adrp x0, init_pg_dir
adrp x1, init_pg_end
sub x1, x1, x0
bl __inval_dcache_area
/*
* Clear the init page tables.
*/
adrp x0, init_pg_dir
adrp x1, init_pg_end
sub x1, x1, x0
1: stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
subs x1, x1, #64
b.ne 1b
mov x7, SWAPPER_MM_MMUFLAGS
/*
* Create the identity mapping.
*/
adrp x0, idmap_pg_dir
adrp x3, __idmap_text_start // __pa(__idmap_text_start)
#if (VA_BITS < 48)
#else
/*
* If VA_BITS == 48, we don't have to configure an additional
* translation level, but the top-level table has more entries.
*/
mov x4, #1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT)
str_l x4, idmap_ptrs_per_pgd, x5
#endif
1:
ldr_l x4, idmap_ptrs_per_pgd
mov x5, x3 // __pa(__idmap_text_start)
adr_l x6, __idmap_text_end // __pa(__idmap_text_end)
map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14
进行页表映射是通过 map_memory 汇编实现的。
.macro map_memory, tbl, rtbl, vstart, vend, flags, phys, pgds, istart, iend, tmp, count, sv
sub \vend, \vend, #1
add \rtbl, \tbl, #PAGE_SIZE
mov \sv, \rtbl
mov \count, #0
compute_indices \vstart, \vend, #PGDIR_SHIFT, \pgds, \istart, \iend, \count
populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
mov \tbl, \sv
mov \sv, \rtbl
#if SWAPPER_PGTABLE_LEVELS > 3
compute_indices \vstart, \vend, #PUD_SHIFT, #PTRS_PER_PUD, \istart, \iend, \count
populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
mov \tbl, \sv
mov \sv, \rtbl
#endif
#if SWAPPER_PGTABLE_LEVELS > 2
compute_indices \vstart, \vend, #SWAPPER_TABLE_SHIFT, #PTRS_PER_PMD, \istart, \iend, \count
populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
mov \tbl, \sv
#endif
compute_indices \vstart, \vend, #SWAPPER_BLOCK_SHIFT, #PTRS_PER_PTE, \istart, \iend, \count
bic \count, \phys, #SWAPPER_BLOCK_SIZE - 1
populate_entries \tbl, \count, \istart, \iend, \flags, #SWAPPER_BLOCK_SIZE, \tmp
.endm
在分析 map_memory 之前,我们分析下 compute_indices,它是用来计算 vstart 和 vend 对应的 pgtable level 的 index 的,两者之差保存在 count 中;
populate_entries 最终建立指向下一级的映射或者 last level 映射
.macro populate_entries, tbl, rtbl, index, eindex, flags, inc, tmp1
.Lpe\@: phys_to_pte \tmp1, \rtbl
orr \tmp1, \tmp1, \flags // tmp1 = table entry
str \tmp1, [\tbl, \index, lsl #3]
add \rtbl, \rtbl, \inc // rtbl = pa next level
add \index, \index, #1
cmp \index, \eindex
b.ls .Lpe\@
.endm
- 获取 rtbl 的 pte, 或上 flags保存在 tmp1
- 将 tmp1 保存在 pgtable 对应 index << 3 的位置处
- 将 rtbl 指向下一 pg level, index + PGSIZE
Cache 属性更改
对于内存的 memory, 其内存属性支持: memory-normal 和 memory-non-cached,其使用函数是:
/* arch/arm64/include/asm/pgtable.h */
#define pgprot_writecombine(prot) \
__pgprot_modify(prot, PTE_ATTRINDX_MASK, PTE_ATTRINDX(MT_NORMAL_NC) | PTE_PXN | PTE_UXN)
对于 device 类型的属性有:
/* arch/arm64/include/asm/pgtable.h */
#define pgprot_noncached(prot) \
__pgprot_modify(prot, PTE_ATTRINDX_MASK, PTE_ATTRINDX(MT_DEVICE_nGnRnE) | PTE_PXN | PTE_UXN)
#define pgprot_device(prot) \
__pgprot_modify(prot, PTE_ATTRINDX_MASK, PTE_ATTRINDX(MT_DEVICE_nGnRE) | PTE_PXN | PTE_UXN)
#define pgprot_tagged(prot) \
__pgprot_modify(prot, PTE_ATTRINDX_MASK, PTE_ATTRINDX(MT_NORMAL_TAGGED))
#define pgprot_mhp pgprot_tagged
对于 device 类型的,有几种属性:
device-nGnRnE : 这个类似 armv7 的 strongly-ordered
device-nGnRE: 支持 gathering, non-re-order, 但是会有 early-ack 功能。