虚地址到物理地址的转换

虚地址到物理地址的转换

虚拟地址的产生

在这里插入图片描述

MMU的转换

MMU的转换分为两部分,分段机制和分页机制,如果没有打开分页机制的话,默认就是分段机制,则得出的线性地址就是物理地址。如果打开了分页机制,就需要引入页表的概念

在这里插入图片描述

分页机制

分页,其实就是把虚拟地址空间划分成为若干个大小相等的页,随后由操作系统为这些虚拟内存页分配真实的物理内存页,它查找物理内存中可用的页,然后在页表中登记这些物理页地址,这样就完成了虚拟页到物理页的映射。

在这里插入图片描述

常用的4级页表模型

在这里插入图片描述

如图所示,页上级目录表和页中间目录表是被置为0,但这两个字段的的位置仍然被保留了下来。控制寄存器cr3里面存储了页全局目录表的基地址,在寻址的时候,内核会将页上级目录表和页中间目录表置为1。由页全局目录表找到页上级目录表再找到到页中间目录表,最终找到页表,通过页表项找到物理内存地址。

实验

实验代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/mm.h>
#include <linux/mm_types.h>
#include <linux/sched.h>
#include <linux/export.h>
#include <linux/delay.h>


static unsigned long cr0,cr3;

static unsigned long vaddr = 0;


static void get_pgtable_macro(void)
{
    cr0 = read_cr0();
    cr3 = read_cr3_pa();
     
    printk("cr0 = 0x%lx, cr3 = 0x%lx\n",cr0,cr3);
    
    //指示线性地址中相应字段能映射区域大小的对数,或者位数
    printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT);
    printk("P4D_SHIFT = %d\n",P4D_SHIFT);
    printk("PUD_SHIFT = %d\n", PUD_SHIFT);
    printk("PMD_SHIFT = %d\n", PMD_SHIFT);
    
    //page_shift是页面的大小的对数,4K=2^12
    printk("PAGE_SHIFT = %d\n", PAGE_SHIFT);
    
 	//指示相应的页目录表中项的个数的
    printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD);
    printk("PTRS_PER_P4D = %d\n", PTRS_PER_P4D);
    printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD);
    printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD);
    printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE);
    
    //页内偏移掩码 用来屏蔽page_offset字段
    printk("PAGE_MASK = 0x%lx\n", PAGE_MASK);
}
 
static unsigned long vaddr2paddr(unsigned long vaddr)
{
    pgd_t *pgd;
    p4d_t *p4d;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *pte;
    unsigned long paddr = 0;
    unsigned long page_addr = 0;
    unsigned long page_offset = 0;
    
    //这里申请的线性地址空间是在内核中,所以应该查内核页表,由于所有的进程都共享同一个内核页表,所以可以用当前进程的mm_struct结构来进行查找
    pgd = pgd_offset(current->mm,vaddr);
    printk("pgd_val = 0x%lx, pgd_index = %lu\n", pgd_val(*pgd),pgd_index(vaddr));
    if (pgd_none(*pgd)){
        printk("not mapped in pgd\n");
        return -1;
    }

    p4d = p4d_offset(pgd, vaddr);
    printk("p4d_val = 0x%lx, p4d_index = %lu\n", p4d_val(*p4d),p4d_index(vaddr));
    if(p4d_none(*p4d))
    { 
        printk("not mapped in p4d\n");
        return -1;
    }

    pud = pud_offset(p4d, vaddr);
    printk("pud_val = 0x%lx, pud_index = %lu\n", pud_val(*pud),pud_index(vaddr));
    if (pud_none(*pud)) {
        printk("not mapped in pud\n");
        return -1;
    }
 
    pmd = pmd_offset(pud, vaddr);
    printk("pmd_val = 0x%lx, pmd_index = %lu\n", pmd_val(*pmd),pmd_index(vaddr));
    if (pmd_none(*pmd)) {
        printk("not mapped in pmd\n");
        return -1;
    }
 
    pte = pte_offset_kernel(pmd, vaddr);
    printk("pte_val = 0x%lx, ptd_index = %lu\n", pte_val(*pte),pte_index(vaddr));

    if (pte_none(*pte)) {
        printk("not mapped in pte\n");
        return -1;
    }
    //页表对应的物理地址与页内偏移掩码进行与操作 取出高48
    page_addr = pte_val(*pte) & PAGE_MASK;
    //vaddr与(页内偏移掩码取反)与操作,取出低12位
    page_offset = vaddr & ~PAGE_MASK;
    //拼接以上的两个地址
    paddr = page_addr | page_offset;
    printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset);
    printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr);
    return paddr;
}

static int __init v2p_init(void)
{
    unsigned long vaddr = 0 ;
    printk("vaddr to paddr module is running..\n");
    get_pgtable_macro();
    printk("\n");
    //在内核空间申请地址
    vaddr = __get_free_page(GFP_KERNEL);
    if (vaddr == 0) {
        printk("__get_free_page failed..\n");
        return 0;
    }
    //将字符串写入到申请的地址中
    sprintf((char *)vaddr, "hello world from kernel");
    printk("get_page_vaddr=0x%lx\n", vaddr);
    vaddr2paddr(vaddr);
    // ssleep(600);
    return 0;
}
static void __exit v2p_exit(void)
{
    printk("vaddr to paddr module is leaving..\n");
    //释放空间
    free_page(vaddr);
}


module_init(v2p_init);
module_exit(v2p_exit);
MODULE_LICENSE("GPL"); 

Makefile

obj-m +=paging_lowmem.o
CURRENT_PATH:=$(shell pwd)
LINUX_KERNEL:=$(shell uname -r)
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
all:
	make -C/lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
	make -C/lib/modules/$(shell uname -r)/build M=$(PWD) clean 

查看了mm_struct的定义,发现在mm_struct里面就定义了pgd

unsigned long task_size;	/* size of task vm space */
		pgd_t * pgd;

查看了p4d_offset的代码,由于页四级目录没有启用,因此返回的是pgd的地址

#define p4d_offset p4d_offset
static inline p4d_t *p4d_offset(pgd_t *pgd, unsigned long address)
{
	if (pgtable_l5_enabled)
		return pgd_pgtable(*pgd) + p4d_index(address);

	return (p4d_t *)pgd;
}

查看了pte_offset_kernel代码,这个函数返回的是pte的物理地址

#ifndef pte_offset_kernel
static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long address)
{
	return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address);
}

运行结果

在这里插入图片描述

打印出了cr0和cr3寄存器的值,cr0=1000 0000 0000 0101 0000 0000 0011 0011,可见PG=1,PE=1,表示CPU开启了保护模式,并开启了分页机制。cr3=0x10789e00为页全局目录表的地址

在这里插入图片描述

在这里插入图片描述

从上图中我们可以看到PGD_SHIFT和PUD_SHIFT都是39,这也就意味着在线性地址中P4D这个字段是空的,我们也可以看到P4D的页目录项是1,这就说明虽然Linux现在使用的5级页表模型,但是实际上使用的页表只有4个。

调试

参考了学堂在线视频,下载了调试工具,参考了链接 ,运行

make							编译
sudo insmod dram.ko				加载dram模块进内核
g++ fileview.cpp -o fileview	编译fileview
sudo mknod /dev/dram c 85 0		创建设备文件
sudo ./fileview /dev/dram		显示内存中的数据,如下图

在这里插入图片描述

根据虚拟地址vaddr=0xffff9725bd125000,对应的二进制1111 1111 1111 1111 1001 0111 0010 0101 1011 1101 0001 0010 0101 0000 0000 0000

线性地址的格式如图:

在这里插入图片描述

根据虚拟地址的格式以及图1打印的这些表的索引可得:

页表二进制值偏移量十六进制的偏移量
PGD1001 0111 0=(302)10302*8970
PUD010 0101 10=(150)10150*84B0
PMD111101 000=(488)10488*8F40
PTE1 0010 0101=(293)10293*8928

同时也可知,PGD的地址 = cr3内容+PGD的偏移量 = 0x16cc36000+0x970=0x16cc36970,查看内容可得pgd_val= 0x55202067

在这里插入图片描述

PUD地址 = pgd基地址l+PUD偏移量 = 0x55202000+0x4B0 = 0x552024B0,输入即可得到pud_val=0x0164cd6063

在这里插入图片描述

PMD地址 = pud基地址+PMD偏移量 = 0x164cd6000+0xF40 = 0x164cd6F40,输入即可得到pmd_val=0x17D12E063

在这里插入图片描述

PTE地址 = pmd基地址+PTE偏移量 = 0x17D12E000+0x928 = 0x17D12E928,输入即可得到ptd_val=0x17D125163

在这里插入图片描述

最终得出物理地址为:0x17D125000,输入该物理地址可以查看之前用sprintf写入的数据

在这里插入图片描述

小结

首先CR3寄存器里面存储的是pgd的基地址,用pgd基地址+对应偏移量可以在pgd中找到下一级页面pud的基地址。以此类推分别可以找到pmd,pte和物理页框page的基地址,然后使用物理页框的基地址+页内偏移可以得到最终的物理地址,并在最终的物理地址中找到了使用sprintf写入的数据,说明这个虚拟地址到物理地址的转换是正确的。

提问

1.有了页表后为什么还会产生二级页表这样的概念?

在阅读了真相还原部分,了解到产生二级页表的大概原因有以下三点:

(1)一级页表中最多可容纳 1M(1048576)个页表项,每个页表项是 4 字节,如果页表项全满的话,

便是 4MB 大小。

(2)一级页表中所有页表项必须要提前建好,原因是操作系统要占用 4GB 虚拟地址空间的高 1GB,

用户进程要占用低 3GB。

(3)每个进程都有自己的页表,进程一多,光是页表占用的空间就很可观了

​ 总的来说是因为一级页表要一次性将表建好,而二级页表是动态建表,解决了一级页表的局限性。一级页表是将这 1M 个标准页放置到一张页表中,二级页表是将这 1M 个标准页平均放置 1K 个页表中。这些页表是由页目录表进行存储。

2.在分页情况下,CPU访存的时候要花费很多时间,怎么办?

在分页情况下,页表是放在内存中的,因此每次访存会花费大量时间,为提高速度引入了TLB(页面高速缓存)。

在这里插入图片描述

具体的原理和cache很类似,在访问线性地址空间的某个地址时,先检查页表项是否在TLB中,如果存在,称之为命中,就可以直接访问,否则命中失败,就要进行两级查表得到相应的页表项。

  • 16
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值