这里,我们讲解一下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");   //在地址中写入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

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

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

Linux内核学习3——虚拟地址转换成物理地址相关推荐

  1. 虚拟地址转换成物理地址

    题目: 某虚拟存储器的用户空间共有32个页面,每页1K,主存16K,假定某时刻系统为用户的第0.1.2.3页分配的物理块号为5.10.4.7,而该用户作业的长度为6页, 将十六进制的虚拟地址0A5CH ...

  2. 用户虚拟地址转换成物理地址

    最近做opengl模拟器的时候,需要从用户虚拟地址得到实际的物理地址,在网上找到一个不错的参考(http://www.eefocus.com/html/09-05/71993s.shtml),稍微修改 ...

  3. 【分页存储管理】将十六进制的虚拟地址0A5C、103C、4251转换成物理地址

    某虚拟存储器的用户空间为32个页面,每页1KB,主存16KB.假定某时刻为用户第0.1.2.3页分配的物理块号为5.10.4.7,该作业长度为5页,将十六进制的虚拟地址0A5C.103C.4251转换 ...

  4. Linux内核学习--内存管理模块

    Linux内核学习--内存管理模块 首先,Linux内核主要由五个部分组成,他们分别是:进程调度模块.内存管理模块.文件系统模块.进程间通信模块和网络接口模块. 本部分所讲的内存是内存管理模块,其主要 ...

  5. linux内核学习之三:linux中的32位与64位

    linux内核学习之三:linux中的"32位"与"64位" 在通用PC领域,不论是windows还是linux界,我们都会经常听到"32位" ...

  6. 线性地址转换成物理地址

    上篇文章介绍了如何查看内核使用的分页模式,笔者电脑上内核使用的分页模式是 4-level paging,这篇文章讲解 4-level paging分页模式如何将线性地址转换成物理地址. 先思考2个问题 ...

  7. Linux疑难杂症解决方案100篇(十五)-万字长文带你深入Linux 内核学习:环境搭建和内核编译

    一.linux内核学习之一:环境搭建--安装Debian7.3 本系列文章假设读者已对linux有一定的了解,其实学习linux内核不需要有很深的关于linux的知识,只需要了解以下内容:linux基 ...

  8. 操作系统进程学习(Linux 内核学习笔记)

    操作系统进程学习(Linux 内核学习笔记) 进程优先级 并非所有进程都具有相同的重要性.除了大多数我们所熟悉的进程优先级之外,进程还有不同的关键度类别,以满足不同需求.首先进程比较粗糙的划分,进程可 ...

  9. linux c十六进制转十进制函数,用Linux/Unix命令把十六进制转换成十进制(转)

    那天写个脚本,需要把十六进制的数字转成十进制的打出来,发现不知道要怎么弄,搜一下,原来还是很简单的,比用C语言什么的容易多了,就一些现成的命令就解决了. 先列两种简单的方法: 1) echo 自己就能 ...

最新文章

  1. windows共享linux的某一文件夹
  2. vc编程中出现 fatal error C1010: 在查找预编译头时遇到意外的文件结尾。是否忘记了向源中添加“#include stdafx.h”?...
  3. python批处理将图片进行放大实例代码
  4. oracle当查询没有输出返回0,ORACLE技术问题专家问答五则
  5. 前天做了一个点击随机改变背景色的JS,请各位老师指导下:
  6. IO 模型 IO 多路复用
  7. 2与27日 双目视觉系统,相机标定,矫正,匹配(特征,稠密),特征提取的方法,得到视差图
  8. 【AOP】面向切面谈恋爱(二)| 学废了AOP,他最终失去了爱情
  9. 按关键字爬取百度图片
  10. 2017计算机二级ms office高级应用成绩查询
  11. 计算机算法在生物信息学中的应用,引力场算法及其在生物信息学中的应用
  12. 代码随想录Day48|198.打家劫舍、213.打家劫舍II、337.打家劫舍III
  13. 32微型计算机中的32室,32 位微型计算机中的32是指(   )_学小易找答案
  14. python黑客库长安十二时辰 更新_爬取3万+评论,告诉你究竟是哪些人不喜欢《长安十二时辰》?...
  15. dataframe两个表合并_Spark DataFrame 不是真正的 DataFrame
  16. 对于CNN卷积神经网络的前向传播和反向传播的理解
  17. 分布式系统全链路压测方法
  18. 2014年2月15日 19:04:39
  19. Ansys Speos 2023 R1新功能 | Texture可视化纹理提升视觉感知
  20. python的while循环嵌套实现打印星号的三角形和正方形

热门文章

  1. robot framework
  2. golang 内存分配
  3. android 创建assetmanager文件,Android AssetManager
  4. 技术Leader,技术和管理哪个重要?
  5. Linux系统安装Java环境
  6. 创业公司为何需要技术合伙人-商业合伙人对技术合伙人的限制和挑战
  7. oracle rowid唯一吗,ROWID唯一性
  8. BES2300x笔记----TWS组对与蓝牙配对
  9. 【BZOJ2440】【中山市选2011】完全平方数
  10. 蓝桥杯 - 三羊献瑞 (java实现)