- 了解linux 页表实现
1.MMU/TLB/Cache概述
-
MMU:完成的工作就是虚拟地址到物理地址的转换,可以让系统中的多个程序跑在自己独立的虚拟地址空间中,相互不会影响。程序可以对底层的物理内存一无所知,物理地址可以是不连续的,但是不妨碍映射连续的虚拟地址空间。
-
TLB:MMU工作的过程就是查询页表的过程,页表放置在内存中时查询开销太大,因此专门有一小片访问更快的区域用于存放地址转换条目,用于提高查找效率。当页表内容有变化的时候,需要清除TLB,以防止地址映射出错。
-
Cache:处理器和存储器之间的缓存机制,用于提高访问速率,在ARMv8上会存在多级Cache,其中L1 Cache分为指令Cache和数据Cache,在CPU Core的内部,支持虚拟地址寻址;L2 Cache容量更大,同时存储指令和数据,为多个CPU Core共用,这多个CPU Core也就组成了一个Cluster。
具体访问流程:
2.虚拟地址到物理地址的转换
虚拟地址到物理地址的映射通过查表的机制来实现,ARMv8中,Kernel Space的页表基地址存放在TTBR1_EL1寄存器中,User Space页表基地址存放在TTBR0_EL0寄存器中,其中内核地址空间的高位为全1,(0xFFFF0000_00000000 ~ 0xFFFFFFFF_FFFFFFFF),用户地址空间的高位为全0,(0x00000000_00000000 ~ 0x0000FFFF_FFFFFFFF)。
ARMv8中:
- 虚拟地址支持
64位虚拟地址中,并不是所有位都用上,除了高16位用于区分内核空间和用户空间外,有效位的配置可以是:36, 39, 42, 47。这可决定Linux内核中地址空间的大小。比如使用的内核中有效位配置为CONFIG_ARM64_VA_BITS=39,用户空间地址范围:0x00000000_00000000 ~ 0x0000007f_ffffffff,大小为512G,内核空间地址范围:0xffffff80_00000000 ~ 0xffffffff_ffffffff,大小为512G。 - 页面大小支持
支持3种页面大小:4KB, 16KB, 64KB。 - 页表支持
支持至少两级页表,至多四级页表,Level 0 ~ Level 3。
结合有效虚拟地址位, 页面大小,页表的级数,可以组合成不同的页表映射方式。例如使用内核配置为:39位有效位,4KB大小页面,3级页表,手册描述了整个translation的过程:
- 虚拟地址[63:39]用于区分内核空间与用户空间,从而选择不同的TTBRn寄存器来获取Level 1页表基地址;
- 虚拟地址[38:30]放置Level 1页表中的索引,从而找到对应的描述符地址并获取描述符内容,根据描述符中的内容获取Level 2页表基地址;
- 虚拟地址[29:21]Level 2页表中的索引,从而找到对应的描述符地址并获取描述符内容,根据描述符中的内容获取Level 3页表基地址;
- 虚拟地址[20:12]Level 3页表中的索引,从而找到对应的描述符地址并获取描述符内容,根据描述符中的内容获取物理地址的高36位,以4K地址对齐;
- 虚拟地址[11:0]放置的是物理地址的偏移,结合获取的物理地址高位,最终得到物理地址。
3.Linux页表映射
3.1.ARM处理器查询页表
32bit的Linux采用三级映射:PGD–>PMD–>PTE,64bit的Linux采用四级映射:PGD–>PUD–>PMD–>PTE,多了个PUD。
缩写:
PGD:Page Global Directory、PUD:Page Upper Directory、PMD:Page Middle Directory、PTE:Page Table Entry。
在ARM32 Linux采用两层映射,省略了PMD,除非定义 CONFIG_ARM_LPAE才会使用3级映射。
Linux虚拟内存三级管理由以下三级组成:
- PGD: Page Global Directory (页目录)
- PMD: Page Middle Directory (页目录)
- PTE: Page Table Entry (页表项)
每一级有以下三个关键描述宏:
- SHIFT
- SIZE
- MASK
如页的对应描述为:
/* PAGE_SHIFT determines the page size asm/page.h */
#define PAGE_SHIFT 12
#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK (~(PAGE_SIZE-1))
数据结构定义如下:
/* asm/page.h */
typedef unsigned long pteval_t;
typedef pteval_t pte_t;
typedef unsigned long pmd_t;
typedef unsigned long pgd_t[2];
typedef unsigned long pgprot_t;
#define pte_val(x) (x)
#define pmd_val(x) (x)
#define pgd_val(x) ((x)[0])
#define pgprot_val(x) (x)
#define __pte(x) (x)
#define __pmd(x) (x)
#define __pgprot(x) (x)
3.1.1. Page Directory (PGD and PMD)
每个进程有它自己的PGD( Page Global Directory),它是一个物理页,并包含一个pgd_t数组。其定义见<asm/page.h>。 进程的pgd_t数据见 task_struct -> mm_struct -> pgd_t * pgd;
ARM架构的PGD和PMD的定义如下<arch/arm/include/asm/pgtable.h>:
#define PTRS_PER_PTE 512 // PTE中可包含的指针<u32>数 (21-12=9bit)
#define PTRS_PER_PMD 1
#define PTRS_PER_PGD 2048 // PGD中可包含的指针<u32>数 (32-21=11bit)</p><p>#define PTE_HWTABLE_PTRS (PTRS_PER_PTE)
#define PTE_HWTABLE_OFF (PTE_HWTABLE_PTRS * sizeof(pte_t))
#define PTE_HWTABLE_SIZE (PTRS_PER_PTE * sizeof(u32))
/*
* PMD_SHIFT determines the size of the area a second-level page table can map
* PGDIR_SHIFT determines what a third-level page table entry can map
*/
#define PMD_SHIFT 21
#define PGDIR_SHIFT 21
虚拟地址SHIFT宏图:
虚拟地址MASK和SIZE宏图:
3.1.2 Page Table Entry
PTEs, PMDs和PGDs分别由pte_t, pmd_t 和pgd_t来描述。为了存储保护位,pgprot_t被定义,它拥有相关的flags并经常被存储在page table entry低位(lower bits),其具体的存储方式依赖于CPU架构。
每个pte_t指向一个物理页的地址,并且所有的地址都是页对齐的。因此在32位地址中有PAGE_SHIFT(12)位是空闲的,它可以为PTE的状态位。
PTE的保护和状态位如下图所示:
3.1.3. 如何通过3级页表访问物理内存
为了通过PGD、PMD和PTE访问物理内存,其相关宏在asm/pgtable.h中定义。
• pgd_offset
根据当前虚拟地址和当前进程的mm_struct获取pgd项的宏定义如下:
/* to find an entry in a page-table-directory */
#define pgd_index(addr) ((addr) >> PGDIR_SHIFT) //获得在pgd表中的索引
#define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr)) //获得pmd表的起始地址
/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
• pmd_offset
根据通过pgd_offset获取的pgd 项和虚拟地址,获取相关的pmd项(即pte表的起始地址)
/* Find an entry in the second-level page table.. */
#define pmd_offset(dir, addr) ((pmd_t *)(dir)) //即为pgd项的值
• pte_offset
根据通过pmd_offset获取的pmd项和虚拟地址,获取相关的pte项(即物理页的起始地址)
#ifndef CONFIG_HIGHPTE
#define __pte_map(pmd) pmd_page_vaddr(*(pmd))
#define __pte_unmap(pte) do { } while (0)
#else
#define __pte_map(pmd) (pte_t *)kmap_atomic(pmd_page(*(pmd)))
#define __pte_unmap(pte) kunmap_atomic(pte)
#endif
#define pte_index(addr) (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
#define pte_offset_kernel(pmd,addr) (pmd_page_vaddr(*(pmd)) + pte_index(addr))
#define pte_offset_map(pmd,addr) (__pte_map(pmd) + pte_index(addr))
#define pte_unmap(pte) __pte_unmap(pte)
#define pte_pfn(pte) (pte_val(pte) >> PAGE_SHIFT)
#define pfn_pte(pfn,prot) __pte(__pfn_to_phys(pfn) | pgprot_val(prot))
#define pte_page(pte) pfn_to_page(pte_pfn(pte))
#define mk_pte(page,prot) pfn_pte(page_to_pfn(page), prot)
#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)
#define pte_clear(mm,addr,ptep) set_pte_ext(ptep, __pte(0), 0)
3.2.ARMV8 页表映射
- 39位有效位,4KB大小页面,3级页表
3.3.代码路径:
- arch/arm64/include/asm/pgtable-types.h:定义pgd_t, pud_t, pmd_t, pte_t等类型;
- arch/arm64/include/asm/pgtable-prot.h:针对页表中entry中的权限内容设置;
- arch/arm64/include/asm/pgtable-hwdef.h:主要包括虚拟地址中PGD/PMD/PUD等的划分,这个与虚拟地址的有效位及分页大小有关,此外还包括硬件页表的定义, TCR寄存器中的设置等;
- arch/arm64/include/asm/pgtable.h:页表设置相关;
Note:
当CONFIG_PGTABLE_LEVELS=4时:pgd–>pud–>pmd–>pte;
当CONFIG_PGTABLE_LEVELS=3时,没有PUD页表:pgd(pud)–>pmd–>pte;
当CONFIG_PGTABLE_LEVELS=2时,没有PUD和PMD页表:pgd(pud, pmd)–>pte
以39位为例宏定义:
3.4.创建启动页表
在汇编代码阶段的head.S文件中,负责创建映射关系的函数是create_page_tables。create_page_tables函数负责identity mapping和kernel image mapping。
- identity map:是指把idmap_text区域的物理地址映射到相等的虚拟地址上,这种映射完成后,其虚拟地址等于物理地址。idmap_text区域都是一些打开MMU相关的代码。
- kernel image map:将kernel运行需要的地址(kernel txt、rodata、data、bss等等)进行映射。
3.4.1 idmap_pg_dir和swapper_pg_dir临时页表
内核启动过程中,在真正的物理内存尚未添加进系统,以及页表还未初始化之前,为了保证系统能正常运行,需要建立两个临时全局页表:idmap_pg_dir和swapper_pg_dir。
__create_page_tables函数:
__create_page_tables:
mov x28, lr
/*
* Invalidate the idmap and swapper page tables to avoid potential
* dirty cache lines being evicted.
*/
adrp x0, idmap_pg_dir
ldr x1, =(IDMAP_DIR_SIZE + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE)
bl __inval_dcache_area
/*
* Clear the idmap and swapper page tables.
*/
adrp x0, idmap_pg_dir
ldr x1, =(IDMAP_DIR_SIZE + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE)
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
其中两个全局页表的定义在arch/arm64/kernel/vmlinux.lds.S中,放置在BSS段之后:
. = ALIGN(PAGE_SIZE);
idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
swapper_pg_dir = .;
. += SWAPPER_DIR_SIZE;
/* 定义了连续的几个页,分别存放PGD,PMD,PTE等,连续在一起,这个也是head.S中填充的 */
#define SWAPPER_DIR_SIZE (SWAPPER_PGTABLE_LEVELS * PAGE_SIZE)
#define IDMAP_DIR_SIZE (IDMAP_PGTABLE_LEVELS * PAGE_SIZE)
-
idmap_pg_dir
identify map,也就是物理地址和虚拟地址是相等的。为什么需要这么一个映射呢?我们都知道在MMU打开之前,CPU访问的都是物理地址,那么当MMU打开后访问的就是虚拟地址了,这段页表的映射就是从CPU到打开MMU之前的这段代码物理地址的映射,防止开启MMU后,无法获取页表。可以从System.map文件中查看这些代码:
-
swapper_pg_dir
Linux内核编译后,kernel image是需要进行映射的,包括text,data等各种段。
汇编结束后的内存映射关系如下图所示:
当执行完上面的map之后,MMU就已经打开了并且开始进入C代码运行阶段,那么下一步就要对dtb进行映射了。参考linux 内存管理(12) - 物理内存初始化
refer to
- http://www.wowotech.net/memory_management/436.html
- https://www.cnblogs.com/pengdonglin137/p/9157639.html
- http://www.wowotech.net/armv8a_arch/create_page_tables.html