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

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

NameNode内存生产配置

Hadoop2.x 系列,配置 NameNode 内存 NameNode 内存默认 2000m ,如果服务器内存 4G , NameNode 内存可以配置 3g 。在 hadoop-env.sh 文件中配置如下。 HADOOP_NAMENODE_OPTS=-Xmx3072m Hadoop3.x 系列,配置 Nam

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级