页表的主要作用是完成虚拟地址到物理地址的转换,更详细的介绍可以参考这个优秀的博客,很好地介绍了页表的理论。Linux如何实现这个页表理论呢?以及如何进行寻址呢?本文将会结合代码,从代码出发,基于ARM64的架构,分析Linux从源码上如何实现页表理论。
从一个页的地址说起
对于ARM64的架构,一个虚拟地址的大小是64bit。但是实际上并不是全部64bit都是用来寻址的,其中一部分bit会基于架构的不同有一样的作用,但是一个最基本的应用是区分当前地址是用户态和内核态的地址。内核可以通过宏CONFIG_ARM64_VA_BITS将用来寻址的bit的大小配置为36,39,42,47,48,52bit。不同的数目的寻址bit可以组成不同level的页表,可以参考上面推荐的博客,这里不做介绍。我们基于48bit的地址线大小结合4级页表,分析ARM64架构下的页是如何进行映射的。
假设目前有一个页,它的64bit虚拟地址是0xffff018140e09000。由于RAM是字节寻址,因此我们可以以字节的形式进行访问,所以一个例子是:
访问这个页的第一个字节,地址是0xffff018140e09000
访问这个页的第二个字节,地址是0xffff018140e09001
访问这个页的第二个字节,地址是0xffff018140e09002
…
他们只是尾部的数据有点不同,其他的位置没有变化。但是为什么会这样呢?
问题一: 这个虚拟地址隐藏了什么信息?
基于4级页表,可以知道页表共有4层映射关系,即PGD、PUD、PMD、PTE。页表首先会根据虚拟地址找到了PGD,再从PGD里面找PUD,再从PUD里面找PMD,再从PMD里面找PTE,最后PTE表的表项记录的是页的物理地址。PGD、PUD、PMD、PTE表的大小都是512,因此使用9bit来表示(1 << 9 = 512)。每一个表含有一个8字节的表项,用于下一级表的索引信息。
由于PTE表的表项记录的是页的物理地址,因此我们可以根据PTE获得一个页。同时由于内存是字节寻址,我们还需要对页内的每一个字节进行寻址,因此Linux对页内的每一个字节的位置,使用Offset来表示。如Offset=0表示页内第一个字节,Offset=1表示页内第二个字节。由于一个页的大小是4096字节,需要12bit来表示(1 << 12 = 4096)页内每一个字节的位置。
由此我们可以知道,内存里的每一个字节,是通过PGD、PUD、PMD、PTE以及Offset进行索引的。它的总体结构如下:

如上图,虚拟地址的地址线部分由PGD、PUD、PMD、PTE、Offset,它门一起作为一个索引值,最终索引到内存的某一个字节处。以虚拟地址0xffff018140e09000为例,它的二进制值是:
1111111111111111000000011000000101000000111000001001000000000000
其中
[63:48]bit值是1111111111111111,不用来直接寻址,一般是用来区分地址是用户态还是内核态,如全1表示内核态,全0表示用户态。
[47:39]bit值是000000011,十进制值是3,因此PGD=3。
[38:30]bit值是000000101,十进制值是5,因此PUD=5。
[29:21]bit值是000000111,十进制值是7,因此PMD=7。
[20:12]bit值是000001001,十进制是11,因此PTE=11。
[11:0]bit值是000000000000,十进制是0,因此Offset=0,表示页内的第一个字节
以此类推,对于虚拟地址0xffff018140e09001,以及0xffff018140e09002,它们只是在[11:0]bit处,即Offset处不同,十进制值分别是1和2,分别表示页内的第二个字节以及第三个字节。
问题二: 虚拟地址是如何跟物理地址对应起来?
从问题一的论述,我们知道虚拟地址是由一定逻辑组织起来,然后作为索引寻址到物理内存上的某个字节。但是实际上的物理内存是怎么分布的呢? PGD、PUD、PMD、PTE等寻址表,又是怎么样进行索引呢?在探讨这个问题之前先介绍一下内核页表和进程页表。
内核页表和进程页表
前面提及的虚拟地址的[63:48]bit用于区分当前地址是用户态的虚拟地址,还是内核态的虚拟地址。例如使用malloc函数分配的地址就是用户态的虚拟地址,而kmalloc函数分配的地址就是内核态的虚拟地址。Linux有两种类型的页表,分别是进程页表和内核页表。进程页表是进程私有的页表,内核页表是所有进程共享的页表。内核页表在系统初始化的时候就会创建,而进程页表则会在用户态进程创建的时候将内核页表复制给当前的进程页表。当进程在用户态运行时,它使用的是进程页表。当进程在内核态运行时,它使用的是内核页表。
进程页表例子: 当一个用户态的进程通过malloc分配了一个大小N个页内存区域A,然后通过指针不断访问内存空间A,此时由于程序在用户态运行,因此它使用的是进程页表进行寻址。
内核页表例子: 当进程写一个文件的时候,它会通过系统调用(如sys_write)进入内核态,此时就会使用内核页表进行寻址。
进程页表在内核对应的索引是task_struct.mm.pgd而内核页表在内核的索引是init_mm.pgd中。内核页表的索引,最终会找到内核页表对应的结构,全局变量swapper_pg_dir,它是一个数组(如下定义),记录了各个PGD在物理内存位置,因此其实这个结构就是上图提及的PGD映射表。
| 1 |
pgd_t swapper_pg_dir[PTRS_PER_PGD]; // 一般PTRS_PER_PGD = 512 |
PGD、PUD、PMD、PTE的初始化
用于虚拟地址寻址的PGD、PUD、PMD、PTE等寻址表在系统建立虚拟地址和物理地址之间的映射关系时建立。其初始化的代码在arch/arm64/mm/mmu.c的paging_init函数,请参考注释:
paging_init函数分析:
| 1 |
void __init paging_init(void) |

本文详细介绍了ARM64架构中,Linux内核如何通过4级页表将虚拟地址转换为物理地址。内容包括页表的结构、寻址原理以及内核页表和进程页表的区别。通过实例解析了虚拟地址的各个部分如何映射到页表的各个层级,并阐述了页表的初始化过程,特别是`paging_init`函数和`map_mem`函数中的关键步骤。此外,还探讨了物理内存的分布和页表项的创建,如`alloc_init_pud`、`alloc_init_cont_pmd`及`init_pte`等函数的作用。
最低0.47元/天 解锁文章
987

被折叠的 条评论
为什么被折叠?



