Linux内存管理(4):内存映射机制
2016-07-29 00:00
513 查看
现代意义上的操作系统都处于32位保护模式下。每个进程一般都能寻址4G的内存空间。但是我们的物理内存常常没有这么大,进程怎么能获得4G的内存空间呢?这就是使用了虚拟地址的好处。我们经常在程序的反汇编代码中看到一些类似0x32118965这样的地址,操作系统中称为线性地址,或虚拟地址。通常我们使用一种叫做虚拟内存的技术来实现,因为可以使用硬盘中的一部分来当作内存使用。另外,现在操作系统都划分为系统空间和用户空间,使用虚拟地址可以很好的保护内核空间不被用户空间破坏。Linux 2.6内核使用了许多技术来改进对大量虚拟内存空间的使用,以及对内存映射的优化,使得Linux比以往任何时候都更适用于企业。包括反向映射(reverse mapping)、使用更大的内存页、页表条目存储在高端内存中,以及更稳定的管理器。 对于虚拟地址如何转为物理地址,这个转换过程有操作系统和CPU共同完成。操作系统为CPU设置好页表。CPU通过MMU单元进行地址转换。CPU做出映射的前提是操作系统要为其准备好内核页表,而对于页表的设置,内核在系统启动的初期和系统初始化完成后都分别进行了设置。
Linux简化了分段机制,使得虚拟地址与线性地址总是一致,因此Linux的虚拟地址空间也为0~4G。Linux内核将这4G字节的空间分为两部分。将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个进程使用,称为“用户空间“。因为每个进程可以通过系统调用进入内核,因此Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。
Linux使用两级保护机制:0级供内核使用,3级供用户程序使用。每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。最高的1GB字节虚拟内核空间则为所有进程以及内核所共享。内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000)开始。对内核空间来说,其地址映射是很简单的线性映射,0xC0000000就是物理地址与线性地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET。
1、与内存映射相关的宏定义
这些宏定义在include/asm-generic/page.h中,用于定义Linux三级分页模型中的页全局目录项pgd、页中间目录项pmd、页表项pte的数据类型,以及基本的地址转换,如下:
2、临时页表的初始化
linux页表映射机制的建立分为两个阶段,第一个阶段是内核进入保护模式之前要先建立一个临时内核页表并开启分页功能,因为在进入保护模式后,内核继续初始化直到建立完整的内存映射机制之前,仍然需要用到页表来映射相应的内存地址。对x86 32位内核,这个工作在保护模式下的内核入口函数arch/x86/kernel/head_32.S:startup_32()中完成。第二阶段是建立完整的内存映射机制,在在setup_arch()--->arch/x86/mm/init.c:init_memory_mapping()中完成。注意对于物理地址扩展(PAE)分页机制,Intel通过在她得处理器上把管脚数从32增加到36已经满足了这些需求,寻址能力可以达到64GB。不过,只有引入一种新的分页机制把32位线性地址转换为36位物理地址才能使用所增加的物理地址。linux为对多种体系的支持,选择了一套简单的通用实现机制。在这里只分析x86 32位下的实现。
arch/x86/kernel/head_32.S中的startup_32()相关汇编代码如下:
(1)swapper_pg_dir是临时全局页目录表起址,它是在内核编译过程中静态初始化的。首先 page_pde_offset得到开始目录项的索引。从这可以看出内核是在swapper_pg_dir的第768个表项开始建立页表。其对应线性地址就是__brk_base(内核编译时指定其值,默认为0xc0000000)以上的地址,即3GB以上的高端地址(3GB-4GB),再次强调这高端的1GB线性空间是内核占据的虚拟空间,在进行实际内存映射时,映射到物理内存却总是从最低地址(0x00000000)开始。
(2)将目录表的地址swapper_pg_dir传给edx,表明内核也要从__brk_base开始建立页表,这样可以保证从以物理地址取指令到以线性地址在系统空间取指令的平稳过渡。
(3)创建并保存PDE条目。
(4)终止条件end + MAPPING_BEYOND_END决定了内核到底要建立多少页表,也就是要映射多少内存空间。在内核初始化程中内核只要保证能映射到包括内核的代码段,数据段,初始页表和用于存放动态数据结构的128k大小的空间就行。在这段代码中,内核为什么要把用户空间和内核空间的前几个目录项映射到相同的页表中去呢?虽然在head_32.S中内核已经进入保护模式,但是内核现在是处于保护模式的段式寻址方式下,因为内核还没有启用分页映射机制,现在都是以物理地址来取指令,如果代码中遇到了符号地址,只能减去0xc0000000才行,当开启了映射机制后就不用了。现在cpu中的取指令指针eip仍指向低区,如果只建立内核空间中的映射,那么当内核开启映射机制后,低区中的地址就没办法寻址了,因为没有对应的页表,除非遇到某个符号地址作为绝对转移或调用子程序为止。因此要尽快开启CPU的页式映射机制。
(5)开启CPU页式映射机制:initial_page_table表示目录表起址,传到eax中,然后保存到cr3控制寄存器中(从而前面“内存模型”介绍中可知cr3保存页目录表起址)。把cr0的最高位置成1来开启映射机制(即设置PG位)。
通过ljmp $__BOOT_CS,$1f这条指令使CPU进入了系统空间继续执行,因为__BOOT_CS是个符号地址,地址在0xc0000000以上。在head_32.S完成了内核临时页表的建立后,它继续进行初始化,包括初始化INIT_TASK,也就是系统开启后的第一个进程;建立完整的中断处理程序,然后重新加载GDT描述符,最后跳转到init/main.c中的start_kernel()函数继续初始化。
3、内存映射机制的完整建立
根据前面介绍,这一阶段在start_kernel()--->setup_arch()中完成。在Linux中,物理内存被分为低端内存区和高端内存区(如果内核编译时配置了高端内存标志的话),为了建立物理内存到虚拟地址空间的映射,需要先计算出物理内存总共有多少页面数,即找出最大可用页框号,这包含了整个低端和高端内存区。还要计算出低端内存区总共占多少页面。
在setup_arch(),首先调用arch/x86/kernel/e820.c:e820_end_of_ram_pfn()找出最大可用页帧号(即总页面数),并保存在全局变量max_pfn中,这个变量定义可以在mm/bootmem.c中找到。它直接调用e820.c中的e820_end_pfn()完成工作。如下:
然后,setup_arch()会调用arch/x86/mm/init_32.c:find_low_pfn_range()找出低端内存区的最大可用页帧号,保存在全局变量max_low_pfn中(也定义在mm/bootmem.c中)。如下:
(1)init_32.c中定义了一个静态全局变量highmem_pages,用来保存用户指定的高端空间的大小(即总页面数)。
(2)在find_low_pfn_range()中,如果物理内存总页面数max_pfn不大于低端页面数上限MAXMEM_PFN(即物理内存大小没有超出低端空间范围),则直接没有高端地址映射,调用lowmem_pfn_init(),将max_low_pfn设成max_pfn。注意若内核编译时通过CONFIG_HIGHMEM指定必须有高端映射,则max_low_pfn的值需要减去高端页面数highmem_pages,以表示低端页面数。
(3)如果物理内存总页面数大于低端页面数上限,则表明有高端映射,因为需要把超出的部分放在高端空间区,这是一般PC机的运行流程。调用highmem_pfn_init(),如果启动时用户没有指定高端页面数,则显然max_low_pfn=MAXMEM_PFN,highmem_pages = max_pfn - MAXMEM_PFN;如果启动时用户通过highmem=x启动参数指定了高端页面数highmem_pages,则仍然有max_low_pfn=MAXMEM_PFN,但max_pfn可能出现不一致的情况,需要更新为MAXMEM_PFN + highmem_pages,如果出现越界(高端空间区太小),则要做相应越界处理。
有了总页面数、低端页面数、高端页面数这些信息,setup_arch()接着调用arch/x86/mm/init.c:init_memory_mapping(0, max_low_pfn<<PAGE_SHIFT)函数建立完整的内存映射机制。该函数在PAGE_OFFSET处建立物理内存的直接映射,即把物理内存中0~max_low_pfn<<12地址范围的低端空间区直接映射到内核虚拟空间(它是从PAGE_OFFSET即0xc0000000开始的1GB线性地址)。这在bootmem初始化之前运行,并且直接从物理内存获取页面,这些页面在前面已经被临时映射了。注意高端映射区并没有映射到实际的物理页面,只是这种机制的初步建立,页表存储的空间保留。代码如下:
(1)激活PSE和PGE,如果它们可用的话。更新page_size_mask掩码,这会在后面设置页表时用到。这个掩码可以用来区分使用的内存页大小,普通内存页为2KB,大内存页为4MB,启用了物理地址扩展(PAE)的系统上是2MB。
(2)根据传进来的地址范围计算起始页面帧号start_pfn和终止页面帧号end_pfn,调用save_mr()将这段页面范围保存到mr数组中,并更新pos,后面会用到。这里mr是由map_range结构构成的结构体数组,map_range结构封装了一个映射范围。
(3)遍历mr数组,合并相同页面大小的连接页面。
(4)调用find_early_table_space()为内核空间直接映射的页表查找可用的空间。然后对mr中的每个物理页面区域,调用核心函数kernel_physical_mapping_init()设置页表映射,以将它映射到内核空间。
(5)调用early_ioremap_page_table_range_init()对高端内存区建立页表映射,并把临时页表基址swapper_pg_dir加载到CR3寄存器中。
(6)因为将基址放到了CR3寄存器中,所以要调用__flush_tlb_all()对其寄存器刷新,以表示将内容放到内存中。然后,调用reserve_early()将分配给建立页表机制的内存空间保留。
map_range结构、save_mr(),以及find_early_table_space()的实现也都在arch/x86/mm/init.c中,如下:
(1)save_mr()将要映射的页面范围start_pfn~end_pfn保存到数组mr的一个元素中去。
(2)find_early_table_space()先计算映射所需的pud, pmd, pte个数,对32位系统,页表存放的起始地址为0x7000。然后,调用find_e820_area()从e820.map中找到连续的足够大小的内存来存放用于映射的页表,并将页表起始地址的物理页面帧号保存到相关的全局变量中。
4、内核空间映射kernel_physical_mapping_init()分析
对32位系统,该函数在arch/x86/mm/init_32.c中。它把低端区的所有max_low_pfn个物理内存页面映射到内核虚拟地址空间,映射页表从内核空间的起始地址处开始创建,即从PAGE_OFFSET(0xc0000000)开始的整个内核空间,直到物理内存映射完毕。理解了这个函数,就能大概理解内核是如何建立页表的,从而完整地弄清这个抽象模型。如下:
(1)函数开始定义了几个变量,pgd_base指向临时全局页表起始地址(即swapper_pg_dir)。pgd指向一个页表目录项开始的地址,pmd指向一个中间目录开始的地址,pte指向一个页表开始的地址,start_pfn为要映射的起始地址所在物理页框号,end_pfn为终止地址所在物理页框号。
(2)函数实现采用两次迭代的方式来实现。第一次迭代使用基于use_pse标志的大内存页或小内存页来进行映射,其他属性则与前期head_32.S中的设置一致。第二次迭代设置内核映射需要的一些特别属性(NX, GLOBAL等)。这种两次迭代的实现方式是为了遵循TLB应用程序的理念,即对任何线性地址,软件不应该用改变页面大小或者物理页框及属性的方式来对页表条目进行写操作。TLB即Translation Lookaside Buffer,旁路转换缓冲,或称为页表缓冲;里面存放的是一些页表(虚拟地址到物理地址的转换表)。又称为快表技术。由于“页表”存储在主存储器中,查询页表所付出的代价很大,由此产生了TLB。
在前面的“内存模型”中介绍过,x86系统使用三级页表机制,第一级页表称为页全局目录pgd,第二级为页中间目录pmd,第三级为页表条目pte。TLB和CPU里的一级、二级缓存之间不存在本质的区别,只不过前者缓存页表数据,而后两个缓存实际数据。当CPU执行机构收到应用程序发来的虚拟地址后,首先到TLB中查找相应的页表数据,如果TLB中正好存放着所需的页表,则称为TLB命中(TLB Hit),接下来CPU再依次看TLB中页表所对应的物理内存地址中的数据是不是已经在一级、二级缓存里了,若没有则到内存中取相应地址所存放的数据。既然说TLB是内存里存放的页表的缓存,那么它里边存放的数据实际上和内存页表区的数据是一致的,在内存的页表区里,每一条记录虚拟页面和物理页框对应关系的记录称之为一个页表条目(Entry),同样地,在TLB里边也缓存了同样大小的页表条目(Entry)。
(3)迭代开始时,pgd_idx根据pgd_index宏计算出开始页框在PGD表中的索引,注意内核要从页目录表中第768个表项开始进行设置,因此索引值会从768开始。 从768到1024这个256个表项被linux内核设置成内核目录项,低768个目录项被用户空间使用。 pgd = pgd_base + pgd_idx使得pgd指向页框所在的pgd目录项。接下来的循环是要填充从该索引值到1024的这256个pgd目录项的内容。对其中每个表项,调用one_md_table_init()创建下一级pmd表,并让pgd表中的目录项指向它。其中若启用了PAE,则Linux需要三级分页以处理大内存页,因此创建pmd表;若没启用PAE,则只需二级映射,这会忽略pmd中间目录表的,因此通过pmd_offset直接返回pgd的地址。
(4)对Linux三级映射模型,需要继续设置pmd表。因此用pmd_index宏计算出页框在PMD表中的索引,定位到对应的pmd目录项,然后用一个循环填充各个pmd目录项的内容(二级映射则直接忽略些循环)。对每个pmd目录项,先计算出物理页框要映射到的内核空间线性地址addr,从代码可以看到它从0xc000000开始的,也就是从内核空间开始。根据use_pse标志来决定是使用大内存页映射,如果是使用普通的4K内存页映射,则调用one_page_table_init()创建一个最终的页表pte,并让pmd目录项指向它。在该函数中,若启动分配器已建立,则利用alloc_bootmem_low_pages()分配一个4k大小的物理页面,否则从刚才分配建立的表中分配空间。然后用set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE))来设置对应pmd表项。page_table显然属于线性地址,先通过__pa宏转化为物理地址,再与上_PAGE_TABLE宏,此时它们还是无符号整数,再通过__pmd宏把无符号整数转化为pmd类型,经过这些转换,就得到了一个具有属性的表项,然后通过set_pmd宏设置pmd表项。
(5)设置pte表也是一个循环。pte表中有1024个表项,先要计算出要映射的页框所在的表项索引值,然后对每个页表项,用__pgprot(PTE_IDENT_ATTR)获取同一个初始化映射属性,因为在第一次迭代中使用这个属性。 is_kernel_text函数判断addr线性地址是否属于内核代码段。PAGE_OFFSET表示内核代码段的开始地址,__init_end是个内核符号,在内核链接的时候生成的,表示内核代码段的终止地址。如果是,那么在设置页表项的时候就要加个PAGE_KERNEL_EXEC属性,如果不是,则加个PAGE_KERNEL属性。第二次迭代会使用这个属性。这些属性定义可以在arch/x86/include/asm/pgtable_types.h中找到。最后通过set_pte(pte, pfn_pte(pfn, ...))来设置页表项,先通过pfn_pte宏根据页框号和页表项的属性值合并成一个页表项值,然户在用set_pte宏把页表项值写到页表项里。注意第一次迭代设置的是init_prot中的属性,第二次迭代设置prot中的属性。
(6)是后,对第一次迭代,还要更新直接映射页面数。并调用__flush_tlb_all()刷新小内存页或大内存页的TLB中的映射内容。
在开始的init_memory_mapping()执行中,当通过kernel_physical_mapping_init()建立完低端物理内存区与内核空间的三级页表映射后,内核页表就设置好了。然后调用early_ioremap_page_table_range_init()初始化高端内存的固定映射区。
5、高端内存固定映射区的初始化
early_ioremap_page_table_range_init()函数也是在arch/x86/mm/init_32.c中。它只是对固定映射区创建页表结构,并不建立实际映射,实际映射将由set_fixmap()来完成。如下:
(1)先计算出固定映射区的起始和终止地址,然后调用page_table_range_init(),用新的bootmem页表项初始化这段高端物理内存要映射到的内核虚拟地址空间,但并不建立实际的映射。最后用early_ioremap_reset()设置after_paging_init为1,表示启动分页机制。
(2)在函数page_table_range_init()中,先获取起址的pgd表项索引、pmd表项索引,然后类似地建立下一级pmd表,和最终的pte页表。在建立页表时需要调用page_table_kmap_check()进行检查,因为在前期可能对固定映射区已经分配了页表项,为使页表分配的空间连续,需要对固定映射区的页表指定区间重新分配。
在init_memory_mapping()中,内核设置好内核页表,并初始化完高端固定映射区后,紧接着调用load_cr3(swapper_pg_dir),将页全局目录表基址swapper_pg_dir送入控制寄存器cr3。每当重新设置cr3时, CPU就会将页面映射目录所在的页面装入CPU内部高速缓存中的TLB部分。现在内存中(实际上是高速缓存中)的映射目录变了,就要再让CPU装入一次。由于页面映射机制本来就是开启着的,所以从load_cr3这条指令执行完以后就扩大了系统空间中有映射区域的大小, 使整个映射覆盖到整个物理内存(高端内存除外)。实际上此时swapper_pg_dir中已经改变的目录项很可能还在高速缓存中,所以还要通过__flush_tlb_all()将高速缓存中的内容冲刷到内存中,这样才能保证内存中映射目录内容的一致性。
通过上述对init_memory_mapping()的剖析,我们可以清晰的看到,构建内核页表,无非就是向相应的表项写入下一级地址和属性。在内核空间保留着一部分内存专门用来存放内核页表。当cpu要进行寻址的时候,无论在内核空间,还是在用户空间,都会通过这个页表来进行映射。对于这个函数,内核把整个物理内存空间都映射完了,当用户空间的进程要使用物理内存时,岂不是不能做相应的映射了?其实不会的,内核只是做了映射,映射不代表使用,这样做是内核为了方便管理内存而已。
Linux简化了分段机制,使得虚拟地址与线性地址总是一致,因此Linux的虚拟地址空间也为0~4G。Linux内核将这4G字节的空间分为两部分。将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个进程使用,称为“用户空间“。因为每个进程可以通过系统调用进入内核,因此Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。
Linux使用两级保护机制:0级供内核使用,3级供用户程序使用。每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。最高的1GB字节虚拟内核空间则为所有进程以及内核所共享。内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000)开始。对内核空间来说,其地址映射是很简单的线性映射,0xC0000000就是物理地址与线性地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET。
1、与内存映射相关的宏定义
这些宏定义在include/asm-generic/page.h中,用于定义Linux三级分页模型中的页全局目录项pgd、页中间目录项pmd、页表项pte的数据类型,以及基本的地址转换,如下:
#ifndef __ASM_GENERIC_PAGE_H #define __ASM_GENERIC_PAGE_H /* * 针对NOMMU体系结构的通用page.h实现,为内存管理提供虚拟定义 */ #ifdef CONFIG_MMU #error need to prove a real asm/page.h #endif /* PAGE_SHIFT决定页的大小 */ #define PAGE_SHIFT 12 #ifdef __ASSEMBLY__ /* 页大小为4KB(不使用大内存页时) */ #define PAGE_SIZE (1 << PAGE_SHIFT) #else #define PAGE_SIZE (1UL << PAGE_SHIFT) #endif #define PAGE_MASK (~(PAGE_SIZE-1)) #include <asm/setup.h> #ifndef __ASSEMBLY__ #define get_user_page(vaddr) __get_free_page(GFP_KERNEL) #define free_user_page(page, addr) free_page(addr) #define clear_page(page) memset((page), 0, PAGE_SIZE) #define copy_page(to,from) memcpy((to), (from), PAGE_SIZE) #define clear_user_page(page, vaddr, pg) clear_page(page) #define copy_user_page(to, from, vaddr, pg) copy_page(to, from) /* * 使用C的类型检查.. */ typedef struct { unsigned long pte; } pte_t; typedef struct { unsigned long pmd[16]; } pmd_t; typedef struct { unsigned long pgd; } pgd_t; typedef struct { unsigned long pgprot; } pgprot_t; typedef struct page *pgtable_t; /* 把x转换成对应无符号整数 */ #define pte_val(x) ((x).pte) #define pmd_val(x) ((&x)->pmd[0]) #define pgd_val(x) ((x).pgd) #define pgprot_val(x) ((x).pgprot) /* 把无符号整数转换成对应的C类型 */ #define __pte(x) ((pte_t) { (x) } ) #define __pmd(x) ((pmd_t) { (x) } ) #define __pgd(x) ((pgd_t) { (x) } ) #define __pgprot(x) ((pgprot_t) { (x) } ) /* 物理内存的起始地址和结束地址 */ extern unsigned long memory_start; extern unsigned long memory_end; #endif /* !__ASSEMBLY__ */ /* 如果内核配置了RAM的基地址,则把页偏移设为这个值,否则为0 */ #ifdef CONFIG_KERNEL_RAM_BASE_ADDRESS #define PAGE_OFFSET (CONFIG_KERNEL_RAM_BASE_ADDRESS) #else #define PAGE_OFFSET (0) #endif #ifndef __ASSEMBLY__ /* 把物理地址x转换为线性地址(即虚拟地址) */ #define __va(x) ((void *)((unsigned long)(x) + PAGE_OFFSET)) /* 把内核空间的线性地址x转换为物理地址 */ #define __pa(x) ((unsigned long) (x) - PAGE_OFFSET) /* 根据内核空间的线性地址得到其物理页框号(即第几页) */ #define virt_to_pfn(kaddr) (__pa(kaddr) >> PAGE_SHIFT) /* 根据物理页框号得到其线性地址 */ #define pfn_to_virt(pfn) __va((pfn) << PAGE_SHIFT) /* 根据用户空间的线性地址得到其物理页号 */ #define virt_to_page(addr) (mem_map + (((unsigned long)(addr)-PAGE_OFFSET) >> PAGE_SHIFT)) /* 根据物理页号得到其用户空间的线性地址 */ #define page_to_virt(page) ((((page) - mem_map) << PAGE_SHIFT) + PAGE_OFFSET) #ifndef page_to_phys #define page_to_phys(page) ((dma_addr_t)page_to_pfn(page) << PAGE_SHIFT) #endif #define pfn_valid(pfn) ((pfn) < max_mapnr) #define virt_addr_valid(kaddr) (((void *)(kaddr) >= (void *)PAGE_OFFSET) && \ ((void *)(kaddr) < (void *)memory_end)) #endif /* __ASSEMBLY__ */ #include <asm-generic/memory_model.h> #include <asm-generic/getorder.h> #endif /* __ASM_GENERIC_PAGE_H */主要的定义有页移位数PAGE_SHIFT为12;页大小PAGE_SIZE为4KB(不使用大内存页时);三级映射映射模型的表项数据类型pte, pmd和pgd;内核空间的物理地址与线性地址的转换__va(x), __pa(x);线性地址与物理页框号的转换virt_to_pfn(), pfn_to_virt(), virt_to_page(), page_to_virt()。
2、临时页表的初始化
linux页表映射机制的建立分为两个阶段,第一个阶段是内核进入保护模式之前要先建立一个临时内核页表并开启分页功能,因为在进入保护模式后,内核继续初始化直到建立完整的内存映射机制之前,仍然需要用到页表来映射相应的内存地址。对x86 32位内核,这个工作在保护模式下的内核入口函数arch/x86/kernel/head_32.S:startup_32()中完成。第二阶段是建立完整的内存映射机制,在在setup_arch()--->arch/x86/mm/init.c:init_memory_mapping()中完成。注意对于物理地址扩展(PAE)分页机制,Intel通过在她得处理器上把管脚数从32增加到36已经满足了这些需求,寻址能力可以达到64GB。不过,只有引入一种新的分页机制把32位线性地址转换为36位物理地址才能使用所增加的物理地址。linux为对多种体系的支持,选择了一套简单的通用实现机制。在这里只分析x86 32位下的实现。
arch/x86/kernel/head_32.S中的startup_32()相关汇编代码如下:
__HEAD ENTRY(startup_32) /* test KEEP_SEGMENTS flag to see if the bootloader is asking us to not reload segments */ testb $(1<<6), BP_loadflags(%esi) jnz 2f /* ...... */ /* * 初始化页表。这会创建一个PDE和一个页表集,存放在__brk_base的上面。 * 变量_brk_end会被设置成指向第一个“安全”的区域。在虚拟地址0(为标识映射) * 和PAGE_OFFSET处会创建映射。注意在这里栈还没有被设置 */ default_entry: #ifdef CONFIG_X86_PAE /* * 在PAE模式下swapper_pg_dir被静态定义包括足够多的条目以包含VMSPLIT选项(即最高的1, * 2或3的条目)。标识映射通过把两个PGD条目指向第一个内核PMD条目来实现 * 注意在这一阶段,每个PMD或PTE的上半部分总是为0 */ #define KPMDS (((-__PAGE_OFFSET) >> 30) & 3) /* 内核PMD的数量 */ xorl %ebx,%ebx /* %ebx保持为0 */ movl $pa(__brk_base), %edi movl $pa(swapper_pg_pmd), %edx movl $PTE_IDENT_ATTR, %eax 10: leal PDE_IDENT_ATTR(%edi),%ecx /* 创建PMD条目 */ movl %ecx,(%edx) /* 保存PMD条目 */ /* 上半部分已经为0 */ addl $8,%edx movl $512,%ecx 11: stosl xchgl %eax,%ebx stosl xchgl %eax,%ebx addl $0x1000,%eax loop 11b /* * 终止条件:我们必须映射到end + MAPPING_BEYOND_END. */ movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp cmpl %ebp,%eax jb 10b 1: addl $__PAGE_OFFSET, %edi movl %edi, pa(_brk_end) shrl $12, %eax movl %eax, pa(max_pfn_mapped) /* 对fixmap区域做初期的初始化 */ movl $pa(swapper_pg_fixmap)+PDE_IDENT_ATTR,%eax movl %eax,pa(swapper_pg_pmd+0x1000*KPMDS-8) #else /* 非PAE */ /* 得到开始目录项的索引 */ page_pde_offset = (__PAGE_OFFSET >> 20); /* 将基地址__brk_base转换成物理地址,传给edi */ movl $pa(__brk_base), %edi /* 将全局页目录表地址传给edx */ movl $pa(swapper_pg_dir), %edx movl $PTE_IDENT_ATTR, %eax 10: leal PDE_IDENT_ATTR(%edi),%ecx /* 创建PDE条目 */ movl %ecx,(%edx) /* 保存标识PDE条目 */ movl %ecx,page_pde_offset(%edx) /* 保存内核PDE条目 */ addl $4,%edx movl $1024, %ecx 11: stosl addl $0x1000,%eax loop 11b /* * 终止条件:我们必须映射到end + MAPPING_BEYOND_END. */ movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp cmpl %ebp,%eax jb 10b addl $__PAGE_OFFSET, %edi movl %edi, pa(_brk_end) shrl $12, %eax movl %eax, pa(max_pfn_mapped) /* 对fixmap区域做初期的初始化 */ movl $pa(swapper_pg_fixmap)+PDE_IDENT_ATTR,%eax movl %eax,pa(swapper_pg_dir+0xffc) #endif jmp 3f /* * Non-boot CPU entry point; entered from trampoline.S * We can't lgdt here, because lgdt itself uses a data segment, but * we know the trampoline has already loaded the boot_gdt for us. * * If cpu hotplug is not supported then this code can go in init section * which will be freed later */ __CPUINIT #ifdef CONFIG_SMP ENTRY(startup_32_smp) cld movl $(__BOOT_DS),%eax movl %eax,%ds movl %eax,%es movl %eax,%fs movl %eax,%gs #endif /* CONFIG_SMP */ 3: /* * New page tables may be in 4Mbyte page mode and may * be using the global pages. * * NOTE! If we are on a 486 we may have no cr4 at all! * So we do not try to touch it unless we really have * some bits in it to set. This won't work if the BSP * implements cr4 but this AP does not -- very unlikely * but be warned! The same applies to the pse feature * if not equally supported. --macro * * NOTE! We have to correct for the fact that we're * not yet offset PAGE_OFFSET.. */ #define cr4_bits pa(mmu_cr4_features) movl cr4_bits,%edx andl %edx,%edx jz 6f movl %cr4,%eax # 打开分页选项(PSE,PAE,...) orl %edx,%eax movl %eax,%cr4 btl $5, %eax # 检查PAE是否开启 jnc 6f /* 检查扩展函数功能是否实现 */ movl $0x80000000, %eax cpuid cmpl $0x80000000, %eax jbe 6f mov $0x80000001, %eax cpuid /* Execute Disable bit supported? */ btl $20, %edx jnc 6f /* 设置EFER (Extended Feature Enable Register) */ movl $0xc0000080, %ecx rdmsr btsl $11, %eax /* 使更改生效 */ wrmsr 6: /* * 开启分页功能 */ movl pa(initial_page_table), %eax movl %eax,%cr3 /* 设置页表指针:cr3控制寄存器保存的是目录表地址 */ movl %cr0,%eax orl $X86_CR0_PG,%eax movl %eax,%cr0 /* ..同时设置分页(PG)标识位 */ ljmp $__BOOT_CS,$1f /* 清除预读取和规格化%eip */ 1: /* 设置栈指针 */ lss stack_start,%esp /* * Initialize eflags. Some BIOS's leave bits like NT set. This would * confuse the debugger if this code is traced. * XXX - best to initialize before switching to protected mode. */ pushl $0 popfl #ifdef CONFIG_SMP cmpb $0, ready jz 1f /* 初始的CPU要清除BSS */ jmp checkCPUtype 1: #endif /* CONFIG_SMP */其中PTE_IDENT_ATTR等常量定义在arch/x86/include/asm/pgtable_types.h中,如下:
/* * 初期标识映射的pte属性宏 */ #ifdef CONFIG_X86_64 #define __PAGE_KERNEL_IDENT_LARGE_EXEC __PAGE_KERNEL_LARGE_EXEC #else /* * For PDE_IDENT_ATTR include USER bit. As the PDE and PTE protection * bits are combined, this will alow user to access the high address mapped * VDSO in the presence of CONFIG_COMPAT_VDSO */ #define PTE_IDENT_ATTR 0x003 /* PRESENT+RW */ #define PDE_IDENT_ATTR 0x067 /* PRESENT+RW+USER+DIRTY+ACCESSED */ #define PGD_IDENT_ATTR 0x001 /* PRESENT (no other attributes) */ #endif分析(其中的非PAE模式):
(1)swapper_pg_dir是临时全局页目录表起址,它是在内核编译过程中静态初始化的。首先 page_pde_offset得到开始目录项的索引。从这可以看出内核是在swapper_pg_dir的第768个表项开始建立页表。其对应线性地址就是__brk_base(内核编译时指定其值,默认为0xc0000000)以上的地址,即3GB以上的高端地址(3GB-4GB),再次强调这高端的1GB线性空间是内核占据的虚拟空间,在进行实际内存映射时,映射到物理内存却总是从最低地址(0x00000000)开始。
(2)将目录表的地址swapper_pg_dir传给edx,表明内核也要从__brk_base开始建立页表,这样可以保证从以物理地址取指令到以线性地址在系统空间取指令的平稳过渡。
(3)创建并保存PDE条目。
(4)终止条件end + MAPPING_BEYOND_END决定了内核到底要建立多少页表,也就是要映射多少内存空间。在内核初始化程中内核只要保证能映射到包括内核的代码段,数据段,初始页表和用于存放动态数据结构的128k大小的空间就行。在这段代码中,内核为什么要把用户空间和内核空间的前几个目录项映射到相同的页表中去呢?虽然在head_32.S中内核已经进入保护模式,但是内核现在是处于保护模式的段式寻址方式下,因为内核还没有启用分页映射机制,现在都是以物理地址来取指令,如果代码中遇到了符号地址,只能减去0xc0000000才行,当开启了映射机制后就不用了。现在cpu中的取指令指针eip仍指向低区,如果只建立内核空间中的映射,那么当内核开启映射机制后,低区中的地址就没办法寻址了,因为没有对应的页表,除非遇到某个符号地址作为绝对转移或调用子程序为止。因此要尽快开启CPU的页式映射机制。
(5)开启CPU页式映射机制:initial_page_table表示目录表起址,传到eax中,然后保存到cr3控制寄存器中(从而前面“内存模型”介绍中可知cr3保存页目录表起址)。把cr0的最高位置成1来开启映射机制(即设置PG位)。
通过ljmp $__BOOT_CS,$1f这条指令使CPU进入了系统空间继续执行,因为__BOOT_CS是个符号地址,地址在0xc0000000以上。在head_32.S完成了内核临时页表的建立后,它继续进行初始化,包括初始化INIT_TASK,也就是系统开启后的第一个进程;建立完整的中断处理程序,然后重新加载GDT描述符,最后跳转到init/main.c中的start_kernel()函数继续初始化。
3、内存映射机制的完整建立
根据前面介绍,这一阶段在start_kernel()--->setup_arch()中完成。在Linux中,物理内存被分为低端内存区和高端内存区(如果内核编译时配置了高端内存标志的话),为了建立物理内存到虚拟地址空间的映射,需要先计算出物理内存总共有多少页面数,即找出最大可用页框号,这包含了整个低端和高端内存区。还要计算出低端内存区总共占多少页面。
在setup_arch(),首先调用arch/x86/kernel/e820.c:e820_end_of_ram_pfn()找出最大可用页帧号(即总页面数),并保存在全局变量max_pfn中,这个变量定义可以在mm/bootmem.c中找到。它直接调用e820.c中的e820_end_pfn()完成工作。如下:
#ifdef CONFIG_X86_32 # ifdef CONFIG_X86_PAE # define MAX_ARCH_PFN (1ULL<<(36-PAGE_SHIFT)) # else # define MAX_ARCH_PFN (1ULL<<(32-PAGE_SHIFT)) # endif #else /* CONFIG_X86_32 */ # define MAX_ARCH_PFN MAXMEM>>PAGE_SHIFT #endif /* * 找出最大可用页帧号 */ static unsigned long __init e820_end_pfn(unsigned long limit_pfn, unsigned type) { int i; unsigned long last_pfn = 0; unsigned long max_arch_pfn = MAX_ARCH_PFN; /* 4G地址空间对应的页面数 */ /* 对e820中所有的内存块,其中e820为从bios中探测到的页面数存放处 */ for (i = 0; i < e820.nr_map; i++) { struct e820entry *ei = &e820.map[i]; /* 第i个物理页面块 */ unsigned long start_pfn; unsigned long end_pfn; if (ei->type != type) /* 与要找的类型不匹配 */ continue; /* 起始地址和结束地址对应的页面帧号 */ start_pfn = ei->addr >> PAGE_SHIFT; end_pfn = (ei->addr + ei->size) >> PAGE_SHIFT; if (start_pfn >= limit_pfn) continue; if (end_pfn > limit_pfn) { /* 找到的结束页面帧号大于上限值时 */ last_pfn = limit_pfn; break; } if (end_pfn > last_pfn) /* 保存更新last_pfn */ last_pfn = end_pfn; } /* 大于4G空间时 */ if (last_pfn > max_arch_pfn) last_pfn = max_arch_pfn; /* 打印输出信息 */ printk(KERN_INFO "last_pfn = %#lx max_arch_pfn = %#lx\n", last_pfn, max_arch_pfn); /* 返回最后一个页面帧号 */ return last_pfn; } unsigned long __init e820_end_of_ram_pfn(void) { /* MAX_ARCH_PFN为4G空间 */ return e820_end_pfn(MAX_ARCH_PFN, E820_RAM); }这里MAX_ARCH_PFN为通常可寻址的4GB空间,如果启用了PAE扩展,则为64GB空间。e820_end_of_ram_pfn()直接调用e820_end_pfn()找出最大可用页面帧号,它会遍历e820.map数组中存放的所有物理页面块,找出其中最大的页面帧号,这就是我们当前需要的max_pfn值。
然后,setup_arch()会调用arch/x86/mm/init_32.c:find_low_pfn_range()找出低端内存区的最大可用页帧号,保存在全局变量max_low_pfn中(也定义在mm/bootmem.c中)。如下:
static unsigned int highmem_pages = -1; /* ...... */ /* * 全部物理内存都在包含在低端空间中 */ void __init lowmem_pfn_init(void) { /* max_low_pfn is 0, we already have early_res support */ max_low_pfn = max_pfn; if (highmem_pages == -1) highmem_pages = 0; #ifdef CONFIG_HIGHMEM if (highmem_pages >= max_pfn) { printk(KERN_ERR MSG_HIGHMEM_TOO_BIG, pages_to_mb(highmem_pages), pages_to_mb(max_pfn)); highmem_pages = 0; } if (highmem_pages) { if (max_low_pfn - highmem_pages < 64*1024*1024/PAGE_SIZE) { printk(KERN_ERR MSG_LOWMEM_TOO_SMALL, pages_to_mb(highmem_pages)); highmem_pages = 0; } max_low_pfn -= highmem_pages; } #else if (highmem_pages) printk(KERN_ERR "ignoring highmem size on non-highmem kernel!\n"); #endif } #define MSG_HIGHMEM_TOO_SMALL \ "only %luMB highmem pages available, ignoring highmem size of %luMB!\n" #define MSG_HIGHMEM_TRIMMED \ "Warning: only 4GB will be used. Use a HIGHMEM64G enabled kernel!\n" /* * 物理内存超出低端空间区:把它们放在高端地址空间中,或者通过启动时的highmem=x启动参数进行配置; * 如果不配置,在这里进行设置大小 */ void __init highmem_pfn_init(void) { /* MAXMEM_PFN为最大物理地址-(4M+4M+8K+128M); 所以低端空间的大小其实比我们说的896M低一些 */ max_low_pfn = MAXMEM_PFN; if (highmem_pages == -1) /* 高端内存页面数如果在开机没有设置 */ highmem_pages = max_pfn - MAXMEM_PFN; /* 总页面数减去低端页面数 */ /* 如果highmem_pages变量在启动项设置了,那么在这里就要进行这样的判断, 因为可能出现不一致的情况 */ if (highmem_pages + MAXMEM_PFN < max_pfn) max_pfn = MAXMEM_PFN + highmem_pages; if (highmem_pages + MAXMEM_PFN > max_pfn) { printk(KERN_WARNING MSG_HIGHMEM_TOO_SMALL, pages_to_mb(max_pfn - MAXMEM_PFN), pages_to_mb(highmem_pages)); highmem_pages = 0; } #ifndef CONFIG_HIGHMEM /* 最大可用内存是可直接寻址的 */ printk(KERN_WARNING "Warning only %ldMB will be used.\n", MAXMEM>>20); if (max_pfn > MAX_NONPAE_PFN) printk(KERN_WARNING "Use a HIGHMEM64G enabled kernel.\n"); else printk(KERN_WARNING "Use a HIGHMEM enabled kernel.\n"); max_pfn = MAXMEM_PFN; #else /* !CONFIG_HIGHMEM */ #ifndef CONFIG_HIGHMEM64G /* 在没有配置64G的情况下,内存的大小不能超过4G */ if (max_pfn > MAX_NONPAE_PFN) { max_pfn = MAX_NONPAE_PFN; printk(KERN_WARNING MSG_HIGHMEM_TRIMMED); } #endif /* !CONFIG_HIGHMEM64G */ #endif /* !CONFIG_HIGHMEM */ } /* * 确定低端和高端内存的页面帧号范围: */ void __init find_low_pfn_range(void) { /* 会更新max_pfn */ /* 当物理内存本来就小于低端空间最大页框数时, 直接没有高端地址映射 */ if (max_pfn <= MAXMEM_PFN) lowmem_pfn_init(); else /* 这是一般PC机的运行流程,存在高端映射 */ highmem_pfn_init(); }分析:
(1)init_32.c中定义了一个静态全局变量highmem_pages,用来保存用户指定的高端空间的大小(即总页面数)。
(2)在find_low_pfn_range()中,如果物理内存总页面数max_pfn不大于低端页面数上限MAXMEM_PFN(即物理内存大小没有超出低端空间范围),则直接没有高端地址映射,调用lowmem_pfn_init(),将max_low_pfn设成max_pfn。注意若内核编译时通过CONFIG_HIGHMEM指定必须有高端映射,则max_low_pfn的值需要减去高端页面数highmem_pages,以表示低端页面数。
(3)如果物理内存总页面数大于低端页面数上限,则表明有高端映射,因为需要把超出的部分放在高端空间区,这是一般PC机的运行流程。调用highmem_pfn_init(),如果启动时用户没有指定高端页面数,则显然max_low_pfn=MAXMEM_PFN,highmem_pages = max_pfn - MAXMEM_PFN;如果启动时用户通过highmem=x启动参数指定了高端页面数highmem_pages,则仍然有max_low_pfn=MAXMEM_PFN,但max_pfn可能出现不一致的情况,需要更新为MAXMEM_PFN + highmem_pages,如果出现越界(高端空间区太小),则要做相应越界处理。
有了总页面数、低端页面数、高端页面数这些信息,setup_arch()接着调用arch/x86/mm/init.c:init_memory_mapping(0, max_low_pfn<<PAGE_SHIFT)函数建立完整的内存映射机制。该函数在PAGE_OFFSET处建立物理内存的直接映射,即把物理内存中0~max_low_pfn<<12地址范围的低端空间区直接映射到内核虚拟空间(它是从PAGE_OFFSET即0xc0000000开始的1GB线性地址)。这在bootmem初始化之前运行,并且直接从物理内存获取页面,这些页面在前面已经被临时映射了。注意高端映射区并没有映射到实际的物理页面,只是这种机制的初步建立,页表存储的空间保留。代码如下:
unsigned long __init_refok init_memory_mapping(unsigned long start, unsigned long end) { unsigned long page_size_mask = 0; unsigned long start_pfn, end_pfn; unsigned long ret = 0; unsigned long pos; struct map_range mr[NR_RANGE_MR]; int nr_range, i; int use_pse, use_gbpages; printk(KERN_INFO "init_memory_mapping: %016lx-%016lx\n", start, end); #if defined(CONFIG_DEBUG_PAGEALLOC) || defined(CONFIG_KMEMCHECK) /* * For CONFIG_DEBUG_PAGEALLOC, identity mapping will use small pages. * This will simplify cpa(), which otherwise needs to support splitting * large pages into small in interrupt context, etc. */ use_pse = use_gbpages = 0; #else use_pse = cpu_has_pse; use_gbpages = direct_gbpages; #endif /* 定义了X86_PAE模式后进行调用 */ set_nx(); if (nx_enabled) printk(KERN_INFO "NX (Execute Disable) protection: active\n"); /* 激活PSE(如果可用) */ if (cpu_has_pse) set_in_cr4(X86_CR4_PSE); /* 激活PGE(如果可用) */ if (cpu_has_pge) { set_in_cr4(X86_CR4_PGE); __supported_pte_mask |= _PAGE_GLOBAL; } /* page_size_mask在这里更新,在后面设置页表时用到 */ if (use_gbpages) page_size_mask |= 1 << PG_LEVEL_1G; if (use_pse) page_size_mask |= 1 << PG_LEVEL_2M; memset(mr, 0, sizeof(mr)); nr_range = 0; /* 作为初始页面帧号值,如果没有大内存页对齐 */ start_pfn = start >> PAGE_SHIFT; /* 在setup函数中调用时,这里为0 */ pos = start_pfn << PAGE_SHIFT; /* pos为0 */ #ifdef CONFIG_X86_32 /* * Don't use a large page for the first 2/4MB of memory * because there are often fixed size MTRRs in there * and overlapping MTRRs into large pages can cause * slowdowns. */ if (pos == 0) /* end_pfn的大小为1k,也就是4M大小的内存 */ end_pfn = 1<<(PMD_SHIFT - PAGE_SHIFT); else end_pfn = ((pos + (PMD_SIZE - 1))>>PMD_SHIFT) << (PMD_SHIFT - PAGE_SHIFT); #else /* CONFIG_X86_64 */ end_pfn = ((pos + (PMD_SIZE - 1)) >> PMD_SHIFT) << (PMD_SHIFT - PAGE_SHIFT); #endif if (end_pfn > (end >> PAGE_SHIFT)) end_pfn = end >> PAGE_SHIFT; if (start_pfn < end_pfn) { /* 4M空间将这个区间存放在mr数组中 */ nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 0); pos = end_pfn << PAGE_SHIFT; } /* 大内存页(2M)范围:对齐到PMD,换算成页面的多少 */ start_pfn = ((pos + (PMD_SIZE - 1))>>PMD_SHIFT) << (PMD_SHIFT - PAGE_SHIFT); #ifdef CONFIG_X86_32 /* 这里的结束地址设置为调用的结束位页面数,也就是 所有的物理页面数 */ end_pfn = (end>>PMD_SHIFT) << (PMD_SHIFT - PAGE_SHIFT); #else /* CONFIG_X86_64 */ end_pfn = ((pos + (PUD_SIZE - 1))>>PUD_SHIFT) << (PUD_SHIFT - PAGE_SHIFT); if (end_pfn > ((end>>PMD_SHIFT)<<(PMD_SHIFT - PAGE_SHIFT))) end_pfn = ((end>>PMD_SHIFT)<<(PMD_SHIFT - PAGE_SHIFT)); #endif if (start_pfn < end_pfn) { /* 将这段内存放入mr中,保存后面用到 */ nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, page_size_mask & (1<<PG_LEVEL_2M)); /* 这里保证了运用PSE时为2M页面而不是PSE时, 仍然为4K页面(上面的按位或和这里的按位与) */ pos = end_pfn << PAGE_SHIFT; /* 更新pos */ } #ifdef CONFIG_X86_64 /* 大内存页(1G)范围 */ start_pfn = ((pos + (PUD_SIZE - 1))>>PUD_SHIFT) << (PUD_SHIFT - PAGE_SHIFT); end_pfn = (end >> PUD_SHIFT) << (PUD_SHIFT - PAGE_SHIFT); if (start_pfn < end_pfn) { nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, page_size_mask & ((1<<PG_LEVEL_2M)|(1<<PG_LEVEL_1G))); pos = end_pfn << PAGE_SHIFT; } /* 尾部不是大内存页(1G)对齐 */ start_pfn = ((pos + (PMD_SIZE - 1))>>PMD_SHIFT) << (PMD_SHIFT - PAGE_SHIFT); end_pfn = (end >> PMD_SHIFT) << (PMD_SHIFT - PAGE_SHIFT); if (start_pfn < end_pfn) { nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, page_size_mask & (1<<PG_LEVEL_2M)); pos = end_pfn << PAGE_SHIFT; } #endif /* 尾部不是大内存页(2M)对齐 */ start_pfn = pos>>PAGE_SHIFT; end_pfn = end>>PAGE_SHIFT; nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 0); /* 合并相同页面大小的连续的页面 */ for (i = 0; nr_range > 1 && i < nr_range - 1; i++) { unsigned long old_start; if (mr[i].end != mr[i+1].start || mr[i].page_size_mask != mr[i+1].page_size_mask) continue; /* move it */ old_start = mr[i].start; memmove(&mr[i], &mr[i+1], (nr_range - 1 - i) * sizeof(struct map_range)); mr[i--].start = old_start; nr_range--; } /* 打印相关信息 */ for (i = 0; i < nr_range; i++) printk(KERN_DEBUG " %010lx - %010lx page %s\n", mr[i].start, mr[i].end, (mr[i].page_size_mask & (1<<PG_LEVEL_1G))?"1G":( (mr[i].page_size_mask & (1<<PG_LEVEL_2M))?"2M":"4k")); /* * 为内核直接映射的页表查找空间 * 以后我们应该在内存映射的本地节点分配这些页表。不幸的是目前这需要在 * 查找到节点之前来做 */ if (!after_bootmem) /*如果内存启动分配器没有建立,则直接从e820.map中找到合适的 连续内存,找到存放页表的空间首地址为e820_table_start */ find_early_table_space(end, use_pse, use_gbpages); #ifdef CONFIG_X86_32 for (i = 0; i < nr_range; i++) /* 对每个保存的区域设置页表映射 */ kernel_physical_mapping_init(mr[i].start, mr[i].end, mr[i].page_size_mask); ret = end; #else /* CONFIG_X86_64 */ for (i = 0; i < nr_range; i++) ret = kernel_physical_mapping_init(mr[i].start, mr[i].end, mr[i].page_size_mask); #endif #ifdef CONFIG_X86_32 /* 对高端内存固定区域建立映射 */ early_ioremap_page_table_range_init(); /* 放入CR3寄存器 */ load_cr3(swapper_pg_dir); #endif #ifdef CONFIG_X86_64 if (!after_bootmem && !start) { pud_t *pud; pmd_t *pmd; mmu_cr4_features = read_cr4(); /* * _brk_end cannot change anymore, but it and _end may be * located on different 2M pages. cleanup_highmap(), however, * can only consider _end when it runs, so destroy any * mappings beyond _brk_end here. */ pud = pud_offset(pgd_offset_k(_brk_end), _brk_end); pmd = pmd_offset(pud, _brk_end - 1); while (++pmd <= pmd_offset(pud, (unsigned long)_end - 1)) pmd_clear(pmd); } #endif __flush_tlb_all(); /* 刷新寄存器 */ /* 将分配给建立页表机制的内存空间保留 */ if (!after_bootmem && e820_table_end > e820_table_start) reserve_early(e820_table_start << PAGE_SHIFT, e820_table_end << PAGE_SHIFT, "PGTABLE"); if (!after_bootmem) early_memtest(start, end); return ret >> PAGE_SHIFT; }分析:
(1)激活PSE和PGE,如果它们可用的话。更新page_size_mask掩码,这会在后面设置页表时用到。这个掩码可以用来区分使用的内存页大小,普通内存页为2KB,大内存页为4MB,启用了物理地址扩展(PAE)的系统上是2MB。
(2)根据传进来的地址范围计算起始页面帧号start_pfn和终止页面帧号end_pfn,调用save_mr()将这段页面范围保存到mr数组中,并更新pos,后面会用到。这里mr是由map_range结构构成的结构体数组,map_range结构封装了一个映射范围。
(3)遍历mr数组,合并相同页面大小的连接页面。
(4)调用find_early_table_space()为内核空间直接映射的页表查找可用的空间。然后对mr中的每个物理页面区域,调用核心函数kernel_physical_mapping_init()设置页表映射,以将它映射到内核空间。
(5)调用early_ioremap_page_table_range_init()对高端内存区建立页表映射,并把临时页表基址swapper_pg_dir加载到CR3寄存器中。
(6)因为将基址放到了CR3寄存器中,所以要调用__flush_tlb_all()对其寄存器刷新,以表示将内容放到内存中。然后,调用reserve_early()将分配给建立页表机制的内存空间保留。
map_range结构、save_mr(),以及find_early_table_space()的实现也都在arch/x86/mm/init.c中,如下:
unsigned long __initdata e820_table_start; unsigned long __meminitdata e820_table_end; unsigned long __meminitdata e820_table_top; int after_bootmem; int direct_gbpages #ifdef CONFIG_DIRECT_GBPAGES = 1 #endif ; /* 查找页表需要的空间 */ static void __init find_early_table_space(unsigned long end, int use_pse, int use_gbpages) { unsigned long puds, pmds, ptes, tables, start; /* 计算需要用到多少pud,当没有pud存在的情况下pud=pgd */ puds = (end + PUD_SIZE - 1) >> PUD_SHIFT; tables = roundup(puds * sizeof(pud_t), PAGE_SIZE); if (use_gbpages) { unsigned long extra; extra = end - ((end>>PUD_SHIFT) << PUD_SHIFT); pmds = (extra + PMD_SIZE - 1) >> PMD_SHIFT; } else pmds = (end + PMD_SIZE - 1) >> PMD_SHIFT; /* 计算映射所有内存所要求的所有pmd的个数 */ tables += roundup(pmds * sizeof(pmd_t), PAGE_SIZE); if (use_pse) { unsigned long extra; extra = end - ((end>>PMD_SHIFT) << PMD_SHIFT); #ifdef CONFIG_X86_32 extra += PMD_SIZE; #endif ptes = (extra + PAGE_SIZE - 1) >> PAGE_SHIFT; } else /* 计算所需要的pte个数 */ ptes = (end + PAGE_SIZE - 1) >> PAGE_SHIFT; tables += roundup(ptes * sizeof(pte_t), PAGE_SIZE); #ifdef CONFIG_X86_32 /* for fixmap */ /* 加上固定内存映射区的页表数量 */ tables += roundup(__end_of_fixed_addresses * sizeof(pte_t), PAGE_SIZE); #endif /* * RED-PEN putting page tables only on node 0 could * cause a hotspot and fill up ZONE_DMA. The page tables * need roughly 0.5KB per GB. */ #ifdef CONFIG_X86_32 start = 0x7000; /* 页表存放的开始地址,这里为什么从这里开始? */ #else start = 0x8000; #endif /* 从e820.map中找到连续的足够大小的内存来存放用于映射的页表, 返回起始地址 */ e820_table_start = find_e820_area(start, max_pfn_mapped<<PAGE_SHIFT, tables, PAGE_SIZE); if (e820_table_start == -1UL) panic("Cannot find space for the kernel page tables"); /* 将页表起始地址的物理页面帧号保存到相关的全局变量中 */ e820_table_start >>= PAGE_SHIFT; e820_table_end = e820_table_start; e820_table_top = e820_table_start + (tables >> PAGE_SHIFT); printk(KERN_DEBUG "kernel direct mapping tables up to %lx @ %lx-%lx\n", end, e820_table_start << PAGE_SHIFT, e820_table_top << PAGE_SHIFT); } struct map_range { unsigned long start; unsigned long end; unsigned page_size_mask; }; #ifdef CONFIG_X86_32 #define NR_RANGE_MR 3 #else /* CONFIG_X86_64 */ #define NR_RANGE_MR 5 #endif /* 将要映射的页面范围保存到mr数组中 */ static int __meminit save_mr(struct map_range *mr, int nr_range, unsigned long start_pfn, unsigned long end_pfn, unsigned long page_size_mask) { if (start_pfn < end_pfn) { if (nr_range >= NR_RANGE_MR) panic("run out of range for init_memory_mapping\n"); mr[nr_range].start = start_pfn<<PAGE_SHIFT; mr[nr_range].end = end_pfn<<PAGE_SHIFT; mr[nr_range].page_size_mask = page_size_mask; nr_range++; } return nr_range; }分析:
(1)save_mr()将要映射的页面范围start_pfn~end_pfn保存到数组mr的一个元素中去。
(2)find_early_table_space()先计算映射所需的pud, pmd, pte个数,对32位系统,页表存放的起始地址为0x7000。然后,调用find_e820_area()从e820.map中找到连续的足够大小的内存来存放用于映射的页表,并将页表起始地址的物理页面帧号保存到相关的全局变量中。
4、内核空间映射kernel_physical_mapping_init()分析
对32位系统,该函数在arch/x86/mm/init_32.c中。它把低端区的所有max_low_pfn个物理内存页面映射到内核虚拟地址空间,映射页表从内核空间的起始地址处开始创建,即从PAGE_OFFSET(0xc0000000)开始的整个内核空间,直到物理内存映射完毕。理解了这个函数,就能大概理解内核是如何建立页表的,从而完整地弄清这个抽象模型。如下:
unsigned long __init kernel_physical_mapping_init(unsigned long start, unsigned long end, unsigned long page_size_mask) { int use_pse = page_size_mask == (1<<PG_LEVEL_2M); unsigned long start_pfn, end_pfn; pgd_t *pgd_base = swapper_pg_dir; int pgd_idx, pmd_idx, pte_ofs; unsigned long pfn; pgd_t *pgd; pmd_t *pmd; pte_t *pte; unsigned pages_2m, pages_4k; int mapping_iter; /* 得到要映射的起始地址和终止地址所在页在页帧号 */ start_pfn = start >> PAGE_SHIFT; end_pfn = end >> PAGE_SHIFT; /* * First iteration will setup identity mapping using large/small pages * based on use_pse, with other attributes same as set by * the early code in head_32.S * * Second iteration will setup the appropriate attributes (NX, GLOBAL..) * as desired for the kernel identity mapping. * * This two pass mechanism conforms to the TLB app note which says: * * "Software should not write to a paging-structure entry in a way * that would change, for any linear address, both the page size * and either the page frame or attributes." */ mapping_iter = 1; if (!cpu_has_pse) use_pse = 0; repeat: pages_2m = pages_4k = 0; pfn = start_pfn; /* 返回页框在PGD表中的索引 */ pgd_idx = pgd_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET); pgd = pgd_base + pgd_idx; for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) { pmd = one_md_table_init(pgd); /* 创建该pgd目录项指向的pmd表 */ if (pfn >= end_pfn) continue; #ifdef CONFIG_X86_PAE /* 三级映射需要设置pmd,因此得到页框在PMD表中的索引 */ pmd_idx = pmd_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET); pmd += pmd_idx; #else pmd_idx = 0; /* 两级映射则无需设置 */ #endif for (; pmd_idx < PTRS_PER_PMD && pfn < end_pfn; pmd++, pmd_idx++) { unsigned int addr = pfn * PAGE_SIZE + PAGE_OFFSET; /* * 如果可能,用大页面来映射,否则创建正常大小的页表: */ if (use_pse) { unsigned int addr2; pgprot_t prot = PAGE_KERNEL_LARGE; /* * first pass will use the same initial * identity mapping attribute + _PAGE_PSE. */ pgprot_t init_prot = __pgprot(PTE_IDENT_ATTR | _PAGE_PSE); addr2 = (pfn + PTRS_PER_PTE-1) * PAGE_SIZE + PAGE_OFFSET + PAGE_SIZE-1; if (is_kernel_text(addr) || is_kernel_text(addr2)) prot = PAGE_KERNEL_LARGE_EXEC; pages_2m++; if (mapping_iter == 1) set_pmd(pmd, pfn_pmd(pfn, init_prot)); else set_pmd(pmd, pfn_pmd(pfn, prot)); pfn += PTRS_PER_PTE; continue; } pte = one_page_table_init(pmd); /* 返回PMD中第一个PTE */ /* PTE的索引 */ pte_ofs = pte_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET); pte += pte_ofs; /* 定位带具体的pte */ for (; pte_ofs < PTRS_PER_PTE && pfn < end_pfn; pte++, pfn++, pte_ofs++, addr += PAGE_SIZE) { pgprot_t prot = PAGE_KERNEL; /* * first pass will use the same initial * identity mapping attribute. */ pgprot_t init_prot = __pgprot(PTE_IDENT_ATTR); if (is_kernel_text(addr)) prot = PAGE_KERNEL_EXEC; pages_4k++; /* 没有PSE */ /* 设置页表,根据MAPPING_ITER变量的不同 对表设置不同的属性 */ if (mapping_iter == 1) /* 第一次迭代,属性设置都一样 */ set_pte(pte, pfn_pte(pfn, init_prot)); else /* 设置为具体的属性 */ set_pte(pte, pfn_pte(pfn, prot)); } } } if (mapping_iter == 1) { /* * 只在第一次迭代中更新直接映射页的数量 */ update_page_count(PG_LEVEL_2M, pages_2m); update_page_count(PG_LEVEL_4K, pages_4k); /* * local global flush tlb, which will flush the previous * mappings present in both small and large page TLB's. */ __flush_tlb_all(); /* * 第二次迭代将设置实际的PTE属性 */ mapping_iter = 2; goto repeat; } return 0; /* 迭代两后返回 */ } static pmd_t * __init one_md_table_init(pgd_t *pgd) { pud_t *pud; pmd_t *pmd_table; #ifdef CONFIG_X86_PAE /* 启用了PAE,需要三级映射,创建PMD表 */ if (!(pgd_val(*pgd) & _PAGE_PRESENT)) { if (after_bootmem) pmd_table = (pmd_t *)alloc_bootmem_pages(PAGE_SIZE); else pmd_table = (pmd_t *)alloc_low_page(); paravirt_alloc_pmd(&init_mm, __pa(pmd_table) >> PAGE_SHIFT); /* 设置PGD,将对应的PGD项设置为PMD表 */ set_pgd(pgd, __pgd(__pa(pmd_table) | _PAGE_PRESENT)); pud = pud_offset(pgd, 0); BUG_ON(pmd_table != pmd_offset(pud, 0)); return pmd_table; } #endif /* 非PAE模式:只需二级映射,直接返回原来pgd地址 */ pud = pud_offset(pgd, 0); pmd_table = pmd_offset(pud, 0); return pmd_table; } static pte_t * __init one_page_table_init(pmd_t *pmd) { if (!(pmd_val(*pmd) & _PAGE_PRESENT)) { pte_t *page_table = NULL; if (after_bootmem) { #if defined(CONFIG_DEBUG_PAGEALLOC) || defined(CONFIG_KMEMCHECK) page_table = (pte_t *) alloc_bootmem_pages(PAGE_SIZE); #endif if (!page_table) page_table = (pte_t *)alloc_bootmem_pages(PAGE_SIZE); } else /* 如果启动分配器还没有建立,那么 从刚才分配建立的表中分配空间 */ page_table = (pte_t *)alloc_low_page(); paravirt_alloc_pte(&init_mm, __pa(page_table) >> PAGE_SHIFT); /* 设置PMD,将对应的PMD项设置为页表 */ set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE)); BUG_ON(page_table != pte_offset_kernel(pmd, 0)); } return pte_offset_kernel(pmd, 0); } static inline int is_kernel_text(unsigned long addr) { if (addr >= PAGE_OFFSET && addr <= (unsigned long)__init_end) return 1; return 0; }分析:
(1)函数开始定义了几个变量,pgd_base指向临时全局页表起始地址(即swapper_pg_dir)。pgd指向一个页表目录项开始的地址,pmd指向一个中间目录开始的地址,pte指向一个页表开始的地址,start_pfn为要映射的起始地址所在物理页框号,end_pfn为终止地址所在物理页框号。
(2)函数实现采用两次迭代的方式来实现。第一次迭代使用基于use_pse标志的大内存页或小内存页来进行映射,其他属性则与前期head_32.S中的设置一致。第二次迭代设置内核映射需要的一些特别属性(NX, GLOBAL等)。这种两次迭代的实现方式是为了遵循TLB应用程序的理念,即对任何线性地址,软件不应该用改变页面大小或者物理页框及属性的方式来对页表条目进行写操作。TLB即Translation Lookaside Buffer,旁路转换缓冲,或称为页表缓冲;里面存放的是一些页表(虚拟地址到物理地址的转换表)。又称为快表技术。由于“页表”存储在主存储器中,查询页表所付出的代价很大,由此产生了TLB。
在前面的“内存模型”中介绍过,x86系统使用三级页表机制,第一级页表称为页全局目录pgd,第二级为页中间目录pmd,第三级为页表条目pte。TLB和CPU里的一级、二级缓存之间不存在本质的区别,只不过前者缓存页表数据,而后两个缓存实际数据。当CPU执行机构收到应用程序发来的虚拟地址后,首先到TLB中查找相应的页表数据,如果TLB中正好存放着所需的页表,则称为TLB命中(TLB Hit),接下来CPU再依次看TLB中页表所对应的物理内存地址中的数据是不是已经在一级、二级缓存里了,若没有则到内存中取相应地址所存放的数据。既然说TLB是内存里存放的页表的缓存,那么它里边存放的数据实际上和内存页表区的数据是一致的,在内存的页表区里,每一条记录虚拟页面和物理页框对应关系的记录称之为一个页表条目(Entry),同样地,在TLB里边也缓存了同样大小的页表条目(Entry)。
(3)迭代开始时,pgd_idx根据pgd_index宏计算出开始页框在PGD表中的索引,注意内核要从页目录表中第768个表项开始进行设置,因此索引值会从768开始。 从768到1024这个256个表项被linux内核设置成内核目录项,低768个目录项被用户空间使用。 pgd = pgd_base + pgd_idx使得pgd指向页框所在的pgd目录项。接下来的循环是要填充从该索引值到1024的这256个pgd目录项的内容。对其中每个表项,调用one_md_table_init()创建下一级pmd表,并让pgd表中的目录项指向它。其中若启用了PAE,则Linux需要三级分页以处理大内存页,因此创建pmd表;若没启用PAE,则只需二级映射,这会忽略pmd中间目录表的,因此通过pmd_offset直接返回pgd的地址。
(4)对Linux三级映射模型,需要继续设置pmd表。因此用pmd_index宏计算出页框在PMD表中的索引,定位到对应的pmd目录项,然后用一个循环填充各个pmd目录项的内容(二级映射则直接忽略些循环)。对每个pmd目录项,先计算出物理页框要映射到的内核空间线性地址addr,从代码可以看到它从0xc000000开始的,也就是从内核空间开始。根据use_pse标志来决定是使用大内存页映射,如果是使用普通的4K内存页映射,则调用one_page_table_init()创建一个最终的页表pte,并让pmd目录项指向它。在该函数中,若启动分配器已建立,则利用alloc_bootmem_low_pages()分配一个4k大小的物理页面,否则从刚才分配建立的表中分配空间。然后用set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE))来设置对应pmd表项。page_table显然属于线性地址,先通过__pa宏转化为物理地址,再与上_PAGE_TABLE宏,此时它们还是无符号整数,再通过__pmd宏把无符号整数转化为pmd类型,经过这些转换,就得到了一个具有属性的表项,然后通过set_pmd宏设置pmd表项。
(5)设置pte表也是一个循环。pte表中有1024个表项,先要计算出要映射的页框所在的表项索引值,然后对每个页表项,用__pgprot(PTE_IDENT_ATTR)获取同一个初始化映射属性,因为在第一次迭代中使用这个属性。 is_kernel_text函数判断addr线性地址是否属于内核代码段。PAGE_OFFSET表示内核代码段的开始地址,__init_end是个内核符号,在内核链接的时候生成的,表示内核代码段的终止地址。如果是,那么在设置页表项的时候就要加个PAGE_KERNEL_EXEC属性,如果不是,则加个PAGE_KERNEL属性。第二次迭代会使用这个属性。这些属性定义可以在arch/x86/include/asm/pgtable_types.h中找到。最后通过set_pte(pte, pfn_pte(pfn, ...))来设置页表项,先通过pfn_pte宏根据页框号和页表项的属性值合并成一个页表项值,然户在用set_pte宏把页表项值写到页表项里。注意第一次迭代设置的是init_prot中的属性,第二次迭代设置prot中的属性。
(6)是后,对第一次迭代,还要更新直接映射页面数。并调用__flush_tlb_all()刷新小内存页或大内存页的TLB中的映射内容。
在开始的init_memory_mapping()执行中,当通过kernel_physical_mapping_init()建立完低端物理内存区与内核空间的三级页表映射后,内核页表就设置好了。然后调用early_ioremap_page_table_range_init()初始化高端内存的固定映射区。
5、高端内存固定映射区的初始化
early_ioremap_page_table_range_init()函数也是在arch/x86/mm/init_32.c中。它只是对固定映射区创建页表结构,并不建立实际映射,实际映射将由set_fixmap()来完成。如下:
void __init early_ioremap_page_table_range_init(void) { pgd_t *pgd_base = swapper_pg_dir; unsigned long vaddr, end; /* * 固定映射,只是创建页表结构,并不建立实际映射。实际映射将由set_fixmap()来完成: */ vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK; end = (FIXADDR_TOP + PMD_SIZE - 1) & PMD_MASK; /* 这里是对临时映射区域进行映射而为页表等分配了空间, 但是没有建立实际的映射 */ page_table_range_init(vaddr, end, pgd_base); /* 置变量after_paging_init为1,表示启动了分页机制 */ early_ioremap_reset(); } static void __init page_table_range_init(unsigned long start, unsigned long end, pgd_t *pgd_base) { int pgd_idx, pmd_idx; unsigned long vaddr; pgd_t *pgd; pmd_t *pmd; pte_t *pte = NULL; vaddr = start; pgd_idx = pgd_index(vaddr); pmd_idx = pmd_index(vaddr); pgd = pgd_base + pgd_idx; for ( ; (pgd_idx < PTRS_PER_PGD) && (vaddr != end); pgd++, pgd_idx++) { pmd = one_md_table_init(pgd); pmd = pmd + pmd_index(vaddr); for (; (pmd_idx < PTRS_PER_PMD) && (vaddr != end); pmd++, pmd_idx++) { /* early fixmap可能对临时映射区中的页表项已经分配了页表, 为使页表分配的空间连续,需要对临时映射区的页表指定区间重新分配 */ /* 在这里已经对pte进行了分配和初始化 */ pte = page_table_kmap_check(one_page_table_init(pmd), pmd, vaddr, pte); vaddr += PMD_SIZE; } pmd_idx = 0; } }分析:
(1)先计算出固定映射区的起始和终止地址,然后调用page_table_range_init(),用新的bootmem页表项初始化这段高端物理内存要映射到的内核虚拟地址空间,但并不建立实际的映射。最后用early_ioremap_reset()设置after_paging_init为1,表示启动分页机制。
(2)在函数page_table_range_init()中,先获取起址的pgd表项索引、pmd表项索引,然后类似地建立下一级pmd表,和最终的pte页表。在建立页表时需要调用page_table_kmap_check()进行检查,因为在前期可能对固定映射区已经分配了页表项,为使页表分配的空间连续,需要对固定映射区的页表指定区间重新分配。
在init_memory_mapping()中,内核设置好内核页表,并初始化完高端固定映射区后,紧接着调用load_cr3(swapper_pg_dir),将页全局目录表基址swapper_pg_dir送入控制寄存器cr3。每当重新设置cr3时, CPU就会将页面映射目录所在的页面装入CPU内部高速缓存中的TLB部分。现在内存中(实际上是高速缓存中)的映射目录变了,就要再让CPU装入一次。由于页面映射机制本来就是开启着的,所以从load_cr3这条指令执行完以后就扩大了系统空间中有映射区域的大小, 使整个映射覆盖到整个物理内存(高端内存除外)。实际上此时swapper_pg_dir中已经改变的目录项很可能还在高速缓存中,所以还要通过__flush_tlb_all()将高速缓存中的内容冲刷到内存中,这样才能保证内存中映射目录内容的一致性。
通过上述对init_memory_mapping()的剖析,我们可以清晰的看到,构建内核页表,无非就是向相应的表项写入下一级地址和属性。在内核空间保留着一部分内存专门用来存放内核页表。当cpu要进行寻址的时候,无论在内核空间,还是在用户空间,都会通过这个页表来进行映射。对于这个函数,内核把整个物理内存空间都映射完了,当用户空间的进程要使用物理内存时,岂不是不能做相应的映射了?其实不会的,内核只是做了映射,映射不代表使用,这样做是内核为了方便管理内存而已。
相关文章推荐
- Linux进程调度(1):CFS调度器的设计框架
- Linux进程调度(3):进程切换分析
- Linux系统管理实践(10):PPPoE上网配置
- Linux系统管理实践(1):远程登录到Linux
- Linux系统管理实践(9):DHCP服务器配置
- Linux中的有趣命令
- Linux内核启动过程分析
- Linux引导过程内幕
- Linux初始RAM磁盘介绍
- Linux内存管理(4):内存映射机制
- Linux系统管理实践(12):Syslog系统日志配置
- Linux 多线程模板
- Linux进程调度(1):CFS调度器的设计框架
- Linux进程调度(3):进程切换分析
- Linux系统管理实践(10):PPPoE上网配置
- Linux系统管理实践(1):远程登录到Linux
- Linux系统管理实践(9):DHCP服务器配置
- Linux中的有趣命令
- Linux进程调度(2):CFS调度操作
- Linux Socket编程