页表存储着虚拟地址到物理地址的映射关系,同时为了减少页表的内存消耗发明了多级页表,更多基础内容可以看浅析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个字节
Logo

更多推荐