页表项和页表的关系_Linux中的页表实现

e629d7dfc04cd910eb806dcc7effd6b2.png

页表是软件实现的,但是页表的查找是MMU完成的,所以硬件定义了页表的实现规则,软件可以做的只有选择页表的级数,是否使用huge page以及填充对应的权限标志位。前面的文章主要介绍了页表的实现规则,本文将讨论Linux系统中页表的具体实现。

相关数据结构

还是那个三级页表,但增加了很多内容。

4b3220457b4e3faa9411f2cee966b110.png

页表PGD的首地址是放在前面介绍到的mm_struct中的,pgd_offset(), pmd_offset(), pmd_offset()分别用于从虚拟(线性)地址中提取查找PGD, PMD和PTE表所需的index。

Linux中用PAGE_SHIFT表示一个PTE所对应的内存范围(也就是一个page的大小)所需占用的bit数(也就是12), PMD_SHIFT和PGDIR_SHIFT分别表示一个PMD和一个PGD所对应的内存范围所需占用的bit数。

41586092d21c443d0fa1187164c85428.png

PAGE_SIZE, PMD_SIZE, PGDIR_SIZE则分别表示一个PTE, PMD, PGD所对应的内存范围的大小。

#define PAGE_SIZE     (1UL << PAGE_SHIFT)   

5bca635fddc2454aa787248a6ada2ded.png

PAGE_MASK, PMD_MASK, PGDIR_MASK则是用于提取地址位的掩码。

#define PAGE_MASK    (~(PAGE_SIZE-1))   

对于页表描述符中的标志位, Linux中基本都有对应的函数来处理。比如PTE中的R/W位,Linux用_PAGE_WRITE宏表示,pte_write()用于检测该位,判断page是否可写;pte_mkwrite()可将该位置1,设置page为可写;pte_wrprotect()可将该位清0,设置page为不可写。

和硬件的配合

不同的处理器支持的页表级数是不一样的,比如IA-32只支持2级,x64支持4级甚至5级,而Linux作为一个通用的操作系统,需要兼容不同的硬件平台,如果针对每种硬件平台进行不同的页表实现,那就太麻烦了。试想,如果Linux采用2级页表,对IA-32到是没问题,对x64就无能为力了,那如果Linux统一采用4级或5级页表,对x64的配合倒是刚刚好,那对IA-32呢?

其实不难,只要把表示PUD中PMD entry个数的"PTRS_PER_PMD"和表示PMD中PTE entry个数的"PTRS_PER_PMD"都设为1就可以了,相当于把中间两级的PUD和PMD折叠了起来,PGD就等同于直接指向PTE了。

a732dae93096a2b08d20dc5b6494f130.png

进程页表的建立

内核加载一个进程后,需要为该进程创建属于它的页表。pgd_alloc(), pmd_alloc()和pte_alloc()分别用于创建PGD, PMD和PTE。

因为页表本身也是放在内存中的, 其本质上是由若干物理pages组成的,分配物理内存本身就是耗时的,加之在分配过程中需要关中断,而页表的创建和销毁又是很频繁的操作,而且现在的页表多达四级甚至五级,所以整个过程的系统开销是不可小觑的。

那怎么加速这一过程呢?加速最常用的就是cache啦,具体的做法是把要提供给页表创建的物理pages放到一组叫"quicklist"的链表中,以后销毁页表释放的页就往这个quicklist里送,创建页表需要的页就直接从这个quicklist里找,其实就是一个专用的freelist(原理和slab差不多),当然要比去整个物理内存的free pages list分配要快啦。

如果建立了新的映射关系,则需要调用mk_pte()创建一个新的页表项,因为页表描述符由物理页面号和权限控制位组成,所以新建页表项的工作主要就是填充这两部分内容,组装完成就可以调用set_pte()插入到对应的页表中去了。

内核页表的建立

内核刚被加载的时候,paging还没有打开,还工作在实模式。x86要求在打开paging之前(置位CR0寄存器的PG位),软件需要至少设置一个page directory和一个page table,从Linux实现的角度,就是至少要有一个PGD和一个PTE。

试想,打开paging后,操作系统给出的地址对于CPU来说就是虚拟地址了,这时如果还没有任何的页表,怎么去获得物理地址,进而操作物理内存呢?

为此,Linux采用的做法是:假设内核初始化时需要用到的物理内存不超过8MB(0x00000000到0x007fffff),若一个page为4KB,则这8MB内存共含有2048个pages,需要2048个PTE来映射。

一个PTE占4个字节,因此一共需要8KB的page(也就是2个page)来存储,这2个用作PTE页表的page用pg0和pg1表示,分别放在物理地址为"0x00102000"和"0x00103000"的位置。

e7f1cd9222dca39d2efe235b45973457.png

而PGD则用一个叫"swapper_pg_dir"的变量表示,放在物理地址为"0x00101000"的位置,大小为1个page。因为这个临时页表的目的就是要让这8MB物理内存在实模式和打开paging的保护模式下都可以被访问,因此需要建立2个实模式下PGD entries的和2个用于保护模式的PGD entries。实模式下本来是没有什么虚拟地址的概念的,但也可以理解为是一种虚拟地址等于物理地址的特殊映射。

页表是使用虚拟地址作为index的,在32位两级页表中,高10位是PGD表的index,对于"0x00000000"到"0x007fffff"的地址,index就是0和1。对于内核空间来说,"0x00000000"到"0x007fffff"的物理地址在保护模式下对应的虚拟地址为"0xc0000000"到0xc07fffff,高10位是"1100000000b和1100000001b",换算成10进制就是768和769。

这里每个PGD entry和PTE的最后4个bit都是0111b(7),结合页表描述符的介绍可以知道,表示的是这8MB的内存是writable的,user和kernel都可以访问的,present的。

实模式下的工作完成之后,就可以将"swapper_pg_dir"的物理地址放入CR3,

movl $swapper_pg_dir-0xc0000000,%eax   
movl %eax,%cr3        /* set the page table pointer.. */    

置PG位为1,进入paging保护模式了

movl %cr0,%eax    
orl $0x80000000,%eax    
movl %eax,%cr0        /* set paging (PG) bit */

这时接力棒就叫到了paging_init()的手中,由它来完成正式的内核页表(被称为master kernel page table)的创建了。master kernel page table中PGD的首地址还是存在swapper_pg_dir变量中的。

参考:

페이지 테이블 API - ARM32

http://parrotshen.blogspot.com/2008/01/test.html

https://www.kernel.org/doc/html/latest/vm/highmem.html

原创文章,转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值