接着上文:MMU如何通过虚拟地址找到物理地址?
5,虚拟内存到物理内存的推导 本文只介绍最普遍的64位地址,四级页表,每个页表4k的这种情况。
linux内核将一个进程的内存映射表建立好之后,在该进程被调度运行的时候,会将PGD的物理地址放置到MMU的页表基地址寄存器中,在X86_64架构下,该寄存器为CR3,ARM64架构下,该寄存器为ttbr0_el1和ttbr1_el1,接下来的寻址过程中,就不需要linux来干预了,MMU会通过PGD-PUD-PMD-PTE-PAGE-OFFSET这个过程,根据虚拟地址,找到其对应的物理地址。那么这个过程是怎样的呢?
1)我们首先来拆分一下虚拟地址,以“filemap-addr:0x7fc3d3e4c000为例”,0x7fc3d3e4c000被分为5个部分,其中 0-11bit为页内偏移地址,根据页基地址+偏移量找到对应的物理内存; 12-20bit为PTE的索引,该索引可以找到物理内存页面的基地址; 21-29bit为PMD的索引,该索引可以找到PTE的页基地址; 30-38bit为PUG的索引,该索引可以找到PMD的页基地址; 39-47bit为PGD的索引,该索引可以找到PUD的页基地址。首先我们使用命令bc来得到0x7fc3d3e4c000的二进制:
我们将该地址按上述规则拆分一下:PGD索引:11111111,需要左移12bit,得到11111111000,即0x7f8 PUD索引:100001111,需要左移12bit,得到100001111000,即0x878 PMD索引:010011111,需要左移12bit,得到010011111000,0x4f8 PTE索引:001001100,需要左移12bit,得到001001100000,即0x260
注意,PGD,PUD.PMD,PTE的索引都要左移12bit,可以看出来PGD的索引7f8,pud的索引878,PMD的索引4f8,PTE的索引260,都能和vtop给的信息对应上。
我们再使用rd命令直接从内存中读取信息看一下:
从这个过程中,我们可以看到MMU有两个数据信息,即PGD的基地址47de000和虚拟地址0x7fc3d3e4c000,MMU通过PGD+0x7f8找到PUD的地址5f7c000,通过PUD+878得到PMD的地址0x5f66000,通过PMD+4f8得到PTE的地址0x5f64000,通过PTE+260得到物理页面的地址28ba000,本例没有页内偏移量,索引0x7fc3d3e4c000对应的物理地址就是28ba000。2)g-addr:0x4c82f0 转换成二进制
获取索引:
可以看出PGD和PUD的索引为0,PMD索引为0x10,PTE的索引为0x640,都能和vtop给的信息对应上。
在得到页面的基地址0x1f4ed000后,再加上该变量在页内的偏移量之后0x2f0,得到该变量的物理地址0x1f4ed2f0。使用rd命令直接从内存中读取信息看一下
最后再看一下该变量的值:
与代码中赋值相同。
3)stack-addr:0x7ffe03a8ef1c 转换成二进制
获取索引
可以看出来PGD的索引7f8,pud的索引fc0,PMD的索引0e8,PTE的索引470,都能和vtop给的信息对应上。
资料直通车:Linux内核源码技术学习路线+视频教程内核源码
学习直通车:Linuxc/c++高级开发【直播公开课】
零声白金VIP体验卡:零声白金VIP体验卡(含基础架构/高性能存储/golang/QT/音视频/Linux内核)
在得到页面的基地址0x2940000后,再加上该变量在页内的偏移量之后0xf1c,得到该变量的物理地址2940f1c。使用rd命令直接从内存中读取信息看一下:
4)heap-addr:0x11226f0 最后,我们再看一下堆内变量0x11226f0,先转换成二进制:
获取索引:
可以看出PGD和PUD的索引为0,PMD索引为0x40,PTE的索引为0x910,都能和vtop给的信息对应上。
在得到页面的基地址28b9000后,再加上该变量在页内的偏移量之后0x6f0,得到该变量的物理地址28b96f0。使用rd命令直接从内存中读取信息看一下
最后再看一下物理内存中的值:
与代码中赋值相符。6,内存映射 我们在查阅内存映射关系的资料的时候,通常会找到一个这样一个图:
这个图很清楚的表示了PGD,PUD,PMD和PTE的关系,下面我们把本例中涉及到的地址数据填充进去,效果看起来会更直观和清晰。
7,总结
总结起来,一个变量的寻址过程就是,在编译或运行时被分配虚拟地址和物理内存,内核为该虚拟地址和物理内存的地址以该进程的PGD表为基础,建立映射关系,并将PGD的物理地址交给MMU,MMU根据映射关系通过虚拟地址找到物理地址,并按照程序的要求读写其中的内容。
1)编译和链接 在本例中有四种类型的变量: filemap-addr:0x7fc3d3e4c000 内存映射文件虚拟地址 g-addr:0x4c82f0 全局变量虚拟地址 stack-addr:0x7ffe03a8ef1c 栈内变量虚拟地址 heap-addr:0x11226f0 堆内变量虚拟地址 其中内存映射文件的虚拟地址是内核执行mmap的时候分配的,栈和堆都是在进程创建的时候分配的物理内存并指定了虚拟内存地址,栈内变量和堆内变量的虚拟地址就是堆栈的虚拟地址加上偏移量获得。全局变量的地址是在编译链接的过程中指定的,本例的全局变量没有初始化,所以放在a.out的bss区,bss区的起始虚拟地址为00000000004c82a0:
全局变量g的虚拟地址为:
2)内存页表的建立 在进程创建的时候,内核会为a.out的每个section分配物理内存,堆栈也要分配物理内存,同时根据为进程创建物理内存和虚拟内存的映射关系。最后,把PGD的物理地址放到MMU的页表基地址寄存器中,剩下的事儿交给MMU。可以理解为从虚拟内存到物理内存的转换的逆过程。
3)MMU进行地址转换 从虚拟内存到物理内存的转换是mmu通过页表映射来实现的,无需操作系统干预,本文所讲的寻址过程是最基础的虚拟地址到物理地址的转换过程,但MMU还会利用tlb来优化地址转换的效率,这个不在本文讨论的范围内。
8,其他 本文只介绍了最简单的4级页表,4k页面的映射关系和寻址过程,其实,内核的内存管理还有更复杂的映射,比如使用5级页表,或者每个映射单位为2M或1G的内存块,这些映射方法的索引级数和各级索引所占的bit位都有所不同。
原文作者:Jeff Labs