背景
说明:
1. 介绍要想理解好Linux的页表映射,MMU的机制是需要去熟悉的,因此将这两个模块放到一起介绍。关于ARMv8 MMU的相关内容,主要参考文档: 《ARM Cortex-A Series Programmer’s Guide for ARMv8-A》 。2. ARMv8 MMU2.1 MMU/TLB/Cache概述
下图浅黄色部分描述的就是一个地址转换的过程。 由于上图没有体现出 L1和L2 Cache 和MMU 的关系,所以再来一张图吧:那具体是怎么访问的呢?再来一张图: 2.2 虚拟地址到物理地址的转换虚拟地址到物理地址的映射通过查表的机制来实现,ARMv8中, Kernel Space 的页表基地址存放在TTBR1_EL1 寄存器中,User Space 页表基地址存放在TTBR0_EL0 寄存器中,其中内核地址空间的高位为全1,(0xFFFF0000_00000000 ~ 0xFFFFFFFF_FFFFFFFF) ,用户地址空间的高位为全0,(0x00000000_00000000 ~ 0x0000FFFF_FFFFFFFF) ARMv8中:
结合有效虚拟地址位, 页面大小,页表的级数,可以组合成不同的页表映射方式。我使用的内核配置为:39位有效位,4KB大小页面,3级页表,所以我会以这个组合来介绍。在ARMv8的手册中刚好找到了下图,描述了整个translation的过程,简直完美:
讲到这里还没有完,是时候看一下 Table Descriptor 了,也就是页表中存放的内容,有以下四种类型:类型有低两位来决定,其中Level 0 中的Table Descriptor 只能输出Level 1 页表的地址,Level 3 中的Table Descriptor 只能输出block addresses 。看到图中的 attributes 了吗,这些可以用于memory的权限控制,memory ordering,cache policy的操作等。在ARMv8中,与页表相关的寄存器有: TCR_EL1, TTBRx_EL1 .3. Linux页表映射3.1 Linux页表基本操作看过《深入理解Linux内核》的同学应该很熟悉下边这张图片,Linux的分页模式(图中以X86为例,页表基地址由CR3寄存器指定): 在Linux内核中支持4级页表的模型,同时适用于32位和64位系统。 那么ARMv8与Linux内核是怎么结合的呢?以我实际使用的设置(39位有效位,4KB大小页面,3级页表)为例,如下图所示: 基本上内核中关于页表的操作都会围绕着上图进行操作,似乎脱离了代码有点不太合适,那么就来一波fucking source code解析吧,主要讲讲各类page table相关的API。
在这些代码中可以看到,
常用的宏定义:
/*描述各级页表中的页表项*/ typedef struct { pteval_t pte; } pte_t; typedef struct { pmdval_t pmd; } pmd_t; typedef struct { pudval_t pud; } pud_t; typedef struct { pgdval_t pgd; } pgd_t;
/* 将页表项类型转换成无符号类型 */ #define pte_val(x) ((x).pte) #define pmd_val(x) ((x).pmd) #define pud_val(x) ((x).pud) #define pgd_val(x) ((x).pgd)
/* 将无符号类型转换成页表项类型 */ #define __pte(x) ((pte_t) { (x) } ) #define __pmd(x) ((pmd_t) { (x) } ) #define __pud(x) ((pud_t) { (x) } ) #define __pgd(x) ((pgd_t) { (x) } )
/* 获取页表项的索引值 */ #define pgd_index(addr) (((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1)) #define pud_index(addr) (((addr) >> PUD_SHIFT) & (PTRS_PER_PUD - 1)) #define pmd_index(addr) (((addr) >> PMD_SHIFT) & (PTRS_PER_PMD - 1)) #define pte_index(addr) (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
/* 获取页表中entry的偏移值 */ #define pgd_offset(mm, addr) (pgd_offset_raw((mm)->pgd, (addr))) #define pgd_offset_k(addr) pgd_offset(&init_mm, addr) #define pud_offset_phys(dir, addr) (pgd_page_paddr(*(dir)) + pud_index(addr) * sizeof(pud_t)) #define pud_offset(dir, addr) ((pud_t *)__va(pud_offset_phys((dir), (addr)))) #define pmd_offset_phys(dir, addr) (pud_page_paddr(*(dir)) + pmd_index(addr) * sizeof(pmd_t)) #define pmd_offset(dir, addr) ((pmd_t *)__va(pmd_offset_phys((dir), (addr)))) #define pte_offset_phys(dir,addr) (pmd_page_paddr(READ_ONCE(*(dir))) + pte_index(addr) * sizeof(pte_t)) #define pte_offset_kernel(dir,addr) ((pte_t *)__va(pte_offset_phys((dir), (addr)))) 3.2 head.S中的页表映射3.2.1 idmap_pg_dir和swapper_pg_dir临时页表是时候来个实例分析了,看看页表的创建过程,代码路径: arch/arm64/kernel/head.S 。内核启动过程中,在真正的物理内存尚未添加进系统,以及页表还未初始化之前,为了保证系统能正常运行,需要建立两个临时全局页表: idmap_pg_dir 和swapper_pg_dir :其中两个全局页表的定义在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)
3.2.2 页表创建在 head.S 中,创建页表相关的有三个宏:
/* * Macro to populate the PGD (and possibily PUD) for the corresponding * block entry in the next level (tbl) for the given virtual address. * * Preserves: tbl, next, virt * Corrupts: tmp1, tmp2 */ .macro create_pgd_entry, tbl, virt, tmp1, tmp2 create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2 #if SWAPPER_PGTABLE_LEVELS > 3 create_table_entry \tbl, \virt, PUD_SHIFT, PTRS_PER_PUD, \tmp1, \tmp2 #endif #if SWAPPER_PGTABLE_LEVELS > 2 create_table_entry \tbl, \virt, SWAPPER_TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2 #endif .endm 上述函数主要是调用 create_table_entry ,由于SWAPPER_PGTABLES 配置为3,因此相当于创建了pgd和pmd 两级页表,此处需要注意一点,create_table_entry 函数执行后,tbl 参数会自动加上PAGE_SIZE ,也就是说pgd和pmd 两级页表是物理连续的。
/* * Macro to populate block entries in the page table for the start..end * virtual range (inclusive). * * Preserves: tbl, flags * Corrupts: phys, start, end, pstate */ .macro create_block_map, tbl, flags, phys, start, end lsr \phys, \phys, #SWAPPER_BLOCK_SHIFT lsr \start, \start, #SWAPPER_BLOCK_SHIFT and \start, \start, #PTRS_PER_PTE - 1 // table index orr \phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT // table entry lsr \end, \end, #SWAPPER_BLOCK_SHIFT and \end, \end, #PTRS_PER_PTE - 1 // table end index 9999: str \phys, [\tbl, \start, lsl #3] // store the entry add \start, \start, #1 // next entry add \phys, \phys, #SWAPPER_BLOCK_SIZE // next block cmp \start, \end b.ls 9999b .endm 上述函数主要是往 block 中填充pte entry ,真正创建虚拟地址到物理地址的映射,映射区域:start ~ end 。
/* * Macro to create a table entry to the next page. * * tbl: page table address * virt: virtual address * shift: #imm page table shift * ptrs: #imm pointers per table page * * Preserves: virt * Corrupts: tmp1, tmp2 * Returns: tbl -> next level table page address */ .macro create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2 lsr \tmp1, \virt, #\shift and \tmp1, \tmp1, #\ptrs - 1 // table index add \tmp2, \tbl, #PAGE_SIZE orr \tmp2, \tmp2, #PMD_TYPE_TABLE // address of next table and entry type str \tmp2, [\tbl, \tmp1, lsl #3] add \tbl, \tbl, #PAGE_SIZE // next level table page .endm 上述函数创建页表项,并且返回下一个Level的页表地址。 上述三个孤立的函数并不直观,所以,图来了: 总体来说,页表的创建过程相对来说还是比较易懂的,掌握好几级页表及各级页表index所占的位域,此外熟悉各个Level页表中entry的格式,理解起来就会顺畅很多了。 一抠细节深似海,点到为止,防止一叶障目不见泰山,收工! ---------------------------------------------------------------------------------------------------------------------- 我们尊重原创,也注重分享,文章来源于微信公众号:LoyenWang,建议关注公众号查看原文。如若侵权请联系qter@qter.org。 ---------------------------------------------------------------------------------------------------------------------- |