linux 内存管理(9) -页表实现

  • 了解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
发布了161 篇原创文章 · 获赞 15 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_41028621/article/details/104465525