ASM:Linux内存管理模型(2)
PART4:内存映射机制
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的数据类型,以及基本的地址转换,如下:
1 #ifndef __ASM_GENERIC_PAGE_H 2 #define __ASM_GENERIC_PAGE_H 3 /* 4 * 针对NOMMU体系结构的通用page.h实现,为内存管理提供虚拟定义 5 */ 6 7 #ifdef CONFIG_MMU 8 #error need to prove a real asm/page.h 9 #endif 10 11 12 /* PAGE_SHIFT决定页的大小 */ 13 14 #define PAGE_SHIFT 12 15 #ifdef __ASSEMBLY__ 16 /* 页大小为4KB(不使用大内存页时) */ 17 #define PAGE_SIZE (1 << PAGE_SHIFT) 18 #else 19 #define PAGE_SIZE (1UL << PAGE_SHIFT) 20 #endif 21 #define PAGE_MASK (~(PAGE_SIZE-1)) 22 23 #include <asm/setup.h> 24 25 #ifndef __ASSEMBLY__ 26 27 #define get_user_page(vaddr) __get_free_page(GFP_KERNEL) 28 #define free_user_page(page, addr) free_page(addr) 29 30 #define clear_page(page) memset((page), 0, PAGE_SIZE) 31 #define copy_page(to,from) memcpy((to), (from), PAGE_SIZE) 32 33 #define clear_user_page(page, vaddr, pg) clear_page(page) 34 #define copy_user_page(to, from, vaddr, pg) copy_page(to, from) 35 36 /* 37 * 使用C的类型检查.. 38 */ 39 typedef struct { 40 unsigned long pte; 41 } pte_t; 42 typedef struct { 43 unsigned long pmd[16]; 44 } pmd_t; 45 typedef struct { 46 unsigned long pgd; 47 } pgd_t; 48 typedef struct { 49 unsigned long pgprot; 50 } pgprot_t; 51 typedef struct page *pgtable_t; 52 53 /* 把x转换成对应无符号整数 */ 54 #define pte_val(x) ((x).pte) 55 #define pmd_val(x) ((&x)->pmd[0]) 56 #define pgd_val(x) ((x).pgd) 57 #define pgprot_val(x) ((x).pgprot) 58 59 /* 把无符号整数转换成对应的C类型 */ 60 #define __pte(x) ((pte_t) { (x) } ) 61 #define __pmd(x) ((pmd_t) { (x) } ) 62 #define __pgd(x) ((pgd_t) { (x) } ) 63 #define __pgprot(x) ((pgprot_t) { (x) } ) 64 65 /* 物理内存的起始地址和结束地址 */ 66 extern unsigned long memory_start; 67 extern unsigned long memory_end; 68 69 #endif /* !__ASSEMBLY__ */ 70 71 /* 如果内核配置了RAM的基地址,则把页偏移设为这个值,否则为0 */ 72 #ifdef CONFIG_KERNEL_RAM_BASE_ADDRESS 73 #define PAGE_OFFSET (CONFIG_KERNEL_RAM_BASE_ADDRESS) 74 #else 75 #define PAGE_OFFSET (0) 76 #endif 77 78 #ifndef __ASSEMBLY__ 79 80 /* 把物理地址x转换为线性地址(即虚拟地址) */ 81 #define __va(x) ((void *)((unsigned long)(x) + PAGE_OFFSET)) 82 /* 把内核空间的线性地址x转换为物理地址 */ 83 #define __pa(x) ((unsigned long) (x) - PAGE_OFFSET) 84 85 /* 根据内核空间的线性地址得到其物理页框号(即第几页) */ 86 #define virt_to_pfn(kaddr) (__pa(kaddr) >> PAGE_SHIFT) 87 /* 根据物理页框号得到其线性地址 */ 88 #define pfn_to_virt(pfn) __va((pfn) << PAGE_SHIFT) 89 90 /* 根据用户空间的线性地址得到其物理页号 */ 91 #define virt_to_page(addr) (mem_map + (((unsigned long)(addr)-PAGE_OFFSET) >> PAGE_SHIFT)) 92 /* 根据物理页号得到其用户空间的线性地址 */ 93 #define page_to_virt(page) ((((page) - mem_map) << PAGE_SHIFT) + PAGE_OFFSET) 94 95 #ifndef page_to_phys 96 #define page_to_phys(page) ((dma_addr_t)page_to_pfn(page) << PAGE_SHIFT) 97 #endif 98 99 #define pfn_valid(pfn) ((pfn) < max_mapnr) 100 101 #define virt_addr_valid(kaddr) (((void *)(kaddr) >= (void *)PAGE_OFFSET) && \ 102 ((void *)(kaddr) < (void *)memory_end)) 103 104 #endif /* __ASSEMBLY__ */ 105 106 #include <asm-generic/memory_model.h> 107 #include <asm-generic/getorder.h> 108 109 #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()相关汇编代码如下:
1 __HEAD 2 ENTRY(startup_32) 3 /* test KEEP_SEGMENTS flag to see if the bootloader is asking 4 us to not reload segments */ 5 testb $(1<<6), BP_loadflags(%esi) 6 jnz 2f 7 8 /* ...... */ 9 10 /* 11 * 初始化页表。这会创建一个PDE和一个页表集,存放在__brk_base的上面。 12 * 变量_brk_end会被设置成指向第一个“安全”的区域。在虚拟地址0(为标识映射) 13 * 和PAGE_OFFSET处会创建映射。注意在这里栈还没有被设置 14 */ 15 default_entry: 16 #ifdef CONFIG_X86_PAE 17 18 /* 19 * 在PAE模式下swapper_pg_dir被静态定义包括足够多的条目以包含VMSPLIT选项(即最高的1, 20 * 2或3的条目)。标识映射通过把两个PGD条目指向第一个内核PMD条目来实现 21 * 注意在这一阶段,每个PMD或PTE的上半部分总是为0 22 */ 23 24 #define KPMDS (((-__PAGE_OFFSET) >> 30) & 3) /* 内核PMD的数量 */ 25 26 xorl %ebx,%ebx /* %ebx保持为0 */ 27 28 movl $pa(__brk_base), %edi 29 movl $pa(swapper_pg_pmd), %edx 30 movl $PTE_IDENT_ATTR, %eax 31 10: 32 leal PDE_IDENT_ATTR(%edi),%ecx /* 创建PMD条目 */ 33 movl %ecx,(%edx) /* 保存PMD条目 */ 34 /* 上半部分已经为0 */ 35 addl $8,%edx 36 movl $512,%ecx 37 11: 38 stosl 39 xchgl %eax,%ebx 40 stosl 41 xchgl %eax,%ebx 42 addl $0x1000,%eax 43 loop 11b 44 45 /* 46 * 终止条件:我们必须映射到end + MAPPING_BEYOND_END. 47 */ 48 movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp 49 cmpl %ebp,%eax 50 jb 10b 51 1: 52 addl $__PAGE_OFFSET, %edi 53 movl %edi, pa(_brk_end) 54 shrl $12, %eax 55 movl %eax, pa(max_pfn_mapped) 56 57 /* 对fixmap区域做初期的初始化 */ 58 movl $pa(swapper_pg_fixmap)+PDE_IDENT_ATTR,%eax 59 movl %eax,pa(swapper_pg_pmd+0x1000*KPMDS-8) 60 #else /* 非PAE */ 61 62 /* 得到开始目录项的索引 */ 63 page_pde_offset = (__PAGE_OFFSET >> 20); 64 /* 将基地址__brk_base转换成物理地址,传给edi */ 65 movl $pa(__brk_base), %edi 66 /* 将全局页目录表地址传给edx */ 67 movl $pa(swapper_pg_dir), %edx 68 movl $PTE_IDENT_ATTR, %eax 69 10: 70 leal PDE_IDENT_ATTR(%edi),%ecx /* 创建PDE条目 */ 71 movl %ecx,(%edx) /* 保存标识PDE条目 */ 72 movl %ecx,page_pde_offset(%edx) /* 保存内核PDE条目 */ 73 addl $4,%edx 74 movl $1024, %ecx 75 11: 76 stosl 77 addl $0x1000,%eax 78 loop 11b 79 /* 80 * 终止条件:我们必须映射到end + MAPPING_BEYOND_END. 81 */ 82 movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp 83 cmpl %ebp,%eax 84 jb 10b 85 addl $__PAGE_OFFSET, %edi 86 movl %edi, pa(_brk_end) 87 shrl $12, %eax 88 movl %eax, pa(max_pfn_mapped) 89 90 /* 对fixmap区域做初期的初始化 */ 91 movl $pa(swapper_pg_fixmap)+PDE_IDENT_ATTR,%eax 92 movl %eax,pa(swapper_pg_dir+0xffc) 93 #endif 94 jmp 3f 95 /* 96 * Non-boot CPU entry point; entered from trampoline.S 97 * We can't lgdt here, because lgdt itself uses a data segment, but 98 * we know the trampoline has already loaded the boot_gdt for us. 99 * 100 * If cpu hotplug is not supported then this code can go in init section 101 * which will be freed later 102 */ 103 104 __CPUINIT 105 106 #ifdef CONFIG_SMP 107 ENTRY(startup_32_smp) 108 cld 109 movl $(__BOOT_DS),%eax 110 movl %eax,%ds 111 movl %eax,%es 112 movl %eax,%fs 113 movl %eax,%gs 114 #endif /* CONFIG_SMP */ 115 3: 116 117 /* 118 * New page tables may be in 4Mbyte page mode and may 119 * be using the global pages. 120 * 121 * NOTE! If we are on a 486 we may have no cr4 at all! 122 * So we do not try to touch it unless we really have 123 * some bits in it to set. This won't work if the BSP 124 * implements cr4 but this AP does not -- very unlikely 125 * but be warned! The same applies to the pse feature 126 * if not equally supported. --macro 127 * 128 * NOTE! We have to correct for the fact that we're 129 * not yet offset PAGE_OFFSET.. 130 */ 131 #define cr4_bits pa(mmu_cr4_features) 132 movl cr4_bits,%edx 133 andl %edx,%edx 134 jz 6f 135 movl %cr4,%eax # 打开分页选项(PSE,PAE,...) 136 orl %edx,%eax 137 movl %eax,%cr4 138 139 btl $5, %eax # 检查PAE是否开启 140 jnc 6f 141 142 /* 检查扩展函数功能是否实现 */ 143 movl $0x80000000, %eax 144 cpuid 145 cmpl $0x80000000, %eax 146 jbe 6f 147 mov $0x80000001, %eax 148 cpuid 149 /* Execute Disable bit supported? */ 150 btl $20, %edx 151 jnc 6f 152 153 /* 设置EFER (Extended Feature Enable Register) */ 154 movl $0xc0000080, %ecx 155 rdmsr 156 157 btsl $11, %eax 158 /* 使更改生效 */ 159 wrmsr 160 161 6: 162 163 /* 164 * 开启分页功能 165 */ 166 movl pa(initial_page_table), %eax 167 movl %eax,%cr3 /* 设置页表指针:cr3控制寄存器保存的是目录表地址 */ 168 movl %cr0,%eax 169 orl $X86_CR0_PG,%eax 170 movl %eax,%cr0 /* ..同时设置分页(PG)标识位 */ 171 ljmp $__BOOT_CS,$1f /* 清除预读取和规格化%eip */ 172 1: 173 /* 设置栈指针 */ 174 lss stack_start,%esp 175 176 /* 177 * Initialize eflags. Some BIOS's leave bits like NT set. This would 178 * confuse the debugger if this code is traced. 179 * XXX - best to initialize before switching to protected mode. 180 */ 181 pushl $0 182 popfl 183 184 #ifdef CONFIG_SMP 185 cmpb $0, ready 186 jz 1f /* 初始的CPU要清除BSS */ 187 jmp checkCPUtype 188 1: 189 #endif /* CONFIG_SMP */
其中PTE_IDENT_ATTR等常量定义在arch/x86/include/asm/pgtable_types.h中,如下:
1 /* 2 * 初期标识映射的pte属性宏 3 */ 4 #ifdef CONFIG_X86_64 5 #define __PAGE_KERNEL_IDENT_LARGE_EXEC __PAGE_KERNEL_LARGE_EXEC 6 #else 7 /* 8 * For PDE_IDENT_ATTR include USER bit. As the PDE and PTE protection 9 * bits are combined, this will alow user to access the high address mapped 10 * VDSO in the presence of CONFIG_COMPAT_VDSO 11 */ 12 #define PTE_IDENT_ATTR 0x003 /* PRESENT+RW */ 13 #define PDE_IDENT_ATTR 0x067 /* PRESENT+RW+USER+DIRTY+ACCESSED */ 14 #define PGD_IDENT_ATTR 0x001 /* PRESENT (no other attributes) */ 15 #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()完成工作。如下:
1 #ifdef CONFIG_X86_32 2 # ifdef CONFIG_X86_PAE 3 # define MAX_ARCH_PFN (1ULL<<(36-PAGE_SHIFT)) 4 # else 5 # define MAX_ARCH_PFN (1ULL<<(32-PAGE_SHIFT)) 6 # endif 7 #else /* CONFIG_X86_32 */ 8 # define MAX_ARCH_PFN MAXMEM>>PAGE_SHIFT 9 #endif 10 11 /* 12 * 找出最大可用页帧号 13 */ 14 static unsigned long __init e820_end_pfn(unsigned long limit_pfn, unsigned type) 15 { 16 int i; 17 unsigned long last_pfn = 0; 18 unsigned long max_arch_pfn = MAX_ARCH_PFN; /* 4G地址空间对应的页面数 */ 19 /* 对e820中所有的内存块,其中e820为从bios中探测到的页面数存放处 */ 20 for (i = 0; i < e820.nr_map; i++) { 21 struct e820entry *ei = &e820.map[i]; /* 第i个物理页面块 */ 22 unsigned long start_pfn; 23 unsigned long end_pfn; 24 25 if (ei->type != type) /* 与要找的类型不匹配 */ 26 continue; 27 /* 起始地址和结束地址对应的页面帧号 */ 28 start_pfn = ei->addr >> PAGE_SHIFT; 29 end_pfn = (ei->addr + ei->size) >> PAGE_SHIFT; 30 31 if (start_pfn >= limit_pfn) 32 continue; 33 if (end_pfn > limit_pfn) { 34 /* 找到的结束页面帧号大于上限值时 */ 35 last_pfn = limit_pfn; 36 break; 37 } 38 if (end_pfn > last_pfn) /* 保存更新last_pfn */ 39 last_pfn = end_pfn; 40 } 41 /* 大于4G空间时 */ 42 if (last_pfn > max_arch_pfn) 43 last_pfn = max_arch_pfn; 44 /* 打印输出信息 */ 45 printk(KERN_INFO "last_pfn = %#lx max_arch_pfn = %#lx\n", 46 last_pfn, max_arch_pfn); 47 /* 返回最后一个页面帧号 */ 48 return last_pfn; 49 } 50 unsigned long __init e820_end_of_ram_pfn(void) 51 { 52 /* MAX_ARCH_PFN为4G空间 */ 53 return e820_end_pfn(MAX_ARCH_PFN, E820_RAM); 54 }
这里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中)。如下:
1 static unsigned int highmem_pages = -1; 2 3 /* ...... */ 4 5 /* 6 * 全部物理内存都在包含在低端空间中 7 */ 8 void __init lowmem_pfn_init(void) 9 { 10 /* max_low_pfn is 0, we already have early_res support */ 11 max_low_pfn = max_pfn; 12 13 if (highmem_pages == -1) 14 highmem_pages = 0; 15 #ifdef CONFIG_HIGHMEM 16 if (highmem_pages >= max_pfn) { 17 printk(KERN_ERR MSG_HIGHMEM_TOO_BIG, 18 pages_to_mb(highmem_pages), pages_to_mb(max_pfn)); 19 highmem_pages = 0; 20 } 21 if (highmem_pages) { 22 if (max_low_pfn - highmem_pages < 64*1024*1024/PAGE_SIZE) { 23 printk(KERN_ERR MSG_LOWMEM_TOO_SMALL, 24 pages_to_mb(highmem_pages)); 25 highmem_pages = 0; 26 } 27 max_low_pfn -= highmem_pages; 28 } 29 #else 30 if (highmem_pages) 31 printk(KERN_ERR "ignoring highmem size on non-highmem kernel!\n"); 32 #endif 33 } 34 35 #define MSG_HIGHMEM_TOO_SMALL \ 36 "only %luMB highmem pages available, ignoring highmem size of %luMB!\n" 37 38 #define MSG_HIGHMEM_TRIMMED \ 39 "Warning: only 4GB will be used. Use a HIGHMEM64G enabled kernel!\n" 40 /* 41 * 物理内存超出低端空间区:把它们放在高端地址空间中,或者通过启动时的highmem=x启动参数进行配置; 42 * 如果不配置,在这里进行设置大小 43 */ 44 void __init highmem_pfn_init(void) 45 { 46 /* MAXMEM_PFN为最大物理地址-(4M+4M+8K+128M); 47 所以低端空间的大小其实比我们说的896M低一些 */ 48 max_low_pfn = MAXMEM_PFN; 49 50 if (highmem_pages == -1) /* 高端内存页面数如果在开机没有设置 */ 51 highmem_pages = max_pfn - MAXMEM_PFN; /* 总页面数减去低端页面数 */ 52 /* 如果highmem_pages变量在启动项设置了,那么在这里就要进行这样的判断, 53 因为可能出现不一致的情况 */ 54 if (highmem_pages + MAXMEM_PFN < max_pfn) 55 max_pfn = MAXMEM_PFN + highmem_pages; 56 57 if (highmem_pages + MAXMEM_PFN > max_pfn) { 58 printk(KERN_WARNING MSG_HIGHMEM_TOO_SMALL, 59 pages_to_mb(max_pfn - MAXMEM_PFN), 60 pages_to_mb(highmem_pages)); 61 highmem_pages = 0; 62 } 63 #ifndef CONFIG_HIGHMEM 64 /* 最大可用内存是可直接寻址的 */ 65 printk(KERN_WARNING "Warning only %ldMB will be used.\n", MAXMEM>>20); 66 if (max_pfn > MAX_NONPAE_PFN) 67 printk(KERN_WARNING "Use a HIGHMEM64G enabled kernel.\n"); 68 else 69 printk(KERN_WARNING "Use a HIGHMEM enabled kernel.\n"); 70 max_pfn = MAXMEM_PFN; 71 #else /* !CONFIG_HIGHMEM */ 72 #ifndef CONFIG_HIGHMEM64G 73 /* 在没有配置64G的情况下,内存的大小不能超过4G */ 74 if (max_pfn > MAX_NONPAE_PFN) { 75 max_pfn = MAX_NONPAE_PFN; 76 printk(KERN_WARNING MSG_HIGHMEM_TRIMMED); 77 } 78 #endif /* !CONFIG_HIGHMEM64G */ 79 #endif /* !CONFIG_HIGHMEM */ 80 } 81 82 /* 83 * 确定低端和高端内存的页面帧号范围: 84 */ 85 void __init find_low_pfn_range(void) 86 { 87 /* 会更新max_pfn */ 88 89 /* 当物理内存本来就小于低端空间最大页框数时, 90 直接没有高端地址映射 */ 91 if (max_pfn <= MAXMEM_PFN) 92 lowmem_pfn_init(); 93 else /* 这是一般PC机的运行流程,存在高端映射 */ 94 highmem_pfn_init(); 95 }
分析:
(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 unsigned long __init_refok init_memory_mapping(unsigned long start, 2 unsigned long end) 3 { 4 unsigned long page_size_mask = 0; 5 unsigned long start_pfn, end_pfn; 6 unsigned long ret = 0; 7 unsigned long pos; 8 9 struct map_range mr[NR_RANGE_MR]; 10 int nr_range, i; 11 int use_pse, use_gbpages; 12 13 printk(KERN_INFO "init_memory_mapping: %016lx-%016lx\n", start, end); 14 15 #if defined(CONFIG_DEBUG_PAGEALLOC) || defined(CONFIG_KMEMCHECK) 16 /* 17 * For CONFIG_DEBUG_PAGEALLOC, identity mapping will use small pages. 18 * This will simplify cpa(), which otherwise needs to support splitting 19 * large pages into small in interrupt context, etc. 20 */ 21 use_pse = use_gbpages = 0; 22 #else 23 use_pse = cpu_has_pse; 24 use_gbpages = direct_gbpages; 25 #endif 26 /* 定义了X86_PAE模式后进行调用 */ 27 set_nx(); 28 if (nx_enabled) 29 printk(KERN_INFO "NX (Execute Disable) protection: active\n"); 30 31 /* 激活PSE(如果可用) */ 32 if (cpu_has_pse) 33 set_in_cr4(X86_CR4_PSE); 34 35 /* 激活PGE(如果可用) */ 36 if (cpu_has_pge) { 37 set_in_cr4(X86_CR4_PGE); 38 __supported_pte_mask |= _PAGE_GLOBAL; 39 } 40 /* page_size_mask在这里更新,在后面设置页表时用到 */ 41 if (use_gbpages) 42 page_size_mask |= 1 << PG_LEVEL_1G; 43 if (use_pse) 44 page_size_mask |= 1 << PG_LEVEL_2M; 45 46 memset(mr, 0, sizeof(mr)); 47 nr_range = 0; 48 49 /* 作为初始页面帧号值,如果没有大内存页对齐 */ 50 start_pfn = start >> PAGE_SHIFT; /* 在setup函数中调用时,这里为0 */ 51 pos = start_pfn << PAGE_SHIFT; /* pos为0 */ 52 #ifdef CONFIG_X86_32 53 /* 54 * Don't use a large page for the first 2/4MB of memory 55 * because there are often fixed size MTRRs in there 56 * and overlapping MTRRs into large pages can cause 57 * slowdowns. 58 */ 59 if (pos == 0) /* end_pfn的大小为1k,也就是4M大小的内存 */ 60 end_pfn = 1<<(PMD_SHIFT - PAGE_SHIFT); 61 else 62 end_pfn = ((pos + (PMD_SIZE - 1))>>PMD_SHIFT) 63 << (PMD_SHIFT - PAGE_SHIFT); 64 #else /* CONFIG_X86_64 */ 65 end_pfn = ((pos + (PMD_SIZE - 1)) >> PMD_SHIFT) 66 << (PMD_SHIFT - PAGE_SHIFT); 67 #endif 68 if (end_pfn > (end >> PAGE_SHIFT)) 69 end_pfn = end >> PAGE_SHIFT; 70 if (start_pfn < end_pfn) { /* 4M空间将这个区间存放在mr数组中 */ 71 nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 0); 72 pos = end_pfn << PAGE_SHIFT; 73 } 74 75 /* 大内存页(2M)范围:对齐到PMD,换算成页面的多少 */ 76 start_pfn = ((pos + (PMD_SIZE - 1))>>PMD_SHIFT) 77 << (PMD_SHIFT - PAGE_SHIFT); 78 #ifdef CONFIG_X86_32 79 /* 这里的结束地址设置为调用的结束位页面数,也就是 80 所有的物理页面数 */ 81 end_pfn = (end>>PMD_SHIFT) << (PMD_SHIFT - PAGE_SHIFT); 82 #else /* CONFIG_X86_64 */ 83 end_pfn = ((pos + (PUD_SIZE - 1))>>PUD_SHIFT) 84 << (PUD_SHIFT - PAGE_SHIFT); 85 if (end_pfn > ((end>>PMD_SHIFT)<<(PMD_SHIFT - PAGE_SHIFT))) 86 end_pfn = ((end>>PMD_SHIFT)<<(PMD_SHIFT - PAGE_SHIFT)); 87 #endif 88 89 if (start_pfn < end_pfn) { 90 /* 将这段内存放入mr中,保存后面用到 */ 91 nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 92 page_size_mask & (1<<PG_LEVEL_2M)); /* 这里保证了运用PSE时为2M页面而不是PSE时, 93 仍然为4K页面(上面的按位或和这里的按位与) */ 94 pos = end_pfn << PAGE_SHIFT; /* 更新pos */ 95 } 96 97 #ifdef CONFIG_X86_64 98 /* 大内存页(1G)范围 */ 99 start_pfn = ((pos + (PUD_SIZE - 1))>>PUD_SHIFT) 100 << (PUD_SHIFT - PAGE_SHIFT); 101 end_pfn = (end >> PUD_SHIFT) << (PUD_SHIFT - PAGE_SHIFT); 102 if (start_pfn < end_pfn) { 103 nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 104 page_size_mask & 105 ((1<<PG_LEVEL_2M)|(1<<PG_LEVEL_1G))); 106 pos = end_pfn << PAGE_SHIFT; 107 } 108 109 /* 尾部不是大内存页(1G)对齐 */ 110 start_pfn = ((pos + (PMD_SIZE - 1))>>PMD_SHIFT) 111 << (PMD_SHIFT - PAGE_SHIFT); 112 end_pfn = (end >> PMD_SHIFT) << (PMD_SHIFT - PAGE_SHIFT); 113 if (start_pfn < end_pfn) { 114 nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 115 page_size_mask & (1<<PG_LEVEL_2M)); 116 pos = end_pfn << PAGE_SHIFT; 117 } 118 #endif 119 120 /* 尾部不是大内存页(2M)对齐 */ 121 start_pfn = pos>>PAGE_SHIFT; 122 end_pfn = end>>PAGE_SHIFT; 123 nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 0); 124 125 /* 合并相同页面大小的连续的页面 */ 126 for (i = 0; nr_range > 1 && i < nr_range - 1; i++) { 127 unsigned long old_start; 128 if (mr[i].end != mr[i+1].start || 129 mr[i].page_size_mask != mr[i+1].page_size_mask) 130 continue; 131 /* move it */ 132 old_start = mr[i].start; 133 memmove(&mr[i], &mr[i+1], 134 (nr_range - 1 - i) * sizeof(struct map_range)); 135 mr[i--].start = old_start; 136 nr_range--; 137 } 138 /* 打印相关信息 */ 139 for (i = 0; i < nr_range; i++) 140 printk(KERN_DEBUG " %010lx - %010lx page %s\n", 141 mr[i].start, mr[i].end, 142 (mr[i].page_size_mask & (1<<PG_LEVEL_1G))?"1G":( 143 (mr[i].page_size_mask & (1<<PG_LEVEL_2M))?"2M":"4k")); 144 145 /* 146 * 为内核直接映射的页表查找空间 147 * 以后我们应该在内存映射的本地节点分配这些页表。不幸的是目前这需要在 148 * 查找到节点之前来做 149 */ 150 if (!after_bootmem) /*如果内存启动分配器没有建立,则直接从e820.map中找到合适的 151 连续内存,找到存放页表的空间首地址为e820_table_start */ 152 find_early_table_space(end, use_pse, use_gbpages); 153 154 #ifdef CONFIG_X86_32 155 for (i = 0; i < nr_range; i++) /* 对每个保存的区域设置页表映射 */ 156 kernel_physical_mapping_init(mr[i].start, mr[i].end, 157 mr[i].page_size_mask); 158 ret = end; 159 #else /* CONFIG_X86_64 */ 160 for (i = 0; i < nr_range; i++) 161 ret = kernel_physical_mapping_init(mr[i].start, mr[i].end, 162 mr[i].page_size_mask); 163 #endif 164 165 #ifdef CONFIG_X86_32 166 /* 对高端内存固定区域建立映射 */ 167 early_ioremap_page_table_range_init(); 168 /* 放入CR3寄存器 */ 169 load_cr3(swapper_pg_dir); 170 #endif 171 172 #ifdef CONFIG_X86_64 173 if (!after_bootmem && !start) { 174 pud_t *pud; 175 pmd_t *pmd; 176 177 mmu_cr4_features = read_cr4(); 178 179 /* 180 * _brk_end cannot change anymore, but it and _end may be 181 * located on different 2M pages. cleanup_highmap(), however, 182 * can only consider _end when it runs, so destroy any 183 * mappings beyond _brk_end here. 184 */ 185 pud = pud_offset(pgd_offset_k(_brk_end), _brk_end); 186 pmd = pmd_offset(pud, _brk_end - 1); 187 while (++pmd <= pmd_offset(pud, (unsigned long)_end - 1)) 188 pmd_clear(pmd); 189 } 190 #endif 191 __flush_tlb_all(); /* 刷新寄存器 */ 192 /* 将分配给建立页表机制的内存空间保留 */ 193 if (!after_bootmem && e820_table_end > e820_table_start) 194 reserve_early(e820_table_start << PAGE_SHIFT, 195 e820_table_end << PAGE_SHIFT, "PGTABLE"); 196 197 if (!after_bootmem) 198 early_memtest(start, end); 199 200 return ret >> PAGE_SHIFT; 201 }
分析:
(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 unsigned long __initdata e820_table_start; 2 unsigned long __meminitdata e820_table_end; 3 unsigned long __meminitdata e820_table_top; 4 5 int after_bootmem; 6 7 int direct_gbpages 8 #ifdef CONFIG_DIRECT_GBPAGES 9 = 1 10 #endif 11 ; 12 13 /* 查找页表需要的空间 */ 14 static void __init find_early_table_space(unsigned long end, int use_pse, 15 int use_gbpages) 16 { 17 unsigned long puds, pmds, ptes, tables, start; 18 /* 计算需要用到多少pud,当没有pud存在的情况下pud=pgd */ 19 puds = (end + PUD_SIZE - 1) >> PUD_SHIFT; 20 tables = roundup(puds * sizeof(pud_t), PAGE_SIZE); 21 22 if (use_gbpages) { 23 unsigned long extra; 24 25 extra = end - ((end>>PUD_SHIFT) << PUD_SHIFT); 26 pmds = (extra + PMD_SIZE - 1) >> PMD_SHIFT; 27 } else 28 pmds = (end + PMD_SIZE - 1) >> PMD_SHIFT; 29 /* 计算映射所有内存所要求的所有pmd的个数 */ 30 tables += roundup(pmds * sizeof(pmd_t), PAGE_SIZE); 31 32 if (use_pse) { 33 unsigned long extra; 34 35 extra = end - ((end>>PMD_SHIFT) << PMD_SHIFT); 36 #ifdef CONFIG_X86_32 37 extra += PMD_SIZE; 38 #endif 39 ptes = (extra + PAGE_SIZE - 1) >> PAGE_SHIFT; 40 } else /* 计算所需要的pte个数 */ 41 ptes = (end + PAGE_SIZE - 1) >> PAGE_SHIFT; 42 43 tables += roundup(ptes * sizeof(pte_t), PAGE_SIZE); 44 45 #ifdef CONFIG_X86_32 46 /* for fixmap */ 47 /* 加上固定内存映射区的页表数量 */ 48 tables += roundup(__end_of_fixed_addresses * sizeof(pte_t), PAGE_SIZE); 49 #endif 50 51 /* 52 * RED-PEN putting page tables only on node 0 could 53 * cause a hotspot and fill up ZONE_DMA. The page tables 54 * need roughly 0.5KB per GB. 55 */ 56 #ifdef CONFIG_X86_32 57 start = 0x7000; /* 页表存放的开始地址,这里为什么从这里开始? */ 58 #else 59 start = 0x8000; 60 #endif 61 /* 从e820.map中找到连续的足够大小的内存来存放用于映射的页表, 62 返回起始地址 */ 63 e820_table_start = find_e820_area(start, max_pfn_mapped<<PAGE_SHIFT, 64 tables, PAGE_SIZE); 65 if (e820_table_start == -1UL) 66 panic("Cannot find space for the kernel page tables"); 67 /* 将页表起始地址的物理页面帧号保存到相关的全局变量中 */ 68 e820_table_start >>= PAGE_SHIFT; 69 e820_table_end = e820_table_start; 70 e820_table_top = e820_table_start + (tables >> PAGE_SHIFT); 71 72 printk(KERN_DEBUG "kernel direct mapping tables up to %lx @ %lx-%lx\n", 73 end, e820_table_start << PAGE_SHIFT, e820_table_top << PAGE_SHIFT); 74 } 75 76 struct map_range { 77 unsigned long start; 78 unsigned long end; 79 unsigned page_size_mask; 80 }; 81 82 #ifdef CONFIG_X86_32 83 #define NR_RANGE_MR 3 84 #else /* CONFIG_X86_64 */ 85 #define NR_RANGE_MR 5 86 #endif 87 /* 将要映射的页面范围保存到mr数组中 */ 88 static int __meminit save_mr(struct map_range *mr, int nr_range, 89 unsigned long start_pfn, unsigned long end_pfn, 90 unsigned long page_size_mask) 91 { 92 if (start_pfn < end_pfn) { 93 if (nr_range >= NR_RANGE_MR) 94 panic("run out of range for init_memory_mapping\n"); 95 mr[nr_range].start = start_pfn<<PAGE_SHIFT; 96 mr[nr_range].end = end_pfn<<PAGE_SHIFT; 97 mr[nr_range].page_size_mask = page_size_mask; 98 nr_range++; 99 } 100 101 return nr_range; 102 }
分析:
(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 unsigned long __init 2 kernel_physical_mapping_init(unsigned long start, 3 unsigned long end, 4 unsigned long page_size_mask) 5 { 6 int use_pse = page_size_mask == (1<<PG_LEVEL_2M); 7 unsigned long start_pfn, end_pfn; 8 pgd_t *pgd_base = swapper_pg_dir; 9 int pgd_idx, pmd_idx, pte_ofs; 10 unsigned long pfn; 11 pgd_t *pgd; 12 pmd_t *pmd; 13 pte_t *pte; 14 unsigned pages_2m, pages_4k; 15 int mapping_iter; 16 /* 得到要映射的起始地址和终止地址所在页在页帧号 */ 17 start_pfn = start >> PAGE_SHIFT; 18 end_pfn = end >> PAGE_SHIFT; 19 20 /* 21 * First iteration will setup identity mapping using large/small pages 22 * based on use_pse, with other attributes same as set by 23 * the early code in head_32.S 24 * 25 * Second iteration will setup the appropriate attributes (NX, GLOBAL..) 26 * as desired for the kernel identity mapping. 27 * 28 * This two pass mechanism conforms to the TLB app note which says: 29 * 30 * "Software should not write to a paging-structure entry in a way 31 * that would change, for any linear address, both the page size 32 * and either the page frame or attributes." 33 */ 34 mapping_iter = 1; 35 36 if (!cpu_has_pse) 37 use_pse = 0; 38 39 repeat: 40 pages_2m = pages_4k = 0; 41 pfn = start_pfn; 42 /* 返回页框在PGD表中的索引 */ 43 pgd_idx = pgd_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET); 44 pgd = pgd_base + pgd_idx; 45 for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) { 46 pmd = one_md_table_init(pgd); /* 创建该pgd目录项指向的pmd表 */ 47 48 if (pfn >= end_pfn) 49 continue; 50 #ifdef CONFIG_X86_PAE 51 /* 三级映射需要设置pmd,因此得到页框在PMD表中的索引 */ 52 pmd_idx = pmd_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET); 53 pmd += pmd_idx; 54 #else 55 pmd_idx = 0; /* 两级映射则无需设置 */ 56 #endif 57 for (; pmd_idx < PTRS_PER_PMD && pfn < end_pfn; 58 pmd++, pmd_idx++) { 59 unsigned int addr = pfn * PAGE_SIZE + PAGE_OFFSET; 60 61 /* 62 * 如果可能,用大页面来映射,否则创建正常大小的页表: 63 */ 64 if (use_pse) { 65 unsigned int addr2; 66 pgprot_t prot = PAGE_KERNEL_LARGE; 67 /* 68 * first pass will use the same initial 69 * identity mapping attribute + _PAGE_PSE. 70 */ 71 pgprot_t init_prot = 72 __pgprot(PTE_IDENT_ATTR | 73 _PAGE_PSE); 74 75 addr2 = (pfn + PTRS_PER_PTE-1) * PAGE_SIZE + 76 PAGE_OFFSET + PAGE_SIZE-1; 77 78 if (is_kernel_text(addr) || 79 is_kernel_text(addr2)) 80 prot = PAGE_KERNEL_LARGE_EXEC; 81 82 pages_2m++; 83 if (mapping_iter == 1) 84 set_pmd(pmd, pfn_pmd(pfn, init_prot)); 85 else 86 set_pmd(pmd, pfn_pmd(pfn, prot)); 87 88 pfn += PTRS_PER_PTE; 89 continue; 90 } 91 pte = one_page_table_init(pmd); /* 返回PMD中第一个PTE */ 92 /* PTE的索引 */ 93 pte_ofs = pte_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET); 94 pte += pte_ofs; /* 定位带具体的pte */ 95 for (; pte_ofs < PTRS_PER_PTE && pfn < end_pfn; 96 pte++, pfn++, pte_ofs++, addr += PAGE_SIZE) { 97 pgprot_t prot = PAGE_KERNEL; 98 /* 99 * first pass will use the same initial 100 * identity mapping attribute. 101 */ 102 pgprot_t init_prot = __pgprot(PTE_IDENT_ATTR); 103 104 if (is_kernel_text(addr)) 105 prot = PAGE_KERNEL_EXEC; 106 107 pages_4k++; /* 没有PSE */ 108 /* 设置页表,根据MAPPING_ITER变量的不同 109 对表设置不同的属性 */ 110 if (mapping_iter == 1) /* 第一次迭代,属性设置都一样 */ 111 set_pte(pte, pfn_pte(pfn, init_prot)); 112 else /* 设置为具体的属性 */ 113 set_pte(pte, pfn_pte(pfn, prot)); 114 } 115 } 116 } 117 if (mapping_iter == 1) { 118 /* 119 * 只在第一次迭代中更新直接映射页的数量 120 */ 121 update_page_count(PG_LEVEL_2M, pages_2m); 122 update_page_count(PG_LEVEL_4K, pages_4k); 123 124 /* 125 * local global flush tlb, which will flush the previous 126 * mappings present in both small and large page TLB's. 127 */ 128 __flush_tlb_all(); 129 130 /* 131 * 第二次迭代将设置实际的PTE属性 132 */ 133 mapping_iter = 2; 134 goto repeat; 135 } 136 return 0; /* 迭代两后返回 */ 137 } 138 139 static pmd_t * __init one_md_table_init(pgd_t *pgd) 140 { 141 pud_t *pud; 142 pmd_t *pmd_table; 143 144 #ifdef CONFIG_X86_PAE 145 /* 启用了PAE,需要三级映射,创建PMD表 */ 146 if (!(pgd_val(*pgd) & _PAGE_PRESENT)) { 147 if (after_bootmem) 148 pmd_table = (pmd_t *)alloc_bootmem_pages(PAGE_SIZE); 149 else 150 pmd_table = (pmd_t *)alloc_low_page(); 151 paravirt_alloc_pmd(&init_mm, __pa(pmd_table) >> PAGE_SHIFT); 152 /* 设置PGD,将对应的PGD项设置为PMD表 */ 153 set_pgd(pgd, __pgd(__pa(pmd_table) | _PAGE_PRESENT)); 154 pud = pud_offset(pgd, 0); 155 BUG_ON(pmd_table != pmd_offset(pud, 0)); 156 157 return pmd_table; 158 } 159 #endif 160 /* 非PAE模式:只需二级映射,直接返回原来pgd地址 */ 161 pud = pud_offset(pgd, 0); 162 pmd_table = pmd_offset(pud, 0); 163 164 return pmd_table; 165 } 166 167 static pte_t * __init one_page_table_init(pmd_t *pmd) 168 { 169 if (!(pmd_val(*pmd) & _PAGE_PRESENT)) { 170 pte_t *page_table = NULL; 171 172 if (after_bootmem) { 173 #if defined(CONFIG_DEBUG_PAGEALLOC) || defined(CONFIG_KMEMCHECK) 174 page_table = (pte_t *) alloc_bootmem_pages(PAGE_SIZE); 175 #endif 176 if (!page_table) 177 page_table = 178 (pte_t *)alloc_bootmem_pages(PAGE_SIZE); 179 } else /* 如果启动分配器还没有建立,那么 180 从刚才分配建立的表中分配空间 */ 181 page_table = (pte_t *)alloc_low_page(); 182 183 paravirt_alloc_pte(&init_mm, __pa(page_table) >> PAGE_SHIFT); 184 /* 设置PMD,将对应的PMD项设置为页表 */ 185 set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE)); 186 BUG_ON(page_table != pte_offset_kernel(pmd, 0)); 187 } 188 189 return pte_offset_kernel(pmd, 0); 190 } 191 192 static inline int is_kernel_text(unsigned long addr) 193 { 194 if (addr >= PAGE_OFFSET && addr <= (unsigned long)__init_end) 195 return 1; 196 return 0; 197 }
分析:
(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 void __init early_ioremap_page_table_range_init(void) 2 { 3 pgd_t *pgd_base = swapper_pg_dir; 4 unsigned long vaddr, end; 5 6 /* 7 * 固定映射,只是创建页表结构,并不建立实际映射。实际映射将由set_fixmap()来完成: 8 */ 9 vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK; 10 end = (FIXADDR_TOP + PMD_SIZE - 1) & PMD_MASK; 11 /* 这里是对临时映射区域进行映射而为页表等分配了空间, 12 但是没有建立实际的映射 */ 13 page_table_range_init(vaddr, end, pgd_base); 14 /* 置变量after_paging_init为1,表示启动了分页机制 */ 15 early_ioremap_reset(); 16 } 17 18 static void __init 19 page_table_range_init(unsigned long start, unsigned long end, pgd_t *pgd_base) 20 { 21 int pgd_idx, pmd_idx; 22 unsigned long vaddr; 23 pgd_t *pgd; 24 pmd_t *pmd; 25 pte_t *pte = NULL; 26 27 vaddr = start; 28 pgd_idx = pgd_index(vaddr); 29 pmd_idx = pmd_index(vaddr); 30 pgd = pgd_base + pgd_idx; 31 32 for ( ; (pgd_idx < PTRS_PER_PGD) && (vaddr != end); pgd++, pgd_idx++) { 33 pmd = one_md_table_init(pgd); 34 pmd = pmd + pmd_index(vaddr); 35 for (; (pmd_idx < PTRS_PER_PMD) && (vaddr != end); 36 pmd++, pmd_idx++) { 37 /* early fixmap可能对临时映射区中的页表项已经分配了页表, 38 为使页表分配的空间连续,需要对临时映射区的页表指定区间重新分配 */ 39 /* 在这里已经对pte进行了分配和初始化 */ 40 pte = page_table_kmap_check(one_page_table_init(pmd), 41 pmd, vaddr, pte); 42 43 vaddr += PMD_SIZE; 44 } 45 pmd_idx = 0; 46 } 47 }
分析:
(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要进行寻址的时候,无论在内核空间,还是在用户空间,都会通过这个页表来进行映射。对于这个函数,内核把整个物理内存空间都映射完了,当用户空间的进程要使用物理内存时,岂不是不能做相应的映射了?其实不会的,内核只是做了映射,映射不代表使用,这样做是内核为了方便管理内存而已。
PART5:分页机制和管理区初始化
1、初始化启动内存分配器
在内存子系统初始化以前,即boot阶段也需要进行内存管理,启动内存分配器是专为此而设计的。linux启动内存分配器是在伙伴系统、slab机制实现之前,为满足内核中内存的分配而建立的。本身的机制比较简单,使用位图来进行标志分配和释放。arch/x86/kernel/setup.c:setup_arch()在用init_memory_mapping(0, max_low_pfn<<PAGE_SHIFT)建立完内核页表之后,就会调用arch/x86/mm/init_32.c:initmem_init(0, max_pfn)启动bootmem内存分配器。如下:
1 #ifndef CONFIG_NEED_MULTIPLE_NODES 2 void __init initmem_init(unsigned long start_pfn, 3 unsigned long end_pfn) 4 { 5 #ifdef CONFIG_HIGHMEM 6 highstart_pfn = highend_pfn = max_pfn; 7 if (max_pfn > max_low_pfn) 8 highstart_pfn = max_low_pfn; 9 /* 注册内存活动区 */ 10 e820_register_active_regions(0, 0, highend_pfn); 11 sparse_memory_present_with_active_regions(0); 12 printk(KERN_NOTICE "%ldMB HIGHMEM available.\n", 13 pages_to_mb(highend_pfn - highstart_pfn)); 14 num_physpages = highend_pfn; 15 /* 计算高端内存地址 */ 16 high_memory = (void *) __va(highstart_pfn * PAGE_SIZE - 1) + 1; 17 #else 18 e820_register_active_regions(0, 0, max_low_pfn); 19 sparse_memory_present_with_active_regions(0); 20 num_physpages = max_low_pfn; 21 high_memory = (void *) __va(max_low_pfn * PAGE_SIZE - 1) + 1; 22 #endif 23 #ifdef CONFIG_FLATMEM 24 max_mapnr = num_physpages; 25 #endif 26 __vmalloc_start_set = true; 27 28 printk(KERN_NOTICE "%ldMB LOWMEM available.\n", 29 pages_to_mb(max_low_pfn)); 30 31 setup_bootmem_allocator(); /* 启动内存分配器 */ 32 } 33 #endif /* !CONFIG_NEED_MULTIPLE_NODES */
主要工作是调用e820_register_active_regions()在节点0上注册内存活动区,然后调用setup_bootmem_allocator()建立启动内存分配器。Linux的内存活动区域其实就是全局变量e820中的内存块做了相关检查和处理后的区域,它会在管理区初始化等地方被用到。注册时,要根据是否配置了高端内存来决定活动的区的终止地址。
函数e820_register_active_regions()在arch/x86/kernel/e820.c中,它扫描e820内存图,并在一个节点nid上注册活动区。如下:
1 /* 扫描e820内存图,并在一个节点上注册活动区 */ 2 void __init e820_register_active_regions(int nid, unsigned long start_pfn, 3 unsigned long last_pfn) 4 { 5 unsigned long ei_startpfn; 6 unsigned long ei_endpfn; 7 int i; 8 9 for (i = 0; i < e820.nr_map; i++) 10 /* 从全局变量e820中查找活动区 */ 11 if (e820_find_active_region(&e820.map[i], 12 start_pfn, last_pfn, 13 &ei_startpfn, &ei_endpfn)) 14 /* 添加查找到的活动区 */ 15 add_active_range(nid, ei_startpfn, ei_endpfn); 16 } 17 18 /* 19 * 在start_pfn到last_pfn的地址范围内查找一个活动区,并在ei_startpfn和ei_endpfn中返回 20 * 这个e820内存块的范围 21 */ 22 int __init e820_find_active_region(const struct e820entry *ei, 23 unsigned long start_pfn, 24 unsigned long last_pfn, 25 unsigned long *ei_startpfn, 26 unsigned long *ei_endpfn) 27 { 28 u64 align = PAGE_SIZE; 29 30 *ei_startpfn = round_up(ei->addr, align) >> PAGE_SHIFT; 31 *ei_endpfn = round_down(ei->addr + ei->size, align) >> PAGE_SHIFT; 32 33 /* 跳过内存图中比一个页面还小的各个内存块 */ 34 if (*ei_startpfn >= *ei_endpfn) 35 return 0; 36 37 /* 如果内存图中的所有内存块都在节点范围之外,则跳过 */ 38 if (ei->type != E820_RAM || *ei_endpfn <= start_pfn || 39 *ei_startpfn >= last_pfn) 40 return 0; 41 42 /* 检查是否有重叠 */ 43 if (*ei_startpfn < start_pfn) 44 *ei_startpfn = start_pfn; 45 if (*ei_endpfn > last_pfn) 46 *ei_endpfn = last_pfn; 47 48 return 1; 49 }
主要的工作是在start_pfn到last_pfn的地址范围内,从e820内存图的各内存块中查找一个物理活动区,若找到,则把其物理地址范围保存到ei_startpfn和ei_endpfn中,然后调用mm/page_alloc.c中的add_active_range()函数在nid节点上注册这块活动区。如下:
1 /* 添加活动区域,需要对原有的进行检查 */ 2 void __init add_active_range(unsigned int nid, unsigned long start_pfn, 3 unsigned long end_pfn) 4 { 5 int i; 6 7 mminit_dprintk(MMINIT_TRACE, "memory_register", 8 "Entering add_active_range(%d, %#lx, %#lx) " 9 "%d entries of %d used\n", 10 nid, start_pfn, end_pfn, 11 nr_nodemap_entries, MAX_ACTIVE_REGIONS); 12 13 mminit_validate_memmodel_limits(&start_pfn, &end_pfn); 14 15 /* 如果可能,与存在的活动内存区合并 */ 16 for (i = 0; i < nr_nodemap_entries; i++) { 17 if (early_node_map[i].nid != nid) 18 continue; 19 20 /* 如果一个存在的活动区包含这个要添加的新区,则跳过 */ 21 if (start_pfn >= early_node_map[i].start_pfn && 22 end_pfn <= early_node_map[i].end_pfn) 23 return; 24 25 /* 如果合适,则向前合并 */ 26 if (start_pfn <= early_node_map[i].end_pfn && 27 end_pfn > early_node_map[i].end_pfn) { 28 early_node_map[i].end_pfn = end_pfn; 29 return; 30 } 31 32 /* 如果合适,则向后合并 */ 33 if (start_pfn < early_node_map[i].end_pfn && 34 end_pfn >= early_node_map[i].start_pfn) { 35 early_node_map[i].start_pfn = start_pfn; 36 return; 37 } 38 } 39 40 /* 检查early_node_map是否足够大 */ 41 if (i >= MAX_ACTIVE_REGIONS) { 42 printk(KERN_CRIT "More than %d memory regions, truncating\n", 43 MAX_ACTIVE_REGIONS); 44 return; 45 } 46 47 early_node_map[i].nid = nid; 48 early_node_map[i].start_pfn = start_pfn; 49 early_node_map[i].end_pfn = end_pfn; 50 nr_nodemap_entries = i + 1; 51 }
回到arch/x86/mm/init_32.c:initmem_init(),最后是调用arch/x86/mm/init_32.c:setup_bootmem_allocator()建立内核引导时的启动内存分配器。在建立启动内存分配器的时候,会涉及到保留内存。也就是说,当分配器进行内存分配时,之前保留给页表、分配器本身(用于映射的位图)、io的这些保留内存就不能再分配了。linux中对保留内存空间的部分用下列数据结构表示,在arch/x86/kernel/e820.c中:
1 /* 2 * Early reserved memory areas. 3 */ 4 #define MAX_EARLY_RES 20 /* 保留空间最大块数 */ 5 6 struct early_res { /* 保留空间结构 */ 7 u64 start, end; 8 char name[16]; 9 char overlap_ok; 10 }; 11 /* 保留内存空间全局变量 */ 12 static struct early_res early_res[MAX_EARLY_RES] __initdata = { 13 { 0, PAGE_SIZE, "BIOS data page" }, /* BIOS data page */ 14 {} 15 }; 16 bootmem分配器的数据结构bootmem_data_t用于管理启动内存的分配、释放等,在include/linux/bootmem.h中,如下: 17 /* 用于bootmem分配器的节点数据结构 */ 18 typedef struct bootmem_data { 19 unsigned long node_min_pfn; 20 unsigned long node_low_pfn; 21 void *node_bootmem_map; 22 unsigned long last_end_off; 23 unsigned long hint_idx; 24 struct list_head list; 25 } bootmem_data_t;
这些域分别为存放bootmem位图的第一个页面(即内核映象结束处的第一个页面)、低端内存最大页面号(物理内存的顶点,最高不超过896MB)、位图(各个位代表节点上的所有物理内存页,包括洞)、前一次分配的最后一个字节相对于last_pos的位移量、hint_idx为前一次分配的最后一个页面号、list是用于内存分配的链表。注意在内存节点pg_data_t数据结构中,用bdata指针批向了这个bootmem分配器的数据结构。
全局链表定义可在mm/bootmeme.c中找到,如下:
static struct list_head bdata_list __initdata = LIST_HEAD_INIT(bdata_list);
启动分配器的建立主要的流程为初始化映射位图、活动内存区的映射位置0(表示可用)、保留内存区域处理,其中保留区存放在上面介绍的全局数组中,这里只是将分配器中对应映射位图值1,表示已经分配。核心函数是arch/x86/mm/init_32.c:setup_bootmem_allocator(),以及setup_node_bootmem()。如下:
1 void __init setup_bootmem_allocator(void) 2 { 3 int nodeid; 4 unsigned long bootmap_size, bootmap; 5 /* 6 * 初始化引导时的内存分配器(只是低端内存区): 7 */ 8 /* 计算所需要的映射页面大小一个字节一位,所以需要对总的页面大小除以8 */ 9 bootmap_size = bootmem_bootmap_pages(max_low_pfn)<<PAGE_SHIFT; 10 /* 从e820中查找一个合适的内存块 */ 11 bootmap = find_e820_area(0, max_pfn_mapped<<PAGE_SHIFT, bootmap_size, 12 PAGE_SIZE); 13 if (bootmap == -1L) 14 panic("Cannot find bootmem map of size %ld\n", bootmap_size); 15 /* 将用于位图映射的页面保留 */ 16 reserve_early(bootmap, bootmap + bootmap_size, "BOOTMAP"); 17 18 printk(KERN_INFO " mapped low ram: 0 - %08lx\n", 19 max_pfn_mapped<<PAGE_SHIFT); 20 printk(KERN_INFO " low ram: 0 - %08lx\n", max_low_pfn<<PAGE_SHIFT); 21 /* 扫描每个在线节点 */ 22 for_each_online_node(nodeid) { 23 unsigned long start_pfn, end_pfn; 24 25 #ifdef CONFIG_NEED_MULTIPLE_NODES 26 /* 计算出当前节点的起始地址和终止地址 */ 27 start_pfn = node_start_pfn[nodeid]; 28 end_pfn = node_end_pfn[nodeid]; 29 if (start_pfn > max_low_pfn) 30 continue; 31 if (end_pfn > max_low_pfn) 32 end_pfn = max_low_pfn; 33 #else 34 start_pfn = 0; 35 end_pfn = max_low_pfn; 36 #endif 37 /* 对指定节点安装启动分配器 */ 38 bootmap = setup_node_bootmem(nodeid, start_pfn, end_pfn, 39 bootmap); 40 } 41 /* bootmem的分配制度到这里就已经建立完成,把after_bootmem变量置成1 */ 42 after_bootmem = 1; 43 } 44 45 static unsigned long __init setup_node_bootmem(int nodeid, 46 unsigned long start_pfn, 47 unsigned long end_pfn, 48 unsigned long bootmap) 49 { 50 unsigned long bootmap_size; 51 52 /* 初始化这个内存节点:将映射位图中的所有位置1。不要触及min_low_pfn */ 53 bootmap_size = init_bootmem_node(NODE_DATA(nodeid), 54 bootmap >> PAGE_SHIFT, 55 start_pfn, end_pfn); 56 printk(KERN_INFO " node %d low ram: %08lx - %08lx\n", 57 nodeid, start_pfn<<PAGE_SHIFT, end_pfn<<PAGE_SHIFT); 58 printk(KERN_INFO " node %d bootmap %08lx - %08lx\n", 59 nodeid, bootmap, bootmap + bootmap_size); 60 /* 将活动内存区对应位图相关位置0,表示可被分配的 */ 61 free_bootmem_with_active_regions(nodeid, end_pfn); 62 /* 将保留内存的相关页面对应位置为1,表示已经分配 63 或者不可用(不能被分配) */ 64 early_res_to_bootmem(start_pfn<<PAGE_SHIFT, end_pfn<<PAGE_SHIFT); 65 /* 返回映射页面的最后地址,下次映射即可以从这里开始 */ 66 return bootmap + bootmap_size; 67 }
设置分配器的主要工作是初始化引导时的内存分配器(只是低端内存区);在e820中查找引导内存块;对每个在线节点计算出其起始和终止地址,然后调用setup_node_bootmem()安装启动分配器。在这个函数中,调用init_bootmem_node()初始化这个节点的映射位图。将活动内存区对应位图相关位置0,表示可用;将保留内存的相关页面对应位置为1,表示已经分配(不可用)。其中初始化映射位图的函数init_bootmem_node()在mm/bootmem.c中,调用链为init_bootmem_node()--->init_bootmem_core()--->link_bootmem(bdata),最终将bdata添加到全局的bdata_list链表中。当所有在线内存节点设置好后,bootmem内存分配器就初始化完毕。
mm/bootmem.c实现了完整的引导时物理内存分配器和配置器,包括内存节点初始化、内存分配、释放等各种操作。我们概述一下启动内存分配器的主要操作接口功能:
init_bootmem_node():注册一个节点以作为启动内存。核心操作由init_bootmem_core()完成,每调用它一次来设置自己的分配器。
link_bootmem():按顺序添加一个bdata到全局的bdata_list链表中。
free_all_bootmem_node():释放一个节点的可用页面给伙伴系统。核心操作由free_all_bootmem_core()完成。
free_bootmem_node():将指定节点上的一个页面范围标记为可用(即未分配)。
reserve_bootmem_node():将指定节点上的一个页面范围标记为保留。
__alloc_bootmem_node():为指定节点分配启动内存。核心操作由alloc_bootmem_core()完成。
__free():bootmem分配器的释放内存操作。
__reserve():bootmem分配器的保留内存操作。
alloc_bootmem_core():bootmem分配器的分配内存操作。
2、建立永久的分页机制
在前面的“内存映射机制“介绍中,init_memory_mapping()只是构建了内核页表,作为临时的分页映射。例如只对高端内存固定映射区创建了页表结构,并没有对高端内存区永久映射区进行初始化。setup_arch()在执行完init_memory_mapping()和initmem_init()后,就会调用arch/x86/mm/init_32.c:paging_init()建立虚拟内存管理要用到的完整页表和永久分页机制。如下:
1 void __init paging_init(void) 2 { 3 pagetable_init(); 4 5 __flush_tlb_all(); 6 7 kmap_init(); 8 9 /* 10 * NOTE: 在这里bootmem分配器完全可用了 11 */ 12 sparse_init(); 13 zone_sizes_init(); 14 }
该函数建立完整的页表,注意起始的8MB已经被head_32.S映射了。该函数也会取消虚拟地址0处的页面映射,以便我们可以在内核中陷入并跟踪那些麻烦的NULL引用错误。它的主要工作包括页表初始化、内核永久映射区初始化、稀疏内存映射初始化、管理区初始化。下面重点讨论该函数。
arch/x86/mm/init_32.c:pagetable_init()函数用于完成页表初始化,并初始化高端内存永久映射区。如下:
1 static void __init pagetable_init(void) 2 { 3 pgd_t *pgd_base = swapper_pg_dir; 4 5 permanent_kmaps_init(pgd_base); 6 } 7 8 #ifdef CONFIG_HIGHMEM 9 static void __init permanent_kmaps_init(pgd_t *pgd_base) 10 { 11 unsigned long vaddr; 12 pgd_t *pgd; 13 pud_t *pud; 14 pmd_t *pmd; 15 pte_t *pte; 16 17 vaddr = PKMAP_BASE; 18 /* 该阶段,也就是永久内存映射区的页表初始化 */ 19 page_table_range_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base); 20 21 pgd = swapper_pg_dir + pgd_index(vaddr); 22 pud = pud_offset(pgd, vaddr); 23 pmd = pmd_offset(pud, vaddr); 24 pte = pte_offset_kernel(pmd, vaddr); 25 /* 将永久映射区间映射的第一个页表项保存到pkmap_page_table中 */ 26 pkmap_page_table = pte; 27 } 28 /* ...... */ 29 #else 30 static inline void permanent_kmaps_init(pgd_t *pgd_base) 31 { 32 } 33 #endif /* CONFIG_HIGHMEM */
根据上面代码,只有定义了使用高端内存,才会有高端永久映射区。首先用pgd_base保存页全局目录表的起始地址swapper_pg_dir。而后在函数permanent_kmaps_init()中,调用page_table_range_init()建立页表,这个函数在前面分析过,它会先根据永久映射区起始地址PKMAP_BASE,获取pgd表项索引、pmd表项索引,然后建立下一级pmd表,和最终的pte页表。第一个页表项保存到pkmap_page_table中。如果内核不划分高端内存,则permanent_kmaps_init()什么也不做。注意paging_init()初始化完页表后,要用__flush_tlb_all()刷新缓存TLB中的映射内容。
arch/x86/mm/init_32.c:kmap_init()函数用于缓存第一个kmap页表项,如下:
1 static void __init kmap_init(void) 2 { 3 unsigned long kmap_vstart; 4 5 /* 6 * Cache the first kmap pte: 7 */ 8 /* 得到高端固定内存映射区域的起始内存的页表,将这个页表 9 放到kmap_pte变量中。确切的说应该是固定内存中的临时内存映射区域 */ 10 kmap_vstart = __fix_to_virt(FIX_KMAP_BEGIN); 11 kmap_pte = kmap_get_fixmap_pte(kmap_vstart); 12 13 kmap_prot = PAGE_KERNEL; 14 }
该函数首先把高端固定映射区(即高端临时内存映射区)的起始地址FIX_KMAP_BEGIN转换成虚拟地址,然后获取它的pte页表项,并保存到全局的kmap_pte中。
mm/sparse.c:sparse_init()函数用于初始稀疏内存的映射,这里就不展开了。这里重点介绍管理区初始化,这是内存管理的重要组成部分,在arch/x86/mm/init_32.c:zone_sizes_init()中,如下:
1 static void __init zone_sizes_init(void) 2 { 3 /* 初始化各种管理区中的最大页面数,在后面用于具体的初始化工作 */ 4 unsigned long max_zone_pfns[MAX_NR_ZONES]; 5 memset(max_zone_pfns, 0, sizeof(max_zone_pfns)); 6 max_zone_pfns[ZONE_DMA] = /* DMA区的最大页面帧号,后面的类似 */ 7 virt_to_phys((char *)MAX_DMA_ADDRESS) >> PAGE_SHIFT; 8 max_zone_pfns[ZONE_NORMAL] = max_low_pfn; 9 #ifdef CONFIG_HIGHMEM 10 max_zone_pfns[ZONE_HIGHMEM] = highend_pfn; 11 #endif 12 /* 内存体系的MMU建立,包括伙伴系统的初步建立 */ 13 free_area_init_nodes(max_zone_pfns); 14 }
在“内存描述”一节中对各种管理区类型做了详细介绍,这里首先用数组max_zone_pfns保存各种类型管理区的最大页面数,宏MAX_DMA_ADDRESS在arch/x86/include/asm/dma.h中定义,表示能执行DMA传输的最大地址,其中x86-32非PAE模式下MAX_DMA_ADDRESS为PAGE_OFFSET + 0x1000000,即从内核空间开始处的16MB为DMA区的地址范围,因此DMA区的地址范围为3G~3G+16M这一段空间。把这个最大地址转换成页帧号保存到max_zone_pfns数组,接着保存NORMAL区和HIGHMEM区的最大页面号。最后调用核心函数mm/page_alloc.c:free_area_init_nodes()初始化所有pg_data_t内存节点的各种管理区数据,传入参数为由各管理区最大PFN构成的数组。代码如下:
1 void __init free_area_init_nodes(unsigned long *max_zone_pfn) 2 { 3 unsigned long nid; 4 int i; 5 6 /* Sort early_node_map as initialisation assumes it is sorted */ 7 sort_node_map(); /* 将活动区域进行排序 */ 8 9 /* 记录管理区的界限 */ 10 memset(arch_zone_lowest_possible_pfn, 0, 11 sizeof(arch_zone_lowest_possible_pfn)); 12 memset(arch_zone_highest_possible_pfn, 0, 13 sizeof(arch_zone_highest_possible_pfn)); 14 /* 找出活动内存中最小的页面 */ 15 arch_zone_lowest_possible_pfn[0] = find_min_pfn_with_active_regions(); 16 arch_zone_highest_possible_pfn[0] = max_zone_pfn[0]; 17 for (i = 1; i < MAX_NR_ZONES; i++) { 18 if (i == ZONE_MOVABLE) 19 continue; 20 /* 假定区域连续,下一个区域的最小页面为上一个区的最大页面 */ 21 arch_zone_lowest_possible_pfn[i] = 22 arch_zone_highest_possible_pfn[i-1]; 23 arch_zone_highest_possible_pfn[i] = 24 max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]); 25 } 26 /* 对ZONE_MOVABLE区域设置为0 */ 27 arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; 28 arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; 29 30 /* 找出每个节点上ZONE_MOVABLE区的开始页面号 */ 31 memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); 32 find_zone_movable_pfns_for_nodes(zone_movable_pfn); 33 34 /* 打印管理区的范围 */ 35 printk("Zone PFN ranges:\n"); 36 for (i = 0; i < MAX_NR_ZONES; i++) { 37 if (i == ZONE_MOVABLE) 38 continue; 39 printk(" %-8s %0#10lx -> %0#10lx\n", 40 zone_names[i], 41 arch_zone_lowest_possible_pfn[i], 42 arch_zone_highest_possible_pfn[i]); 43 } 44 45 /* 打印每个节点上ZONE_MOVABLE区开始的页面号 */ 46 printk("Movable zone start PFN for each node\n"); 47 for (i = 0; i < MAX_NUMNODES; i++) { 48 if (zone_movable_pfn[i]) 49 printk(" Node %d: %lu\n", i, zone_movable_pfn[i]); 50 } 51 52 /* 打印early_node_map[] */ 53 printk("early_node_map[%d] active PFN ranges\n", nr_nodemap_entries); 54 for (i = 0; i < nr_nodemap_entries; i++) 55 printk(" %3d: %0#10lx -> %0#10lx\n", early_node_map[i].nid, 56 early_node_map[i].start_pfn, 57 early_node_map[i].end_pfn); 58 59 /* 初始化每个节点 */ 60 mminit_verify_pageflags_layout(); /* 调试用 */ 61 setup_nr_node_ids(); 62 for_each_online_node(nid) { 63 pg_data_t *pgdat = NODE_DATA(nid); 64 /* zone中数据的初始化,伙伴系统建立,但是没有页面 65 和数据,页面在后面的mem_init中得到 */ 66 free_area_init_node(nid, NULL, 67 find_min_pfn_for_node(nid), NULL); 68 69 /* 对该节点上的任何内存区 */ 70 if (pgdat->node_present_pages) 71 node_set_state(nid, N_HIGH_MEMORY); 72 /* 内存的相关检查 */ 73 check_for_regular_memory(pgdat); 74 } 75 } 76 77 void __paginginit free_area_init_node(int nid, unsigned long *zones_size, 78 unsigned long node_start_pfn, unsigned long *zholes_size) 79 { 80 pg_data_t *pgdat = NODE_DATA(nid); 81 82 pgdat->node_id = nid; 83 /* 这个已在前面调用一个函数得到 */ 84 pgdat->node_start_pfn = node_start_pfn; 85 /* 计算系统中节点nid的所有物理页面并保存在数据结构中 */ 86 calculate_node_totalpages(pgdat, zones_size, zholes_size); 87 /* 当节点只有一个时,将节点的map保存到全局变量中 */ 88 alloc_node_mem_map(pgdat); 89 #ifdef CONFIG_FLAT_NODE_MEM_MAP 90 printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n", 91 nid, (unsigned long)pgdat, 92 (unsigned long)pgdat->node_mem_map); 93 #endif 94 /* zone中相关数据的初始化,包括伙伴系统,等待队列,相关变量, 95 数据结构、链表等 */ 96 free_area_init_core(pgdat, zones_size, zholes_size); 97 } 98 99 static void __paginginit free_area_init_core(struct pglist_data *pgdat, 100 unsigned long *zones_size, unsigned long *zholes_size) 101 { 102 enum zone_type j; 103 int nid = pgdat->node_id; 104 unsigned long zone_start_pfn = pgdat->node_start_pfn; 105 int ret; 106 107 pgdat_resize_init(pgdat); 108 pgdat->nr_zones = 0; 109 init_waitqueue_head(&pgdat->kswapd_wait); 110 pgdat->kswapd_max_order = 0; 111 pgdat_page_cgroup_init(pgdat); 112 113 for (j = 0; j < MAX_NR_ZONES; j++) { 114 struct zone *zone = pgdat->node_zones + j; 115 unsigned long size, realsize, memmap_pages; 116 enum lru_list l; 117 /* 下面的两个函数会获得指定节点的真实内存大小 */ 118 size = zone_spanned_pages_in_node(nid, j, zones_size); 119 realsize = size - zone_absent_pages_in_node(nid, j, 120 zholes_size); 121 122 /* 123 * Adjust realsize so that it accounts for how much memory 124 * is used by this zone for memmap. This affects the watermark 125 * and per-cpu initialisations 126 */ 127 memmap_pages = /* 存放页面所需要的内存大小 */ 128 PAGE_ALIGN(size * sizeof(struct page)) >> PAGE_SHIFT; 129 if (realsize >= memmap_pages) { 130 realsize -= memmap_pages; 131 if (memmap_pages) 132 printk(KERN_DEBUG 133 " %s zone: %lu pages used for memmap\n", 134 zone_names[j], memmap_pages); 135 } else 136 printk(KERN_WARNING 137 " %s zone: %lu pages exceeds realsize %lu\n", 138 zone_names[j], memmap_pages, realsize); 139 140 /* Account for reserved pages */ 141 if (j == 0 && realsize > dma_reserve) { 142 realsize -= dma_reserve; /* 减去为DMA保留的页面 */ 143 printk(KERN_DEBUG " %s zone: %lu pages reserved\n", 144 zone_names[0], dma_reserve); 145 } 146 /* 如果不是高端内存区 */ 147 if (!is_highmem_idx(j)) 148 nr_kernel_pages += realsize; 149 nr_all_pages += realsize; 150 151 /* 下面为初始化zone结构的相关变量 */ 152 zone->spanned_pages = size; 153 zone->present_pages = realsize; 154 #ifdef CONFIG_NUMA 155 zone->node = nid; 156 zone->min_unmapped_pages = (realsize*sysctl_min_unmapped_ratio) 157 / 100; 158 zone->min_slab_pages = (realsize * sysctl_min_slab_ratio) / 100; 159 #endif 160 zone->name = zone_names[j]; 161 spin_lock_init(&zone->lock); 162 spin_lock_init(&zone->lru_lock); 163 zone_seqlock_init(zone); 164 zone->zone_pgdat = pgdat; 165 166 zone->prev_priority = DEF_PRIORITY; 167 168 zone_pcp_init(zone); 169 for_each_lru(l) { /* 初始化链表 */ 170 INIT_LIST_HEAD(&zone->lru[l].list); 171 zone->reclaim_stat.nr_saved_scan[l] = 0; 172 } 173 zone->reclaim_stat.recent_rotated[0] = 0; 174 zone->reclaim_stat.recent_rotated[1] = 0; 175 zone->reclaim_stat.recent_scanned[0] = 0; 176 zone->reclaim_stat.recent_scanned[1] = 0; 177 zap_zone_vm_stats(zone); 178 zone->flags = 0; 179 if (!size) 180 continue; 181 /* 需要定义相关宏 */ 182 set_pageblock_order(pageblock_default_order()); 183 /* zone中变量pageblock_flags,表示从启动分配器中进行内存申请 */ 184 setup_usemap(pgdat, zone, size); 185 /* zone中的任务等待队列和zone的伙伴系统(MAX_ORDER个链表)的初始化 */ 186 ret = init_currently_empty_zone(zone, zone_start_pfn, 187 size, MEMMAP_EARLY); 188 BUG_ON(ret); 189 /* zone中page相关属性的初始化工作 */ 190 memmap_init(size, nid, j, zone_start_pfn); 191 zone_start_pfn += size; 192 } 193 }
分析:
(1)free_area_init_nodes()函数用于初始化所有的节点和它们的管理区数据。它会对系统中每个活动节点(即内存簇)调用free_area_init_node(),使用add_active_range()提供的页面范围来计算各节点上每种管理区和洞的大小。如果两个相邻管理区的最大PFN相同,则表明后面这个管理区是空的。例如,如果arch_max_dma_pfn == arch_max_dma32_pfn,则表明arch_max_dma32_pfn没有页面。我们假定管理区是连续的,即后一种管理区的开始位置紧接着前一种管理区的结束位置。例如ZONE_DMA32开始于at arch_max_dma_pfn。函数先计算各种管理区的下限页面号和上限页面号,保存在两个数组中,对于连续的相邻管理区(只有ZONE_MOVABLE管理区的内存是不连续的),后一个管理区的下限页面号为前一个管理区的上限页面号。而ZONE_MOVABLE的上下限页面号均设为0。然后调用find_zone_movable_pfns_for_nodes()找出每个节点上ZONE_MOVABLE的开始PFN。
(2)对每个节点,调用free_area_init_node(),传入参数为节点ID,各个管理区的大小,节点的开始页面号,各洞的大小。该函数先调用calculate_node_totalpages()计算节点上的所有物理页面,并保存在节点的pgdat数据结构中,从“内存描述”一节中可知,节点pg_data_t结构中保存了该节点的所有管理区数据。然后调用free_area_init_core()初始化各个zone中相关数据,包括伙伴系统、等待队列、相关变量、数据结构、链表等。
(3)free_area_init_core()用于设置管理区的各个数据结构,包括标记管理区的所有页面,标记所有内存空队列,清除内存位图。该函数对节点上的每个管理区,计算它需要映射的真实页面数realsize(即真实内存大小),注意对DMA区这需要减去为DMA保留的页面。然后初始化该管理区的zone数据结构中的相关变量,包括总页面数、真实页面数即realsize、未映射页面数的下限(低于此值时将进行页面回收)、用于slab分配器的页面数下限、保护伙伴系统和页面回收的LRU链表的自旋锁、LRU队列初始化、页面回收状态域、用于管理区使用情况统计的vm_stats置0,等等。最后调用init_currently_empty_zone()初始化zone中的任务等待队列和伙伴系统,调用memmap_init()初始化zone中所有page的相关属性。
3、初始化管理区分配机制
从以上分析可以看出,setup_arch()中的内存管理初始化工作是与体系结构相关的,这里介绍的是x86 32位的情况。start_kernel()在执行完setup_arch()后即建立起永久分页机制,然后就会调用mm/page_alloc.c:build_all_zonelists()来初始化管理区分配机制,它通过对每种管理区维护一个管理区队列来实现分配和回收,因此整个初始化工作的核心就是构建所有的管理区队列。一个分配请求在zonelist数据结构上进行操作,该结构在include/linux/mmzone.h中,如下:
1 #ifdef CONFIG_NUMA 2 #define MAX_ZONELISTS 2 3 4 struct zonelist_cache { 5 unsigned short z_to_n[MAX_ZONES_PER_ZONELIST]; /* zone->nid */ 6 DECLARE_BITMAP(fullzones, MAX_ZONES_PER_ZONELIST); /* zone full? */ 7 unsigned long last_full_zap; /* when last zap'd (jiffies) */ 8 }; 9 #else 10 #define MAX_ZONELISTS 1 11 struct zonelist_cache; 12 #endif 13 14 struct zoneref { 15 struct zone *zone; /* Pointer to actual zone */ 16 int zone_idx; /* zone_idx(zoneref->zone) */ 17 }; 18 19 struct zonelist { 20 struct zonelist_cache *zlcache_ptr; // NULL or &zlcache 21 struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1]; 22 #ifdef CONFIG_NUMA 23 struct zonelist_cache zlcache; // optional ... 24 #endif 25 };
从前面“内存描述”介绍中可知,zonelist在节点的pg_data_t结构中维护,以作为节点的备用内存区,当节点没有可用内存时,就从队列中分配内存。一个zonelist表示一个管理区的一个队列,队列中的第一个管理区是分配的目标,其他则为备用管理区,以优先级递减的方式存放在队列中。zonelist_cache结构缓存了每个zonelist中的一些关键信息,以便在get_page_from_freelist()中扫描可用页面时,有更小的开销。其中位图fullzones用来跟踪当前zonelist中哪些管理区开始内存不足了;数组z_to_n[]把zonelist中的每个管理区映射到它的节点id,以便我们能估计在当前进程允许的内存范围内节点是否被设置。zoneref则包含了zonelist中实际的zone信息,封装成一个结构是为了避免解引用时进入一个大的结构体内并且搜索表格。
在zonelist中,zlcache_ptr指针用来标识是否有zlcache。如果非空,则就是zlcache的地址;如果为空,则表示没有zlcache。为了加快zonelist的读取速度,zoneref保存了要读取条目的管理区索引。include/linux/mmzone.h中定义了一些访问zoneref的函数。zonelist_zone()函数返回zoneref中的zone,zonelist_zone_idx()为一个条目返回管理区索引,zonelist_node_idx()为一个条目返回zone中的节点索引。
mm/page_alloc.c:build_all_zonelists()函数如下,这里介绍非NUMA的情况:
1 static void zoneref_set_zone(struct zone *zone, struct zoneref *zoneref) 2 { 3 zoneref->zone = zone; 4 zoneref->zone_idx = zone_idx(zone); 5 } 6 7 /* 8 * 构建管理区环形分配队列,把节点上的所有管理区添加到队列中 9 */ 10 static int build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist, 11 int nr_zones, enum zone_type zone_type) 12 { 13 struct zone *zone; 14 15 BUG_ON(zone_type >= MAX_NR_ZONES); 16 zone_type++; 17 18 do { 19 zone_type--; 20 zone = pgdat->node_zones + zone_type; 21 if (populated_zone(zone)) { /* 如果以页面为单位的管理区的总大小不为0 */ 22 zoneref_set_zone(zone, /* 将管理区添加到链表中 */ 23 &zonelist->_zonerefs[nr_zones++]); 24 check_highest_zone(zone_type); 25 } 26 27 } while (zone_type); 28 return nr_zones; 29 } 30 31 #ifdef CONFIG_NUMA 32 /* ...... */ 33 static void build_zonelists(pg_data_t *pgdat) 34 { 35 /* ...... */ 36 } 37 38 static void build_zonelist_cache(pg_data_t *pgdat) 39 { 40 /* ...... */ 41 } 42 43 #else /* non CONFIG_NUMA */ 44 /* ...... */ 45 static void build_zonelists(pg_data_t *pgdat) 46 { 47 int node, local_node; 48 enum zone_type j; 49 struct zonelist *zonelist; 50 51 local_node = pgdat->node_id; 52 53 zonelist = &pgdat->node_zonelists[0]; 54 /* 将zone添加到zone链表中,这样,zone中page的 55 分配等操作将依靠这个环形的链表 */ 56 j = build_zonelists_node(pgdat, zonelist, 0, MAX_NR_ZONES - 1); 57 58 /* 59 * Now we build the zonelist so that it contains the zones 60 * of all the other nodes. 61 * We don't want to pressure a particular node, so when 62 * building the zones for node N, we make sure that the 63 * zones coming right after the local ones are those from 64 * node N+1 (modulo N) 65 */ 66 /* 对其他在线的节点创建zonelist */ 67 for (node = local_node + 1; node < MAX_NUMNODES; node++) { 68 if (!node_online(node)) 69 continue; 70 j = build_zonelists_node(NODE_DATA(node), zonelist, j, 71 MAX_NR_ZONES - 1); 72 } 73 for (node = 0; node < local_node; node++) { 74 if (!node_online(node)) 75 continue; 76 j = build_zonelists_node(NODE_DATA(node), zonelist, j, 77 MAX_NR_ZONES - 1); 78 } 79 80 zonelist->_zonerefs[j].zone = NULL; 81 zonelist->_zonerefs[j].zone_idx = 0; 82 } 83 84 /* 构建zonelist缓存:对非NUMA的zonelist信息,只是把zlcache_ptr设成NULL */ 85 static void build_zonelist_cache(pg_data_t *pgdat) 86 { 87 pgdat->node_zonelists[0].zlcache_ptr = NULL; 88 } 89 90 #endif /* CONFIG_NUMA */ 91 92 /* 返回int值,因为可能通过stop_machine()调用本函数 */ 93 static int __build_all_zonelists(void *dummy) 94 { 95 int nid; 96 97 #ifdef CONFIG_NUMA 98 memset(node_load, 0, sizeof(node_load)); 99 #endif 100 for_each_online_node(nid) { 101 pg_data_t *pgdat = NODE_DATA(nid); 102 /* 创建zonelists,这个队列用来在分配内存时回绕,循环访问 */ 103 build_zonelists(pgdat); 104 /* 创建zonelist缓存信息:在非NUMA中,仅仅是把相关缓存变量设成NULL */ 105 build_zonelist_cache(pgdat); 106 } 107 return 0; 108 } 109 110 void build_all_zonelists(void) 111 { 112 /* 设置全局变量current_zonelist_order */ 113 set_zonelist_order(); 114 115 /* 对所有节点创建zonelists */ 116 if (system_state == SYSTEM_BOOTING) { /* 系统正在引导时 */ 117 __build_all_zonelists(NULL); 118 mminit_verify_zonelist(); /* 调试用 */ 119 cpuset_init_current_mems_allowed(); 120 } else { 121 /* 非引导时要停止所有cpu以确保没有使用zonelist */ 122 stop_machine(__build_all_zonelists, NULL, NULL); 123 /* cpuset refresh routine should be here */ 124 } 125 /* 计算所有zone中可分配的页面数之和 */ 126 vm_total_pages = nr_free_pagecache_pages(); 127 /* 128 * Disable grouping by mobility if the number of pages in the 129 * system is too low to allow the mechanism to work. It would be 130 * more accurate, but expensive to check per-zone. This check is 131 * made on memory-hotadd so a system can start with mobility 132 * disabled and enable it later 133 */ 134 if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES)) 135 page_group_by_mobility_disabled = 1; 136 else 137 page_group_by_mobility_disabled = 0; 138 139 printk("Built %i zonelists in %s order, mobility grouping %s. " 140 "Total pages: %ld\n", 141 nr_online_nodes, 142 zonelist_order_name[current_zonelist_order], 143 page_group_by_mobility_disabled ? "off" : "on", 144 vm_total_pages); 145 #ifdef CONFIG_NUMA 146 printk("Policy zone: %s\n", zone_names[policy_zone]); 147 #endif 148 }
分析:
(1)build_all_zonelists()调用__build_all_zonelists()来构建所有管理区队列。如果是系统引导时,则直接调用__build_all_zonelists()对所有节点创建zonelist;如果不是引导时,则要通过stop_machine()来调用__build_all_zonelists(),先停止所有CPU以确保没有使用zonelist。然后用nr_free_pagecache_pages()计算所有zone中可分配的页面总数,如果页面总数太小,则禁用页面分组移动功能(因为这个性能开销比较大)。
(2)在__build_all_zonelists()中,对每个在线节点,调用build_zonelists()创建管理区分配的环形队列,调用build_zonelist_cache()创建队列的缓存信息。这两个函数有NUMA版本和非NUMA版本,这里略去NUMA版本,只介绍非NUMA版本。在build_zonelists()中,对每个在线节点,调用build_zonelists_node()构建环形分配队列,把节点上的所有管理区添加到队列中。在build_zonelist_cache()中,对非NUMA的zonelist信息,只是把zlcache_ptr设成NULL。
(3)在build_zonelists_node()中,通过zoneref_set_zone()将每个产生的管理区添加到队列中。
从以上分析可知,内存管理区初始化主要是借助于引导分配器和已初始化的e820全局变量。内存管理区初始化后相应的伙伴系统、slab机制等等就可以在此基础上建立了。

浙公网安备 33010602011771号