ARM体系MMU =========== MMU全称为Memory Management Unit,即内存管理单元。在带有MMU的嵌入式linux中,CPU访问的地址都是虚拟地址,而MMU负责将程序中代码或者数据的虚拟地址翻译为物理地址。在 执行期间,MMU会自动转换CPU发出的虚拟地址,无法人工进行操作,只需要配置好MMU相关属性即可 虚拟地址是在编译和链接时定义的,可以简单的理解为有链接器和链接器脚本指定虚拟地址。除了翻译虚拟地址,MMU还可以配置内存区域的各项配置,如内存区域的访问权限,内存区域是否 使能cache等功能 MMU基本概念 ------------ 页 ^^^^ MMU管理虚拟地址空间时,是按照页为单位来进行管理,在ARMv7的MMU,页大小共有16M(super section)、1M(section)、64k(large page)、4k(page)。页大小可以通过协处理器CP15进行配置, 越小的页意味着内存的颗粒度越小,内存使用时的浪费也会越小,但也意味着使用的TLB行越多。 页帧 ^^^^^ 因为虚拟地址空间需要有对应的物理地址,这样才能在虚拟地址中存储数据,所以MMU管理物理地址空间时,按照页帧为单位进行管理,其大小分为64k或者4k,一段虚拟地址空间有可能存在多个 页,这些页对应着多个页帧.页和页帧时不同地址空间下关于内存空间大小的概念 页表及页表项 ^^^^^^^^^^^^^ MMU在进行地址空间转换时,需要一些信息,存放这些信息的表就是页表。每个页表的最小单位就是页表项。页表存储在物理地址空间中,且一个页表项对应着一个页。在切换页表时,通过将页表的物理 首地址设置到协处理器CP15中的TTBR寄存器(Translation Table Base Register), 此后MMU会通过该地址自动去物理地址空间中找到对应的页表,从而完成虚拟地址到物理地址的映射。 在不考虑TLB和多级页表的情况下,可以简单的如下图所示 .. image:: res/tlb_s.webp TLB ^^^^^ TLB全称为Translation lookaside buffer, 即旁路转换缓冲,它是MMU的cache,用于临时存放虚拟地址到物理地址映射所需要的信息。TLB访问步骤如下 1. CPU访问虚拟地址到MMU 2. MMU根据规则查看虚拟地址是否在TLB中 3. 如果在TLB中,则称为TLB命中,从TLB中直接获取物理地址对内存进行访问 4. 如果不在TLB中,则称为TLB失效,此时MMU将进行translation table walking,即通过访问页表来获取物理地址,并将该虚拟地址的信息存入TLB,以便下次使用 TLB由许多TLB行组成,如下图所示 .. image:: res/tlb_struct.webp TLB行由3个部分组成,分别为标签,ASID和描述符 标签: 该部分由虚拟地址的一部分bit组成,MMU通过将虚拟地址的一部分bit和TLB的所有标签对比进行搜索 ASID: 全称为Address Space ID,一般用于多进程系统 描述符: 由2个部分组成,分为物理地址(一部分bit)和内存区域属性组成。可以理解为cache中的数据 一般情况下,切换进程时会切换页表,因为随着进程的切换,虚拟地址到物理的映射已经改变,此时需要清理TLB来保持TLB一致性,清理TLB一般通过协处理器CP15来完成, 在linux内核中,有flush_tlb_all()和flush_tlb_range()函数来完成该工作 - MMU组成 .. image:: res/mmu_struct.webp MMU工作过程 ------------- ARMv7下的MMU具有2级页表,分为1级页表和2级页表。 一级页表 ^^^^^^^^^^ 1级页表也称主页表和段页表,下面简称L1页表,它将4GB的地址空间划分为4096个1MB大小的段,每个段的地址为32bit,所以1级页表拥有4096个32bit的页表项,支持4种内存大小, 16M/1M的称之为段,64k/4k称之为页 .. note:: 存放在TTBR寄存器的地址需要16k对齐 一级页表项一共有4种格式,如下图所示 .. image:: res/L1_format.webp 每种格式都由物理地址部分+属性部分组成,各种格式的含义如下 1. 1Mb段转换页表项(section),映射到1MB的物理地址范围,其物理地址部分即为所需要映射的物理基地址 2. 物理地址部分指向2级页表的物理基地址 3. 16MB段转换页表项,是一种特殊的1MB段转换页表项,其物理地址部分即为所需要映射的物理基地址 4. 无效页表项,当访问该页表项时,将触发指令取指异常或者取数据异常 - 一级页表转换 以1MB段举例,假设L1页表的物理地址为0x12300000,现有一个虚拟地址0x00100000,其转换过程如图所示 .. image:: res/L1_trans.webp .. image:: res/L1_trans1.webp 1. 查表过程:将虚拟地址高12bit,即0x001乘以4得到0x004, 0x004即为该虚拟地址所在段的页表项在页表中的偏移,所以该虚拟地址对应的页表项的物理地址为0x12300000+0x004=0x12300004 2. 根据查到的页表项,将页表项高12bit和虚拟地址底20bit结合即为该虚拟地址在该1Mb段内的物理地址 完整的转换过程如下 .. image:: res/L1_process.webp 二级页表 ^^^^^^^^^ 2级页表一共有256个4字节大小的页表项,总共1kb大小的内存空间,L2页表的大部分内容与L1页表类似 - 二级页表项 二级页表项一共有3种格式,如下图所示 .. image:: res/L2_struct.webp 1. 粗页表项: 其物理地址部分指向64kb大小的物理基地址 2. 细页表项: 其物理地址指向4kb大小的物理基地址 3. 无效页表项,当访问该页表项时,将触发取指或者取数据异常 - 二级页表转换 L2页表的转换过程与L1页表的转换过程类似,以4kb为例,如下图所示 .. image:: res/L2_translation.webp 转换步骤如下 1. 通过虚拟地址找出L1页表项并转换为L2页表的基地址 2. 根据L2页表基地址并集合虚拟地址的[19:12]bit找出虚拟地址对应的L2页表项 3. 将虚拟地址[11:0]bit和L2页表项的物理地址部分结合得出具体的物理地址 完整转换过程如下 .. image:: res/L2_process.webp MMU内存属性 ------------ 内存区域权限 ^^^^^^^^^^^^ 每个内存区域都有自己的权限,不符合权限的访问都会引发异常,如果是数据访问则引发数据异常,如果是指令访问,且该指令再执行期间都没有被flush,将 引发预取指异常,引发异常的原因将会被设置在CP15的the fault address and fault status registers 内存区域权限由AP、APX和Domain共同控制 操作系统如何使用页表 ----------------------- 进程与MMU ^^^^^^^^^^^ 操作系统会为每个进程分配一个页表,该页表使用物理地址存储。当进程使用类似malloc等需要映射代码或数据的操作时,操作系统会在随后马上修改页表以加入新的物理内存。 当进程完成退出时,内核会将相关的页表项删除,以便分配给新的进程。 .. note:: 当使能MMU之后,CPU直接寻址虚拟地址,而MMU负责虚拟地址到物理地址的转换和翻译工作,地址转换和翻译的依据是页表。页表项的内容是由操作系统负责填充的。如果下一级 页表的基地址是虚拟地址,那么MMU还需要查询另外一个页表才能找到这个虚拟地址对应的物理地址,这样MMU就会陷入死循环,所以这里下一级页表的基地址采用的是物理地址。 关于页表的遍历,MMU会遍历页表,linux内核也会遍历页表,如通过 ``walk_pgd`` 、 ``__create_pgd_mapping`` 、 ``follow_page`` 等函数。通过MMU遍历页表比较容易理解, MMU从页表基地址寄存器得到了PGD页表(L0页表)基地址的物理地址,然后从虚拟地址中得到每级页表的索引值,从而找到对应的页表项,页表项中存储了下一级页表的物理基地址, 以此类推,很容易遍历整个页表。但是站在软件的视角,linux内核的pgd_t, pud_t, pmd_t, pte_t数据结构中并没有存储指向下一级页表的指针。 :: //arch/arm64/include/asm/pgtable-types.h typedef u64 pteval_t; typedef u64 pmdval_t; typedef u64 pudval_t; typedef u64 pgdval_t; typedef struct { pteval_t pte; } pte_t; #define pte_val(x) ((x).pte) #define __pte(x) ((pte_t) { (x) } ) #if CONFIG_PGTABLE_LEVELS > 2 typedef struct { pmdval_t pmd; } pmd_t; #define pmd_val(x) ((x).pmd) #define __pmd(x) ((pmd_t) { (x) } ) #endif #if CONFIG_PGTABLE_LEVELS > 3 typedef struct { pudval_t pud; } pud_t; #define pud_val(x) ((x).pud) #define __pud(x) ((pud_t) { (x) } ) #endif typedef struct { pgdval_t pgd; } pgd_t; #define pgd_val(x) ((x).pgd) #define __pgd(x) ((pgd_t) { (x) } ) ``walk_pagetable`` 函数是软件遍历页表的例子,它的作用是遍历进程的页表查找虚拟地址对应页表中的PTE :: static pte_t *walk_pagetable(struct mm_struct *mm, unsigned long address) { pgd_t *pgdp = NULL; pud_t *pudp; pmd_t *pmdp; pte_t *ptep; pgdp = pgd_offset(mm, address); if(!pgdp || pgd_none(*pgdp)) return NULL; pudp = pud_offset(pgdp, address); if(!pudp || pud_none(*pudp)) return NULL; pmdp = pmd_offset(pudp, address); if(!pmdp || pmd_none(*pmdp)) return NULL; if((pmd_val(*pmdp) & PMD_TYPE_MASK) == PMD_TYPE_SECT) return (pte_t *)pmdp; ptep = pte_offset_kernel(pmdp, address); if(!ptep || pte_none(*ptep)) return NULL; return ptep; } 进程的内存描述符mm_struct中PGD成员存储了该进程的PGD页表基地址的虚拟地址,因此通过pgd_offset很方便可以找到PGD页表项的虚拟地址 :: #define pgd_offset(mm, addr) (pgd_offset_raw((mm)->pgd, (addr))) #define pgd_offset_raw(pgd, addr) (pgd + pgd_index(addr)) #define pgd_index(addr) (((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1)) #define PTRS_PER_PGD (1 << (MAX_USER_VA_BITS - PGDIR_SHIFT)) #define MAX_USER_VA_BITS VA_BITS #define VA_BITS (CONFIG_ARM64_VA_BITS) #define PGDIR_SHIFT ARM64_HW_PGTABLE_LEVEL_SHIFT(4 - CONFIG_PGTABLE_LEVELS) #define ARM64_HW_PGTABLE_LEVEL_SHIFT(n) ((PAGE_SHIFT - 3) * (4 - (n)) + 3) #define PAGE_SHIFT CONFIG_ARM64_PAGE_SHIFT // CONFIG_ARM64_PAGE_SHIFT在.config中定义,一般为CONFIG_ARM64_PAGE_SHIFT=12 // CONFIG_PGTABLE_LEVELS在.config中定义,一般为CONFIG_PGTABLE_LEVELS=4 // CONFIG_ARM64_VA_BITS在.config中定义,一般为CONFIG_ARM64_VA_BITS=48 .. note:: 比较难理解是Linux内核如何查找到下一级页表基地址的虚拟地址,因为pgd_t数据结构中并没有存储下一个指针来指向下一级页表的虚拟地址 在linux内核中,物理内存会线性映射到内核空间中,偏移量为PAGE_OFFSET,在内核空间中可以很方便的实现虚拟地址到物理地址映射的转换。linux提供了两个宏,其中 ``__pa`` 用于 根据内核中线性映射的虚拟地址计算对应的物理地址。而 ``__va`` 宏用于根据内核线性映射中物理地址计算对应的虚拟地址 :: #define pud_offset(dir, addr) ((pud_t *)__va(pud_offset_phys((dir), (addr)))) #define pud_offset_phys(dir, addr) (pgd_page_paddr(READ_ONCE(*(dir))) + pud_index(addr) * sizeof(pud_t)) static inline phys_addr_t pgd_page_paddr(pgd_t pgd) { return __pgd_to_phys(pgd); } #define __pgd_to_phys(pgd) __pte_to_phys(pgd_pte(pgd)) #define __pte_to_phys(pte) (pte_val(pte) & PTE_ADDR_MASK) //arch/arm64/include/asm/memory.h #define __pa(x) __virt_to_phys((unsigned long) (x)) #define __va(x) ((void *)__phys_to_virt((phys_addr_t) (x))) #define __phys_to_virt(x) ((unsigned long)((x) - PHYS_OFFSET) | PAGE_OFFSET) 在PGD页表项中存储了指向下一级页表基地址的物理地址,因此通过__va宏,可以快速把物理地址转换成内核空间的虚拟地址。从而找到下一级页表基地址的虚拟地址