简介
页表的主要作用是完成虚拟地址到物理地址的转换,更详细的介绍可以参考这个优秀的博客,很好地介绍了页表的理论。Linux如何实现这个页表理论呢?以及如何进行寻址呢?本文将会结合代码,从代码出发,基于ARM64的架构,分析Linux从源码上如何实现页表理论。
从一个页的地址说起
对于ARM64的架构,一个虚拟地址的大小是64bit。但是实际上并不是全部64bit都是用来寻址的,其中一部分bit会基于架构的不同有一样的作用,但是一个最基本的应用是区分当前地址是用户态和内核态的地址。内核可以通过宏
假设目前有一个页,它的64bit虚拟地址是
访问这个页的第一个字节,地址是
0xffff018140e09000 访问这个页的第二个字节,地址是
0xffff018140e09001 访问这个页的第二个字节,地址是
0xffff018140e09002 …
他们只是尾部的数据有点不同,其他的位置没有变化。但是为什么会这样呢?
问题一: 这个虚拟地址隐藏了什么信息?
基于4级页表,可以知道页表共有4层映射关系,即
由于
由此我们可以知道,内存里的每一个字节,是通过
如上图,虚拟地址的地址线部分由
1111111111111111000000011000000101000000111000001001000000000000
其中
以此类推,对于虚拟地址
问题二: 虚拟地址是如何跟物理地址对应起来?
从问题一的论述,我们知道虚拟地址是由一定逻辑组织起来,然后作为索引寻址到物理内存上的某个字节。但是实际上的物理内存是怎么分布的呢?
内核页表和进程页表
前面提及的虚拟地址的
进程页表例子: 当一个用户态的进程通过
内核页表例子: 当进程写一个文件的时候,它会通过系统调用(如sys_write)进入内核态,此时就会使用内核页表进行寻址。
进程页表在内核对应的索引是
1 | pgd_t swapper_pg_dir[PTRS_PER_PGD]; // 一般PTRS_PER_PGD = 512 |
PGD、PUD、PMD、PTE的初始化
用于虚拟地址寻址的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | void __init paging_init(void) { phys_addr_t pgd_phys = early_pgtable_alloc(); // 分配一个物理页构建新的PGD映射表 pgd_t *pgdp = pgd_set_fixmap(pgd_phys); // pgd_phys是物理地址,因此通过这个函数转换为虚拟地址,这里这要理解为虚拟地址pgdp是物理地址pgd_phys的映射 map_kernel(pgdp); // 完成内核进程地址空间的一些保留位置的映射,如.text,.data、.bss段等映射,同时完成映射的同时也会创建对应的pgd、pud、pmd、pte map_mem(pgdp); // 完成pgd、pud、pmd、pte的映射 cpu_replace_ttbr1(__va(pgd_phys)); // 切换成当前页表为临时页表 memcpy(swapper_pg_dir, pgdp, PGD_SIZE); // 将新页表对赋予给swapper_pg_dir全局页表 cpu_replace_ttbr1(lm_alias(swapper_pg_dir)); // 再切回swapper_pg_dir页表,完成更新操作 pgd_clear_fixmap(); memblock_free(pgd_phys, PAGE_SIZE); // 新的映射表更新完成,释放掉临时空间 memblock_free(__pa_symbol(swapper_pg_dir) + PAGE_SIZE, __pa_symbol(swapper_pg_end) - __pa_symbol(swapper_pg_dir) - PAGE_SIZE); } |
- 从上图以及上面代码可以知道,由于构建
PGD 映射表需要512个表项,每一个表项的大小是8字节,因此需要4096字节空间才可以构建PGD 映射表。因此early_pgtable_alloc 函数分配一个物理页(4K),用于构建临时的PGD映射表。由于页表是处于虚拟地址空间进行构建的,因此物理地址pgd_phys 需要先转化为虚拟地址即pgdp 。 - 完成内核进程地址空间的一些保留位置的映射,如.text,.data、.bss段等映射,同时完成映射的同时也会创建对应的
PGD 、PUD 、PMD 、PTE 等。 - 完成
PGD 、PUD 、PMD 、PTE 的,即创建PGD-PUD 映射,然后创建PUD-PMD ,重点分析map_mem 这个函数。 - 用于新的临时
PGD 映射表已经构建完成,已经可以称为页表了,因为临时表已经可以根据PGD 、PUD 、PMD 、PTE 找到对应的物理页。接下来就要替换旧的内核页表,因此首先调用cpu_replace_ttbr1 函数以及memcpy 函数完成页表的更新。内核页表swapper_pg_dir 中会在系统运行中一直维持着,因此当系统根据虚拟地址搜索PGD 时,首先就是访问这个表。内核页表可以通过init_mm.pgd 进行访问,即swapper_pg_dir 是所有进程共享的页表。用户进程拥有自己私有的页表,这个私有页表的初始化的。
map_mem 函数分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | static void __init map_mem(pgd_t *pgdp) { phys_addr_t kernel_start = __pa_symbol(_text); phys_addr_t kernel_end = __pa_symbol(__init_begin); struct memblock_region *reg; int flags = 0; memblock_mark_nomap(kernel_start, kernel_end - kernel_start); for_each_memblock(memory, reg) { // 遍历所有的memblock,对嵌入式设备,一般只有一个 phys_addr_t start = reg->base; // 物理内存的起始地址 phys_addr_t end = start + reg->size; // 物理内存的结束地址,reg->size表示物理内存大小 if (start >= end) break; if (memblock_is_nomap(reg)) continue; __map_memblock(pgdp, start, end, PAGE_KERNEL, flags); } __map_memblock(pgdp, kernel_start, kernel_end, PAGE_KERNEL, NO_CONT_MAPPINGS); memblock_clear_nomap(kernel_start, kernel_end - kernel_start); } |
遍历所有的
1 2 3 4 5 6 | static void __init __map_memblock(pgd_t *pgdp, phys_addr_t start, phys_addr_t end, pgprot_t prot, int flags) { __create_pgd_mapping(pgdp, start, __phys_to_virt(start), end - start, prot, early_pgtable_alloc, flags); } |
这个函数将物理地址转换为虚拟地址,作为另外一个参数传入到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | static void __create_pgd_mapping(pgd_t *pgdir, phys_addr_t phys, unsigned long virt, phys_addr_t size, pgprot_t prot, phys_addr_t (*pgtable_alloc)(void), int flags) { unsigned long addr, length, end, next; pgd_t *pgdp = pgd_offset_raw(pgdir, virt); // 获取addr对应的PUD对应的表项 // 下面三个计算,是为了让物理内存由原来的按字节计算位置,改为按页计算位置 phys &= PAGE_MASK; // 获得起始物理地址的页偏移,一般phys=0,那么起始页编号就是0 addr = virt & PAGE_MASK; // 获得起始虚拟地址的页偏移 length = PAGE_ALIGN(size + (virt & ~PAGE_MASK)); // 按PAGE算,内存的大小是多少(N个PAGE) end = addr + length; // 这里是按页算的虚拟地址的结束地址 // 上面步骤的目的是: 算出目前正在初始化的内存,一共包含多少个页,而且页的起始地址和结束地址是什么 do { next = pgd_addr_end(addr, end); // 找到当前PGD的结束地址,一般来说只会有一个PGD,因为一个PGD的范围很大,一个PGD=512GB,因此只会循环一次 alloc_init_pud(pgdp, addr, next, phys, prot, pgtable_alloc, flags); // 初始化该PGD的 phys += next - addr; } while (pgdp++, addr = next, addr != end); } |
再一次注意注意!!!分析这个函数之前,需要明确一点,上面的
下一步如注释所示,即获取该一段物理地址的低12位,然后获取虚拟地址的低12位,这样做的目的是让物理内存由原来的按字节计算,变为按页计算。从这里开始,物理内存的起始、物理内存的大小都是以页作为基本单位。接下来算出目前正在初始化的内存,一共包含多少个页,而且页的起始地址和结束地址是什么。
下一步进入循环,由于每一个
1 | 1 PGD = 512 PUD = 512 * 512 PMD = 512 * 512 * 512 PTE(页) = 512 * 512 * 512 * 4KB = 512GB |
因此大部分情况下(内存少于512GB),只会有一个
下一步就是在该
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | static void alloc_init_pud(pgd_t *pgdp, unsigned long addr, unsigned long end, phys_addr_t phys, pgprot_t prot, phys_addr_t (*pgtable_alloc)(void), int flags) { unsigned long next; pud_t *pudp; pgd_t pgd = READ_ONCE(*pgdp); // 获得了PUD映射表的头地址 if (pgd_none(pgd)) { // 如果该pgd下的表项还没有分配,那么就一次性分配一个物理页,创建512个entry phys_addr_t pud_phys; BUG_ON(!pgtable_alloc); pud_phys = pgtable_alloc(); __pgd_populate(pgdp, pud_phys, PUD_TYPE_TABLE); // 然后与PUD关联起来 pgd = READ_ONCE(*pgdp); } pudp = pud_set_fixmap_offset(pgdp, addr); // 基于addr计算出当前addr属于PUD表的第几个表项 do { pud_t old_pud = READ_ONCE(*pudp); next = pud_addr_end(addr, end); // PUD起始和结束位置,大小是1GB alloc_init_cont_pmd(pudp, addr, next, phys, prot, pgtable_alloc, flags); phys += next - addr; } while (pudp++, addr = next, addr != end); pud_clear_fixmap(); } |
这里传入参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | static void alloc_init_cont_pmd(pud_t *pudp, unsigned long addr, unsigned long end, phys_addr_t phys, pgprot_t prot, phys_addr_t (*pgtable_alloc)(void), int flags) { unsigned long next; pud_t pud = READ_ONCE(*pudp); // 获得了PMD映射表的头地址 if (pud_none(pud)) { // 如果该pud表项还没有分配,那么就一次性分配一个物理页,创建512个entry phys_addr_t pmd_phys; pmd_phys = pgtable_alloc(); __pud_populate(pudp, pmd_phys, PUD_TYPE_TABLE); // 与pmd关联起来 pud = READ_ONCE(*pudp); } do { pgprot_t __prot = prot; next = pmd_cont_addr_end(addr, end); // 计算一个PMD的起始和结束位置,大小是2MB init_pmd(pudp, addr, next, phys, __prot, pgtable_alloc, flags); phys += next - addr; } while (addr = next, addr != end); } |
同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | static void init_pmd(pud_t *pudp, unsigned long addr, unsigned long end, phys_addr_t phys, pgprot_t prot, phys_addr_t (*pgtable_alloc)(void), int flags) { unsigned long next; pmd_t *pmdp; pmdp = pmd_set_fixmap_offset(pudp, addr); // 获取addr对应的PMD对应的表项 do { pmd_t old_pmd = READ_ONCE(*pmdp); // 便利PMD的表项,即遍历不同的PTE表 next = pmd_addr_end(addr, end); // 计算一个PMD的起始和结束位置,一般是4KB(一个页) alloc_init_cont_pte(pmdp, addr, next, phys, prot, pgtable_alloc, flags); phys += next - addr; } while (pmdp++, addr = next, addr != end); pmd_clear_fixmap(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | static void alloc_init_cont_pte(pmd_t *pmdp, unsigned long addr, unsigned long end, phys_addr_t phys, pgprot_t prot, phys_addr_t (*pgtable_alloc)(void), int flags) { unsigned long next; pmd_t pmd = READ_ONCE(*pmdp); // 获得了PTE映射表的头地址 if (pmd_none(pmd)) { // 同理,创建对应的entry phys_addr_t pte_phys; BUG_ON(!pgtable_alloc); pte_phys = pgtable_alloc(); __pmd_populate(pmdp, pte_phys, PMD_TYPE_TABLE); pmd = READ_ONCE(*pmdp); } do { pgprot_t __prot = prot; next = pte_cont_addr_end(addr, end); init_pte(pmdp, addr, next, phys, __prot); // 初始化每一个PTE的表项记录的值(物理页页帧) phys += next - addr; } while (addr = next, addr != end); } |
依然是一个循环,循环当前的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | static void init_pte(pmd_t *pmdp, unsigned long addr, unsigned long end, phys_addr_t phys, pgprot_t prot) { pte_t *ptep; ptep = pte_set_fixmap_offset(pmdp, addr); // 根据addr找到对应的PTE Entry的位置 do { pte_t old_pte = READ_ONCE(*ptep); // 读这个entry的值,一般来说新建的entry是没有valid的值 set_pte(ptep, pfn_pte(__phys_to_pfn(phys), prot)); // 将物理地址转换为页帧,然后写入PTE phys += PAGE_SIZE; } while (ptep++, addr += PAGE_SIZE, addr != end); pte_clear_fixmap(); } static inline void set_pte(pte_t *ptep, pte_t pte) { WRITE_ONCE(*ptep, pte); if (pte_valid_not_user(pte)) dsb(ishst); } |
首先通过
将物理地址转换为物理页帧号
最后通过
这里以虚拟地址
1111111111111111000000000000000101000000111000001001000000000000
它索引方式,如下图所。
- 根据
[47:39]bit 得到PGD=0 的值,然后在PGD 表(swapper_pg_dir )找到对应的表项(蓝色部分),表项记录的数据是下一级PUD 表的头地址。 - 根据
[38:30]bit 得到了PUD=5 的值,以及上一步获得的PUD 表的头地址,可以获取到访问到PUD 表对应的表项(绿色部分),表项记录的数据是下一级PMD 表的头地址。 - 根据
[29:21]bit 得到了PMD=7 的值,以及上一步获得的PMD 表的头地址,可以获取到访问到PMD 表对应的表项(棕黄部分),表项记录的数据是下一级PTE 表的头地址。 - 根据
[20:12]bit 得到了PTE=11 的值,以及上一步获得的PTE 表的头地址,可以获取到访问到PTE 表对应的表项(红色部分),PTE 表项记录的数据是物理页的地址,以及保护信息。 - 获得了
PTE 的Entry信息后,首先通过位操作,分别得到该物理页的保护信息,以及物理地址信息。如果保护信息允许访问,那么根据物理地址信息访问物理内存,然后返回数据。