浅谈虚拟地址转换成物理地址(值得收藏)

article/2025/9/30 10:29:31

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

一、地址转换

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

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

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

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

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

二、Linux页表

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

PGD 页全局目录

PUD 页上级目录

PMD 页中间目录

PT 页表

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

【文章福利】小编推荐自己的Linux内核技术交流群:【977878001】整理一些个人觉得比较好得学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100进群领取,额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

三、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");   //在地址中写入helloprintk("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.17all: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

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


http://chatgpt.dhexx.cn/article/hjFvMast.shtml

相关文章

虚拟地址

虚拟地址 虚拟地址是程序运行在保护模式下&#xff0c;这样程序访问存储器所使用的逻辑地址称为虚拟地址。 那为什么需要虚拟地址呢 我们要说到寻址方式 寻址&#xff1a;根据指令内容确定操作数地址的过程&#xff0c;称为寻址 在16位的cup或者8086cpu的时候&#xff0c;他…

虚拟地址如何访问到物理地址

环境&#xff1a;32bit CPU 一、通过二级页表映射的方式访问物理地址 1、取一级页表的基地址Abase1 2、取虚拟地址的前12bit[31:20]地址O1 3、计算得到新地址Apgd(Abase1&0xFFFFF000)O1&#xff0c;此地址是PGD页表上的地址&#xff0c;取此地址中的数据Abase2 4、取虚拟地…

总线地址、物理地址、虚拟地址

1、总线地址 地址总线&#xff08;Address Bus&#xff09;是一种计算机总线&#xff0c;是CPU或有DMA能力的单元&#xff0c;用来沟通这些单元想要访问&#xff08;读取/写入&#xff09;计算机内存组件/地方的物理地址。 其实就是CPU能够访问内存的范围。 CPU寻找外部的内…

虚拟地址空间和物理地址空间

1.概念 物理地址&#xff1a;物理地址空间是实在的存在于计算机中的一个实体&#xff0c;在每一台计算机中保持唯一独立性。我们可以称它为物理内存&#xff1b;如在32位的机器上&#xff0c;物理空间的大小理论上可以达到2^32字节(4GB)&#xff0c;但如果实际装了512的内存&a…

Linux虚拟地址空间

目录 父子进程地址相同的变量值不同问题运行结果 Linux下进程虚拟地址空间分布什么是虚拟地址空间&#xff1f;进程直接访问物理内存&#xff08;无虚拟空间&#xff09;再述虚拟地址空间&#xff01;虚拟地址空间结构体是如何区域划分?解答最初的问题延伸问题: 一个pid变量怎…

虚拟地址空间

对于每一个进程都会对应一个虚拟地址空间&#xff0c;对于32位的操作系统&#xff08;其指令的位数最大为32位&#xff0c;因此地址码最多32位&#xff09;&#xff0c;虚拟地址空间的大小为B即0~4GB的虚拟地址空间&#xff0c;其中内核空间为1GB&#xff0c;如下所示&#xff…

逻辑地址、物理地址、虚拟地址

文章目录 物理地址(physical address)虚拟地址(virtual memory)逻辑地址(logical address)线性地址(linear address)或也叫虚拟地址(virtual address)地址转换 物理地址(physical address) 用于内存芯片级的单元寻址&#xff0c;与处理器和CPU连接的地址总线相对应。 虽然可以…

CPU中虚拟地址、逻辑地址(有效地址)、线性地址、物理地址

虚拟地址、逻辑地址&#xff08;有效地址&#xff09;、线性地址、物理地址 1、虚拟地址2、逻辑地址&#xff08;有效地址&#xff09;3、线性地址4、物理地址5、总结 1、虚拟地址 在实模式下&#xff0c;虚拟地址是指由程序产生的由段选择符和段内偏移地址组成的地址。经过CPU…

虚拟地址和物理地址

1、地址概念 物理地址&#xff1a;物理内存就是真实的内存&#xff0c;CPU的地址线可以直接进行寻址的内存空间大小。比如在32位平台下&#xff0c;寻址的范围是2^32也就是4G&#xff0c;并且这是固定的。在实际的应用中&#xff0c;很多的应用程序都比较大&#xff0c;计算机…

Linux操作系统~什么是虚拟地址?深度剖析进程地址空间

目录 1.所以进程的地址空间是什么呢&#xff1f; 2.mm_struct内部有什么&#xff1f; 3.虚拟地址空间与物理内存如何关联 页表 4.为什么设计这样一个进程地址空间&#xff0c;不让程序直接访问内存 Q&#xff1a;为什么子进程修改值以后&#xff0c;地址还是相同&#xf…

初识虚拟地址空间

物理地址和虚拟地址 物理寻址&#xff1a;CPU访问存储器的最原始方法就是直接用物理地址&#xff08;Physical Address, 可简称PA&#xff09;。物理地址是唯一的。 虚拟寻址&#xff1a;CPU通过生成一个虚拟的地址来访问内存&#xff0c;在访问前会把虚拟地址转化为物理地址…

虚拟地址空间,虚拟文件系统

1、虚拟地址空间 1、概念与原因 虚拟地址空间是一个抽象的概念&#xff0c;在IBM中&#xff0c;这样说道&#xff1a;它存在&#xff0c;但你看不见&#xff0c;就是虚拟的。虚拟地址空间就是这样一个东西。&#xff08;注意区分虚拟内存与虚拟地址空间&#xff09; 虚拟地址空…

彻底搞懂虚拟内存,虚拟地址,虚拟地址空间

程序经过编译后&#xff0c;变成了可执行的文件&#xff0c;可执行文件主要包括代码和数据两部分&#xff0c;代码是只读的&#xff0c;数据则是可读可写的。 可执行文件由操作系统加载到内存中&#xff0c;交由CPU去执行&#xff0c;现在问题来了&#xff0c;CPU怎么去访问代…

使用POI导出Excel(并使用公式)

使用POI导出Excel&#xff08;并使用公式&#xff09; 使用java直接生成Excel并填充数据 可以参考POI官方文档&#xff0c;就是对Sheet&#xff0c;row&#xff0c;cell&#xff0c;Formula等操作&#xff0c; https://poi.apache.org/components/index.html 这种方式也可以生成…

poi导出excel日期格式问题

POI导出Excel的时候有时需要日期格式&#xff0c;在筛选时是这样的 private XSSFWorkbook wb null; String dateFormat "yyyy-MM-dd";//或者"yyyy/MM/dd"格式,"yyyy/M/d"这样的格式不会自动补0 public void setCell(int index,Date value,bool…

POI导出Excel详细教程

文章目录 前言一、引入jar包依赖二、创建自定义导出Excel样式类三、创建核心导出工具类四、创建导出对象实体Bean五、具体使用案例5.1.创建SQL脚本和初始化数据5.2.写一个查询所有学生信息接口5.3.查询学生基本信息返回数据格式5.4.导出Excel方法5.5.通过页面导出按钮导出Excel…

Java使用POI导出Excel文件

Java使用POI导出Excel文件 POI概述Apache POI 下载依赖引用关系图如下所示:直接下载Maven下载 POI实例总结 POI概述 HSSF 是 POI 项目的 Excel 97(-2007) 文件格式的纯 Java 实现。XSSF 是 POI 项目的 Excel 2007 OOXML (.xlsx) 文件格式的纯 Java 实现。 HSSF 和 XSSF 提供了读…

Java用POI导出Excel表格中的数据

poi操作Excel 主要通过HSSF,XSSF两种方式。 HSSF只能解析.xls格式的excel文件&#xff0c;XSSF支持.xls与.xlsx两种格式。 功能&#xff1a; 传递一个Excel文件&#xff0c;拿到里面所有的数据&#xff0c;返回一个集合。 Excel中的数据是什么类型&#xff0c;就返回什么类型的…

springboot+poi导出excel

在web开发中经常遇到将数据写入excel并导出的需求&#xff0c;下面整理springbootpoi实现导出excel的实例。 搭建springboot工程&#xff0c;引入依赖&#xff0c;细节不在赘述。 引入poi依赖 <dependency><groupId>org.apache.poi</groupId><artifactId…

POI实现导入导出excel

poi在日常的导入导出中是比较常用到的&#xff0c;最近也总结了下接触到的poi相关的导入导出的一些代码&#xff0c;有问题可以指出&#xff1a; package com.poi;import km.org.apache.poi.hssf.usermodel.*; import km.org.apache.poi.hssf.util.HSSFColor; import km.org.a…