这里,我们讲解一下Linux是如何将虚拟地址转换成物理地址的

一、地址转换

在这里插入图片描述

在进程中,我们不直接对物理地址进行操作,CPU在运行时,指定的地址要经过MMU转换后才能访问到真正的物理内存。

地址转换的过程分为两部分,分段和分页。

分段机制简单的来说是将进程的代码、数据、栈分在不同的虚拟地址段上,从而避免进程间的互相影响。分段之前的地址我们称之为逻辑地址,它有两部分组成,高位的段选择符和低位的段内偏移。在分段时先用段选择符在相应的段描述符表中找到段描述符,也就是某一个段的基地址,再加上段内偏移量就得到了对应的线性地址,线性地址也称之为虚拟地址。

而在实际的应用中,Linux为了增加可移植性并没有完整的使用分段机制,它让所有的段都指向相同的段地址范围,段的基地址都为0,这样逻辑地址和线性地址在数值上就相同了

所以,这里我们分析的重点在分页,也就是由线性地址到物理地址的转换过程。

二、Linux页表

在这里插入图片描述

Linux为了兼容32位和64位CPU,它需要一个统一的页面地址模型,目前常用的是4级页表模型。

PGD 页全局目录

PUD 页上级目录

PMD 页中间目录

PT 页表

根据不同的需要,其中的某些页表可能未被使用。线性地址中每一部分的索引的大小会根据具体的计算机体系结构做相应的改变。举个例子来说,对于没有启用物理地址扩展功能的32位系统来说,两级页表就足够了,那么Linux会让线性地址中的页上级目录和页中间目录索引这两位置为0,从根本上就取消了这两个字段,但是这两个页目录在指针序列中的位置仍然被保留下来。也就是说寻址的过程中不能跳过页上级目录和页中间目录直接由页全局目录到页表,内核会将这两个页目录的表项都置为1

三、Linux线性地址

在这里插入图片描述

由于64位处理器硬件的限制,它的地址线只有48条,所以线性地址实际使用的也只有48位

在Linux中使用的4级页表结构,它的线性地址划分如上图所示。页全局目录的索引、页上级目录的索引、页中间目录的索引、页表的索引分别占了9位,最后页内偏移占了12位,共计48位,剩下的高位都是保留,留作以后扩展使用。

在这种情况下,页面的大小都为4kb,每一个页表项大小为8bit,整个页表可以映射的空间是256TB。

而新的Intel芯片的硬件规定可以进行5级的页表管理。所以在4.15的内核中,Linux已经在页全局目录和页上级目录之间又增加了一个新的页目录,叫做p4d页目录。这个页目录同32位中的情况一样,现在还未被使用,它的页目录项只有一个,线性地址中也没有它的索引位。

这里有一个很重要的寄存器,CR3寄存器,它是一系列CPU控制寄存器之一,它用来保存当前进程的页全局目录的地址,寻页的开始就是从页全局目录开始的。那么页全局目录的地址又在哪呢?

内核在创建一个进程时就会为它分配页全局目录,在进程描述符task_struct结构中有一个指向mm_struct结构的指针mm,而mm_struct结构是用来描述进程的虚拟地址空间的,在mm _struct中有个字段PGD,就是用来保存该进程的页全局目录的(物理)地址的。所以在进程切换的时候,操作系统通过访问task_struct结构,再访问mm_struct结构,最终找到PGD字段取得新进程的页全局目录的地址,填充到CR3寄存器中就完成页表的切换

以上表项在page.h中定义

四、模块编程举例

好了了解了这些之后,我们在实际的系统中来看看寻页的过程是如何完成的

结合上面的介绍,我们编写一个内核模块,把一个给定的虚地址转换为内存的物理地址:

这个内核模块的主要功能是在内核中先申请一个页面,然后利用内核提供的函数按照寻页的步骤一步步查询各级页目录,最终找到对应的物理地址。这些步骤就相当于我们手动模拟了MMU单元的寻页过程

paging_lowmem.c:

#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);
    printk("PAGE_SHIFT = %d\n", PAGE_SHIFT);   //指示page offset字段,映射的是一个页面的大小,一个页面大小是4k,转换成以2为底的对数就是12,其他的宏类似
 
 //下面的这些宏是用来指示相应的页目录表中的项的个数的,这些宏都是为了方便寻页时进行位运算的
    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);
    printk("PAGE_MASK = 0x%lx\n", PAGE_MASK);   //page_mask,页内偏移掩码,用来屏蔽掉page offset字段
}
 
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;
    
    pgd = pgd_offset(current->mm,vaddr);  //第一个参数是当前进程的mm_struct结构(我们申请的线性地址空间是内核,所以应该查内核页表,又因为所有的进程都共享同一个内核页表,所以可以用当前进程的mm_struct结构来进行查找)
    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);  //查找到的页全局目录项pgd作为下级查找的参数传入到p4d_offset中
    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;
    }
    //从页表的线性地址中取出该页表所映射页框的物理地址
    page_addr = pte_val(*pte) & PAGE_MASK;    //取出其高48位
    //取出页偏移地址,页偏移量也就是线性地址中的低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);   //在内核的ZONE_NORMAL中申请了一块页面,GFP_KERNEL标志指示优先从内核的ZONE_NORMAL中申请页框
    if (vaddr == 0) {
        printk("__get_free_page failed..\n");
        return 0;
    }
    sprintf((char *)vaddr, "hello world from kernel");   //在地址中写入hello
    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
PWD:= $(shell pwd)
KERNELDIR:= /home/shupeiyao/linux-5.14.17

all:
	make -C $(KERNELDIR)  M=$(PWD) modules
clean:
	@rm -rf *.o *.mod.c *.mod.o *.ko *.order *.symvers .*.cmd .tmp_versions


make之后
将模块插入

在这里插入图片描述

用dmesg命令查看

在这里插入图片描述

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

PAGE_MASK是一个低12位都为0,其余位都为1的一个64位的数

我们申请的线性地址是get_page_vaddr

我们依次查找了它的页全局目录项的线性地址、页四级目录项的线性地址、页上级目录项的线性地址、页中间目录项的地址,最后得到了页表项的物理地址

最后我们将线性地址vaddr转换成了物理地址paddr

我们可以看到物理地址paddr最高位是8,转换到二进制就是最高位63位是1,这是一个x86平台上用来标识该物理页框是不能用来执行代码保护的一个保护位的,这里我们不去管它,其物理页框的物理地址就是 c184000

好了,到这里我们就完成了从虚拟地址到物理地址的转换了

如果以上内容对您有帮助,麻烦点赞收藏或者关注一波哦~

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐