linux内存-x86-64页表初始化

2024-03-17 01:30

本文主要是介绍linux内存-x86-64页表初始化,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

页表存储着虚拟地址到物理地址的映射关系,同时为了减少页表的内存消耗发明了多级页表,更多基础内容可以看浅析linux内存管理.

一个虚拟地址到物理地址通过页表的转换过程如下,<深入理解LINUX内核>的经典图:
页表和地址转换过程
32bit系统上一般只有PGD(Page Global Directory)和pte(page table entry),32bit虚拟地址划分成三段: 10:10:12,高10bit是PGD中偏移,中间10bit是pte数组中的偏移,低12bit是页内偏移.在x86 arch中cr3负责加载页表,它属于MMU组件部分,它看到的是物理地址空间,页表存储在进程的task_struct->active_mm->pgd中,不过它是个虚拟地址,经过pa转换才传给寄存器,具体可以看switch_mm->load_cr3.

页帧的大小是预先设定好的一组值,不是随意设定的,桌面版上一般是4k,它还能提供页帧配置的选项,对于服务器有特别的意义,在x86上如果pte上设置了PSE,则page的大小就是4M.
本来是一整块物理内存,现在分成页帧来管理,这样必然会有一些管理数据,在linux中对应的数据结构就是page,页帧为4k时8G内存需要128M的page区域,而页帧为4M只需要128K的page区域,所以在服务器上4k的页帧设置已经不适宜了;此外页帧越小,相同的虚拟地址空间大小页表项所需越少,TLB miss事件会更频繁.不过更大的内存会有更多页内碎片,造成页内浪费.

在32bit系统中,每个地址需要4byte表示,经典的10:10:12划分中,每个地址空间PGD中有需要2^10=1024项,所以每个进程PGD占据4k,每个PGD指向的pte也是占据4k,这样刚好不浪费空间.

页地址是4k对齐的,低12bit全是0,即PGD中和PTE中低12bit是冗余的,所以通常用作他途,下面是PGD中冗余位中存储一些flag.
pgd extra flag

  • S 标识page size,如果置位则页大小是4M,此时pte中PSE位也需要置位;如果是0,则页大小是4k
  • A 标识是否访问过是否访问过范围的页
  • D 标识是否Cache Disable,如果置位这个范围的页则不会cache,每次都要从memory中读写
  • W 标识Write through策略,如果设置则是write-through,如果是0则是write-back
  • U 标识范围的页属于用户空间还是内核空间,页的访问控制基于特权级别.如果设置,这个页属于用户空间,没有限制;如果没有设置,页属于内核空间,只有内核能够访问.
  • R 标识R/W, 1:可读可写 0:只读
  • P 标识Present,如果置位则映射有物理页,如果是0可能是还没分配物理页或者是swap out了.
    pte flag

关于pte的flag详细用途:https://blog.csdn.net/faxiang1230/article/details/106112857

里面的有些flag和PGD中的flag作用是相同的,下面只列出了不同项:

  • C 标识是否Cache Disable,和pgd中的 ‘D’ 位作用相同
  • G 标识全局属性,如果置位,如果CR3重新设置,它仍然在TLB中保持这部分页表项,需要CR4中使能这个功能
  • D 标识页是否被写过,这个是由MMU访问时自动置位的,但是需要CPU在回写完成后清除flag

x86-64的地址空间

64bit地址空间是对32bit的有效扩充,不过地址空间实在太大了,没有机器的内存能够达到这种级别.
目前64bit系统中地址空间只使用了低48bit,即256TB大小.intel规划了下一步可以扩充到57bit的地址空间方案,128PB大小,目前看虚拟地址资源短时间内应该不是瓶颈.

和32bit系统中有限的虚拟地址空间相比,64bit基本上可以随意使用虚拟地址空间,它去除了一些概念:HIGHMEM区域的物理内存,pkmap区域.不过地址空间划分继续保持了对32bit程序的兼容性.

  1. 兼容32bit程序是当时64位设计时必然要考虑的问题,32bit系统存在的时代有很多优秀的软件,这也是一种财富,不能到了64bit就不能继续使用了,而且最大限度的保持兼容,不需要重新编译即可运行.
    所以32bit程序的用户空间是0-3G,在64bit空间中用户空间地址是0-128TB,32bit程序运行时只占据了它最低端的一部分空间,运行时空间的兼容性使得不需要重新编译.另外是系统调用的兼容性,这里不在赘述,看linux系统调用过程剖析

  2. 在用户空间地址和内核空间中间有个大大的hole,它利用高16bit不参与寻址的特点,制造了这么大一个hole,只能说有钱任性.

  3. 64TB的空间来做直接映射,也就是最大能够支持64TB的线性映射内存,高于64TB估计也得在vmalloc中动态使用了,目前64TB的内存支持是足够大了

  4. ffffffff80000000 - ffffffffa0000000这块空间用来映射内核的文本段,数据段等,最大512M,物理内存区域和直接映射区域是交叉的,不过直接映射区域不访问它,页表映射一块物理内存区域多次又有什么关系呢?

  5. 其他区域就不再赘述了

https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt

0000000000000000 - 00007fffffffffff (=47 bits) user space, different per mm        
hole caused by [48:63] sign extension                                           
ffff800000000000 - ffff87ffffffffff (=43 bits) guard hole, reserved for hypervisor
ffff880000000000 - ffffc7ffffffffff (=64 TB) direct mapping of all phys. memory 
ffffc80000000000 - ffffc8ffffffffff (=40 bits) hole                             
ffffc90000000000 - ffffe8ffffffffff (=45 bits) vmalloc/ioremap space            
ffffe90000000000 - ffffe9ffffffffff (=40 bits) hole                             
ffffea0000000000 - ffffeaffffffffff (=40 bits) virtual memory map (1TB)         
... unused hole ...                                                             
ffffec0000000000 - fffffc0000000000 (=44 bits) kasan shadow memory (16TB)          
... unused hole ...                                                             
ffffff0000000000 - ffffff7fffffffff (=39 bits) %esp fixup stacks                
... unused hole ...                                                             
ffffffff80000000 - ffffffffa0000000 (=512 MB)  kernel text mapping, from phys 0 
ffffffffa0000000 - fffffffffeffffff (=1520 MB) module mapping space
ffffffffff000000 - FIXADDR_START unused hole
FIXADDR_START - ffffffffff9fffff (~0.5 MB) kernel-internal fixmap range, variable size and offset       
ffffffffffa00000 - ffffffffffdfffff (=8 MB) vsyscalls                           
ffffffffffe00000 - ffffffffffffffff (=2 MB) unused hole 

x86_64 地址空间
对于虚拟地址空间来说,这样的划分不是固定的,从2004年x86_64的address map文档合并到内核Documentation/x86/x86_64/mm.txt一直到最新的内核文档来说,变化真的很大,虚拟地址空间的规划只是众多约定的一种

x86-64的页表初始化

64bit地址需要占据8byte,所以如果是4k的页大小,则每个页只能允许512项,即2^9,每一组PGD,PUD等都占据一页的大小,页内偏移仍然是12bit.

PGDPUDPMDPTEpage offset
999912
  1. 初始化状态

目前x86的内核镜像基本都是经过压缩的,这能减少load内核镜像花费的IO时间,将经过压缩的镜像load到内核之后,头部包含自解压代码,将解压后的内核放到约定的地址CONFIG_PHYSICAL_START.另外前期bootloader已经开启了MMU功能,创建了部分页表,但是内核是一个独立的系统,它不能依赖于bootloader的工作,所以虽然它自己现在可以运行,仍需初始化内存管理数据并创建加载自己的页表.
32bit和64bit平台上加载内核的方式是保持兼容的,内核加载后的地址布局和32bit中仍然是相同的,查看/proc/iomem获取详细信息
内核加载之后的地址布局
2.内核的页表初始化

内核的入口地址.head.text,在链接脚本vmlinux.ldS中指定链接顺序,在System.map中也可以观察到入口函数是startup_64

ffffffff81000000 T _text                                                                                                                       
ffffffff81000000 T startup_64                                                   
ffffffff81000110 T secondary_startup_64

64bit中使用4级页表,在初始化的时候分别是:early_level4_pgt, level3_kernel_pgt,level2_kernel_pgt,level2_fixmap_pgt,level1_fixmap_pgt,在编译的时候,进行了页表初始化.
在地址空间规划中,内核镜像映射地址为ffffffff80000000 - ffffffffa0000000 kernel text mapping, from phys 0,下面计算一下它在各级页表中对应的哪些项,在早期页表中内核镜像的页帧设置成了2M的大小,只需要三级页表就可以完成映射

#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))            ==> (0xffffffff80000000 >> 39) &(512-1) = 511
#define pud_index(address) (((address) >> PUD_SHIFT) & (PTRS_PER_PUD - 1)                 ==> (0xffffffff80000000 >> 30) &(512-1) = 510
#define pmd_index(address) (((address) >> PMD_SHIFT) & (PTRS_PER_PMD - 1)                ==> (0xffffffff80000000 >> 21) &(512-1) = 0

而除了内核的text和data段的映射关系,还有fixmap的映射关系,计算方法类似.

  1. 编译时初始化页表的结果如下,在运行时会进行一些偏移校准

arch/x86/kernel/head_64.S

early_level4_pgt[511] -> level3_kernel_pgt[0]
level3_kernel_pgt[510] -> level2_kernel_pgt[0]
level3_kernel_pgt[511] -> level2_fixmap_pgt[0]
level2_kernel_pgt[0]   -> 512 MB kernel mapping
level2_fixmap_pgt[507] -> level1_fixmap_pgt
  1. 内核启动早期的页表初始化,在4.0内核中位于arch/x86/kernel/head_64.S中,在编译期间已经做完了初始化的工作,运行的时候进行偏移校准并且加载到CR3寄存器中生效。

下面是编译时页表初始化的代码注释

    leaq    _text(%rip), %rbp                                                   subq    $_text - __START_KERNEL_map, %rbp   //rbp中存储编译地址和运行地址的偏移/** 校准内核镜像映射区域页表项的物理地址偏移*/addq    %rbp, early_level4_pgt + (L4_START_KERNEL*8)(%rip)                     addq    %rbp, level3_kernel_pgt + (510*8)(%rip)                                addq    %rbp, level3_kernel_pgt + (511*8)(%rip)                                //校准固定映射区域页表项的物理地址偏移                         addq    %rbp, level2_fixmap_pgt + (506*8)(%rip)/* Fixup phys_base */                                                       addq %rbp,phys_base(%rip)                                                                                     movq    $(early_level4_pgt - __START_KERNEL_map), %rax                      jmp 1f
1:                                                                      /* 使能PGE即大页模式 */                                               movl    $(X86_CR4_PAE | X86_CR4_PGE), %ecx                                  movq    %rcx, %cr4                                                                           /* Setup early boot stage 4 level pagetables. */                            addq    phys_base(%rip), %rax                                               movq    %rax, %cr3      //load cr3
NEXT_PAGE(early_level4_pgt) 
//前面511个地址全部清零,没有进一步设置页表之前,访问这部分地址是非法的          .fill   511,8,0 
//__START_KERNEL_map即kernel mapping区域的基地址,level3_kernel_pgt代表符号的加载地址,
//他们的差就是三级页表的物理地址;地址低位存储标志位                                       .quad   level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE                        
NEXT_PAGE(level3_kernel_pgt).fill   L3_START_KERNEL,8,0 //kernel mapping区域之前的页表项清零/* (2^48-(2*1024*1024*1024)-((2^39)*511))/(2^30) = 510 */ .quad   level2_kernel_pgt - __START_KERNEL_map + _KERNPG_TABLE //指向二级页表.quad   level2_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE   //fixmap区域的页表NEXT_PAGE(level2_kernel_pgt)                                                    /*                                                                          * 512 MB kernel mapping. We spend a full page on this pagetable            * anyway.                                                                  *                                                                          * The kernel code+data+bss must not be bigger than that.                   *                                                                          * (NOTE: at +512MB starts the module area, see MODULES_VADDR.              *  If you want to increase this then increase MODULES_VADDR                *  too.)                                                                   */
//除了设置页表项之外,它还设置了PSE标志,内核早期的页帧的大小为2M,level2_kernel_pgt就是这块区域的最后一级页表                                                                         PMDS(0, __PAGE_KERNEL_LARGE_EXEC, KERNEL_IMAGE_SIZE/PMD_SIZE) //内核默认最大512M,这段空间直接映射到从0开始的物理内存,即虚拟地址0xffffffff80000000对应物理地址0
NEXT_PAGE(level2_fixmap_pgt).fill   506,8,0	//二级页表项每项管理2M的空间.quad   level1_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE //fixmap最多2M的空间/* 8MB reserved for vsyscalls + a 2MB hole = 4 + 1 entries */.fill   5,8,0    //最后2M空间是个hole,还有8M给vsyscalls预留的空间NEXT_PAGE(level1_fixmap_pgt).fill   512,8,0       //固定映射只是初始化了,但是present没有设置,是不能使用的

附录

asm中fill的用法为:

.fill repeat , size , value  //在该地址处重复repeat次,每次迭代地址增加size字节,填充值为value
.quad value				//在该地址放置4个字的数值,即8个字节

这篇关于linux内存-x86-64页表初始化的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/817379

相关文章

Linux使用nload监控网络流量的方法

《Linux使用nload监控网络流量的方法》Linux中的nload命令是一个用于实时监控网络流量的工具,它提供了传入和传出流量的可视化表示,帮助用户一目了然地了解网络活动,本文给大家介绍了Linu... 目录简介安装示例用法基础用法指定网络接口限制显示特定流量类型指定刷新率设置流量速率的显示单位监控多个

ElasticSearch+Kibana通过Docker部署到Linux服务器中操作方法

《ElasticSearch+Kibana通过Docker部署到Linux服务器中操作方法》本文介绍了Elasticsearch的基本概念,包括文档和字段、索引和映射,还详细描述了如何通过Docker... 目录1、ElasticSearch概念2、ElasticSearch、Kibana和IK分词器部署

Linux流媒体服务器部署流程

《Linux流媒体服务器部署流程》文章详细介绍了流媒体服务器的部署步骤,包括更新系统、安装依赖组件、编译安装Nginx和RTMP模块、配置Nginx和FFmpeg,以及测试流媒体服务器的搭建... 目录流媒体服务器部署部署安装1.更新系统2.安装依赖组件3.解压4.编译安装(添加RTMP和openssl模块

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

linux下多个硬盘划分到同一挂载点问题

《linux下多个硬盘划分到同一挂载点问题》在Linux系统中,将多个硬盘划分到同一挂载点需要通过逻辑卷管理(LVM)来实现,首先,需要将物理存储设备(如硬盘分区)创建为物理卷,然后,将这些物理卷组成... 目录linux下多个硬盘划分到同一挂载点需要明确的几个概念硬盘插上默认的是非lvm总结Linux下多

golang内存对齐的项目实践

《golang内存对齐的项目实践》本文主要介绍了golang内存对齐的项目实践,内存对齐不仅有助于提高内存访问效率,还确保了与硬件接口的兼容性,是Go语言编程中不可忽视的重要优化手段,下面就来介绍一下... 目录一、结构体中的字段顺序与内存对齐二、内存对齐的原理与规则三、调整结构体字段顺序优化内存对齐四、内

linux进程D状态的解决思路分享

《linux进程D状态的解决思路分享》在Linux系统中,进程在内核模式下等待I/O完成时会进入不间断睡眠状态(D状态),这种状态下,进程无法通过普通方式被杀死,本文通过实验模拟了这种状态,并分析了如... 目录1. 问题描述2. 问题分析3. 实验模拟3.1 使用losetup创建一个卷作为pv的磁盘3.

Linux环境变量&&进程地址空间详解

《Linux环境变量&&进程地址空间详解》本文介绍了Linux环境变量、命令行参数、进程地址空间以及Linux内核进程调度队列的相关知识,环境变量是系统运行环境的参数,命令行参数用于传递给程序的参数,... 目录一、初步认识环境变量1.1常见的环境变量1.2环境变量的基本概念二、命令行参数2.1通过命令编程

Linux之进程状态&&进程优先级详解

《Linux之进程状态&&进程优先级详解》文章介绍了操作系统中进程的状态,包括运行状态、阻塞状态和挂起状态,并详细解释了Linux下进程的具体状态及其管理,此外,文章还讨论了进程的优先级、查看和修改进... 目录一、操作系统的进程状态1.1运行状态1.2阻塞状态1.3挂起二、linux下具体的状态三、进程的

Linux编译器--gcc/g++使用方式

《Linux编译器--gcc/g++使用方式》文章主要介绍了C/C++程序的编译过程,包括预编译、编译、汇编和链接四个阶段,并详细解释了每个阶段的作用和具体操作,同时,还介绍了调试和发布版本的概念... 目录一、预编译指令1.1预处理功能1.2指令1.3问题扩展二、编译(生成汇编)三、汇编(生成二进制机器语