本文主要是介绍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.
- 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详细用途: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程序的兼容性.
-
兼容32bit程序是当时64位设计时必然要考虑的问题,32bit系统存在的时代有很多优秀的软件,这也是一种财富,不能到了64bit就不能继续使用了,而且最大限度的保持兼容,不需要重新编译即可运行.
所以32bit程序的用户空间是0-3G,在64bit空间中用户空间地址是0-128TB,32bit程序运行时只占据了它最低端的一部分空间,运行时空间的兼容性使得不需要重新编译.另外是系统调用的兼容性,这里不在赘述,看linux系统调用过程剖析 -
在用户空间地址和内核空间中间有个大大的hole,它利用高16bit不参与寻址的特点,制造了这么大一个hole,只能说有钱任性.
-
64TB的空间来做直接映射,也就是最大能够支持64TB的线性映射内存,高于64TB估计也得在vmalloc中动态使用了,目前64TB的内存支持是足够大了
-
ffffffff80000000 - ffffffffa0000000
这块空间用来映射内核的文本段,数据段等,最大512M,物理内存区域和直接映射区域是交叉的,不过直接映射区域不访问它,页表映射一块物理内存区域多次又有什么关系呢? -
其他区域就不再赘述了
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
对于虚拟地址空间来说,这样的划分不是固定的,从2004年x86_64的address map文档合并到内核Documentation/x86/x86_64/mm.txt
一直到最新的内核文档来说,变化真的很大,虚拟地址空间的规划只是众多约定的一种
x86-64的页表初始化
64bit地址需要占据8byte,所以如果是4k的页大小,则每个页只能允许512项,即2^9,每一组PGD,PUD等都占据一页的大小,页内偏移仍然是12bit.
PGD | PUD | PMD | PTE | page offset |
---|---|---|---|---|
9 | 9 | 9 | 9 | 12 |
- 初始化状态
目前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的映射关系,计算方法类似.
- 编译时初始化页表的结果如下,在运行时会进行一些偏移校准
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
- 内核启动早期的页表初始化,在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页表初始化的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!