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


    这里,我们讲解一下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:

    1. #include <linux/init.h>
    2. #include <linux/module.h>
    3. #include <linux/mm.h>
    4. #include <linux/mm_types.h>
    5. #include <linux/sched.h>
    6. #include <linux/export.h>
    7. #include <linux/delay.h>
    8. static unsigned long cr0,cr3;
    9. static unsigned long vaddr = 0;
    10. static void get_pgtable_macro(void) //打印页机制中的一些重要参数
    11. {
    12. cr0 = read_cr0();
    13. cr3 = read_cr3_pa();
    14. printk("cr0 = 0x%lx, cr3 = 0x%lx\n",cr0,cr3);
    15. //这些宏是用来指示线性地址中相应字段所能映射的区域大小的对数的
    16. printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT);
    17. printk("P4D_SHIFT = %d\n",P4D_SHIFT);
    18. printk("PUD_SHIFT = %d\n", PUD_SHIFT);
    19. printk("PMD_SHIFT = %d\n", PMD_SHIFT);
    20. printk("PAGE_SHIFT = %d\n", PAGE_SHIFT); //指示page offset字段,映射的是一个页面的大小,一个页面大小是4k,转换成以2为底的对数就是12,其他的宏类似
    21. //下面的这些宏是用来指示相应的页目录表中的项的个数的,这些宏都是为了方便寻页时进行位运算的
    22. printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD);
    23. printk("PTRS_PER_P4D = %d\n", PTRS_PER_P4D);
    24. printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD);
    25. printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD);
    26. printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE);
    27. printk("PAGE_MASK = 0x%lx\n", PAGE_MASK); //page_mask,页内偏移掩码,用来屏蔽掉page offset字段
    28. }
    29. static unsigned long vaddr2paddr(unsigned long vaddr) //线性地址到物理地址转换
    30. {
    31. //首先为每个目录项创建一个变量将它们保存起来
    32. pgd_t *pgd;
    33. p4d_t *p4d;
    34. pud_t *pud;
    35. pmd_t *pmd;
    36. pte_t *pte;
    37. unsigned long paddr = 0;
    38. unsigned long page_addr = 0;
    39. unsigned long page_offset = 0;
    40. pgd = pgd_offset(current->mm,vaddr); //第一个参数是当前进程的mm_struct结构(我们申请的线性地址空间是内核,所以应该查内核页表,又因为所有的进程都共享同一个内核页表,所以可以用当前进程的mm_struct结构来进行查找)
    41. printk("pgd_val = 0x%lx, pgd_index = %lu\n", pgd_val(*pgd),pgd_index(vaddr));
    42. if (pgd_none(*pgd)){
    43. printk("not mapped in pgd\n");
    44. return -1;
    45. }
    46. p4d = p4d_offset(pgd, vaddr); //查找到的页全局目录项pgd作为下级查找的参数传入到p4d_offset中
    47. printk("p4d_val = 0x%lx, p4d_index = %lu\n", p4d_val(*p4d),p4d_index(vaddr));
    48. if(p4d_none(*p4d))
    49. {
    50. printk("not mapped in p4d\n");
    51. return -1;
    52. }
    53. pud = pud_offset(p4d, vaddr);
    54. printk("pud_val = 0x%lx, pud_index = %lu\n", pud_val(*pud),pud_index(vaddr));
    55. if (pud_none(*pud)) {
    56. printk("not mapped in pud\n");
    57. return -1;
    58. }
    59. pmd = pmd_offset(pud, vaddr);
    60. printk("pmd_val = 0x%lx, pmd_index = %lu\n", pmd_val(*pmd),pmd_index(vaddr));
    61. if (pmd_none(*pmd)) {
    62. printk("not mapped in pmd\n");
    63. return -1;
    64. }
    65. pte = pte_offset_kernel(pmd, vaddr); //与上面略有不同,这里表示在内核页表中查找,在进程页表中查找是另外一个完全不同的函数 这里最后取得了页表的线性地址
    66. printk("pte_val = 0x%lx, ptd_index = %lu\n", pte_val(*pte),pte_index(vaddr));
    67. if (pte_none(*pte)) {
    68. printk("not mapped in pte\n");
    69. return -1;
    70. }
    71. //从页表的线性地址中取出该页表所映射页框的物理地址
    72. page_addr = pte_val(*pte) & PAGE_MASK; //取出其高48
    73. //取出页偏移地址,页偏移量也就是线性地址中的低12
    74. page_offset = vaddr & ~PAGE_MASK;
    75. //将两个地址拼接起来,就得到了想要的物理地址了
    76. paddr = page_addr | page_offset;
    77. printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset);
    78. printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr);
    79. return paddr;
    80. }
    81. static int __init v2p_init(void) //内核模块的注册函数
    82. {
    83. unsigned long vaddr = 0 ;
    84. printk("vaddr to paddr module is running..\n");
    85. get_pgtable_macro();
    86. printk("\n");
    87. vaddr = __get_free_page(GFP_KERNEL); //在内核的ZONE_NORMAL中申请了一块页面,GFP_KERNEL标志指示优先从内核的ZONE_NORMAL中申请页框
    88. if (vaddr == 0) {
    89. printk("__get_free_page failed..\n");
    90. return 0;
    91. }
    92. sprintf((char *)vaddr, "hello world from kernel"); //在地址中写入hello
    93. printk("get_page_vaddr=0x%lx\n", vaddr);
    94. vaddr2paddr(vaddr);
    95. ssleep(600);
    96. return 0;
    97. }
    98. static void __exit v2p_exit(void) //内核模块的卸载函数
    99. {
    100. printk("vaddr to paddr module is leaving..\n");
    101. free_page(vaddr); //将申请的线性地址空间释放掉
    102. }
    103. module_init(v2p_init);
    104. module_exit(v2p_exit);
    105. MODULE_LICENSE("GPL");

    Makefile文件如下:

    1. obj-m:= paging_lowmem.o
    2. PWD:= $(shell pwd)
    3. KERNELDIR:= /home/shupeiyao/linux-5.14.17
    4. all:
    5. make -C $(KERNELDIR) M=$(PWD) modules
    6. clean:
    7. @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

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

  • 相关阅读:
    器械团体怎么利用自动化流程缩短工程期限
    sql:SQL优化知识点记录(十一)
    Docker的网络模式 (4+1)
    【leetcode】 数组双指针和滑动窗口
    【车载开发系列】UDS诊断---电控单元复位 ($0x11)
    【赠书第6期】MATLAB科学计算从入门到精通
    链表经典面试题(五)
    Power BI 自定义门户----大成
    虚拟机无法启动提示give root password for maintenance的多种解决方法
    云原生周刊:Grafana Beyla 发布 | 2023.9.18
  • 原文地址:https://blog.csdn.net/m0_74282605/article/details/128098220