3. 分页机制的初始化
paging_init负责建立只能用于内核的页表,用户空间无法访问。这对管理普通应用程序和内核访问内存的方式,有深远影响。在IA-32系统上内核通常将总的4GB可用虚拟地址空间按3:1划分。低端3GB用于用户态应用程序,而高端1GB则专用于内核。尽管在分配内核的虚拟地址空间时,当前系统上下文是不相干的,但每个进程都有自身特定的地址空间。
这些划分重要的目的是:
A. 在用户应用程序的执行切换到核心态时(这总是会发生的,例如在使用系统调用或发生周期性的时钟中断时),内核必须装载在一个可靠的环境中。因此有必要将地址空间的一部分分配给内核专用。
B. 物理内存页则映射到内核地址空间的起始处,以便内核直接访问,而无需复杂的页表操作。如果所有物理内存页都映射到用户空间进程能访问的地址空间中,如果在系统上有几个应用程序在运行,将导致严重的安全问题。每个应用程序都能够读取和修改其他进程在物理内存中的内存区,显然必须补习任何代价避免该情况出现。
虽然用于应用层进程的虚拟地址部分随进程切换而改变,但是内核部分总是相同的。
按3:1的比例划分地址空间,只是约略反应了内核中的情况,内核地址空间自身又分为各个段,如图所示:
(1)地址空间的第一段用于将系统的所有物理内存页映射到内核的虚拟地址空间中。由于内核地址空间从偏移0xC0000000开始,即经常提到的3GB,每个虚拟地址x都对应于物理地址x-0xC0000000,因此这是一个简单的线性平移。如图所示,直接映射区从0xC0000000-hignmem地址。
(2)如果物理内存超过896MB,则内核无法直接映射全部物理内存。加入物理内存大小为1GB,那么896MB后面的128MB如何使用?
A. 虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配。该机制通常用于用户进程,内核自身会试图尽力避免使用非连续的物理地址。内核通常会分配成功,因为大部分内存块都在启动时分配给内核,那时内存的碎片尚不严重。分配失败主要出现在动态加载模块时。
B. 持久映射区用于将高端内存域中的非持久页映射到内核中
C. 固定映射区是与物理地址空间中的固定页关联的虚拟地址空间,但具体关联的页帧可以自由选择。它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间的关联可以自行定义,关联建立以后内核总是会注意到。
两个相关的宏:
./mm/pgtable_32.c:unsigned int __VMALLOC_RESERVE = 128 << 20;
__VMALLOC_RESERVE:设置vmalloc区域的长度
#define MAXMEM (VMALLOC_END - PAGE_OFFSET - __VMALLOC_RESERVE)
MAXMEM则表示内核可以直接寻址的物理内存的最大可能值。
/include/asm/pgtable_32_types.h:
* newer 3-level PAE-mode page tables. */ #ifdef CONFIG_X86_PAE # include <asm/pgtable-3level_types.h> # define PMD_SIZE (1UL << PMD_SHIFT) # define PMD_MASK (~(PMD_SIZE - 1)) #else # include <asm/pgtable-2level_types.h> #endif #define PGDIR_SIZE (1UL << PGDIR_SHIFT) #define PGDIR_MASK (~(PGDIR_SIZE - 1)) /* Just any arbitrary offset to the start of the vmalloc VM area: the * current 8MB value just means that there will be a 8MB "hole" after the * physical memory until the kernel virtual memory starts. That means that * any out-of-bounds memory accesses will hopefully be caught. * The vmalloc() routines leaves a hole of 4kB between each vmalloced * area for the same reason. ;) */ #define VMALLOC_OFFSET (8 * 1024 * 1024) #ifndef __ASSEMBLY__ extern bool __vmalloc_start_set; /* set once high_memory is set */ #endif #define VMALLOC_START ((unsigned long)high_memory + VMALLOC_OFFSET) #ifdef CONFIG_X86_PAE #define LAST_PKMAP 512 #else #define LAST_PKMAP 1024 #endif #define PKMAP_BASE ((FIXADDR_BOOT_START - PAGE_SIZE * (LAST_PKMAP + 1)) \ & PMD_MASK) #ifdef CONFIG_HIGHMEM # define VMALLOC_END (PKMAP_BASE - 2 * PAGE_SIZE) #else # define VMALLOC_END (FIXADDR_START - 2 * PAGE_SIZE) #endif #define MODULES_VADDR VMALLOC_START #define MODULES_END VMALLOC_END #define MODULES_LEN (MODULES_VADDR - MODULES_END) #define MAXMEM (VMALLOC_END - PAGE_OFFSET - __VMALLOC_RESERVE) #endif /* _ASM_X86_PGTABLE_32_DEFS_H */ 关于high_memory: #ifdef CONFIG_X86_32 /* max_low_pfn get updated here */ find_low_pfn_range(); #else check_x2apic(); /* How many end-of-memory variables you have, grandma! */ /* need this before calling reserve_initrd */ if (max_pfn > (1UL<<(32 - PAGE_SHIFT))) max_low_pfn = e820_end_of_low_ram_pfn(); else max_low_pfn = max_pfn; high_memory = (void *)__va(max_pfn * PAGE_SIZE - 1) + 1; #endif
Max_low_pfn指定了物理内存数量小于896MB的系统上内存页的数目。该值的上界受限于896MB可容纳的最大页数,具体的计算可在find_low_pfn_range();中给出。如果启用了高端内存的支持,则high_memory表示两个内存区之间的边界,总共896MB.
如果VMALLOC_OFFSET取默认值,那么在直接映射的所有内存页和用于非连续分配的区域之间,会出现一个缺口。
#define VMALLOC_OFFSET (8 * 1024 * 1024) #define VMALLOC_START ((unsigned long)high_memory + VMALLOC_OFFSET)
VMALLOC_START和VMALLOC_END定义了vmalloc区域的开始和结束,该区域用于物理上不连续的内核映射,这两个值通常没有定义为常数,而是依赖于其他参数:
#define VMALLOC_START ((unsigned long)high_memory + VMALLOC_OFFSET)
Vmalloc区域的起始地址,取决于在直接映射物理内存时,使用了多少虚拟地址空间内存。内核还考虑到两个区域之间有至少为2*PAGE_SIZE的一个缺口,而且vmalloc区域可被VMALLOC_OFFSET整除的地址开始。
Vmalloc区域在何处结束取决于是否用了高端内存支持。如果没有启用,那么就不需要持久映射区域,因为整个物理内存都可以直接映射。因此,根据不同的配置,该区域结束于持久内核映射或固定映射区域的起始:
#ifdef CONFIG_HIGHMEM # define VMALLOC_END (PKMAP_BASE - 2 * PAGE_SIZE) #else # define VMALLOC_END (FIXADDR_START - 2 * PAGE_SIZE) #endif
持久映射区域的起始和结束定义如下:
#ifdef CONFIG_X86_PAE #define LAST_PKMAP 512 #else #define LAST_PKMAP 1024 #endif #define PKMAP_BASE ((FIXADDR_BOOT_START - PAGE_SIZE * (LAST_PKMAP + 1)) \ & PMD_MASK)
PKMAP_BASE定义了其起始地址,LAST_PKMAP定义了容纳该映射所需的页数。
最后一个内存段由固定映射占据。这些地址所指向物理内存中的随机位置。相对于内核空间起始处的线性映射,在该映射内部的虚拟地址和物理地址之间的管理是不可预设的,而可以自由定义。但定义后不可改变。固定映射区域会延伸到虚拟地址空间的顶端。
#define FIXADDR_SIZE (__end_of_permanent_fixed_addresses << PAGE_SHIFT) #define FIXADDR_BOOT_SIZE (__end_of_fixed_addresses << PAGE_SHIFT) #define FIXADDR_START (FIXADDR_TOP - FIXADDR_SIZE) #define FIXADDR_BOOT_START (FIXADDR_TOP - FIXADDR_BOOT_SIZE)
固定映射地址的优点在于,在编译时对此类地址的处理类似于常数,内核一启动即为其分配了物理地址。此类地址的反引用比普通指针要快速。内核会确保在上下文切换期间,对应于固定映射的页表不会从TLB刷出,因此在访问固定映射的内存时,总是通过TLB高速缓存取得对应的物理地址。
对每个固定映射地址都会创建一个常数,加入到fixed_addresses枚举值列表中:
/* * Here we define all the compile-time 'special' virtual * addresses. The point is to have a constant address at * compile time, but to set the physical address only * in the boot process. * for x86_32: We allocate these special addresses * from the end of virtual memory (0xfffff000) backwards. * Also this lets us do fail-safe vmalloc(), we * can guarantee that these special addresses and * vmalloc()-ed addresses never overlap. * * These 'compile-time allocated' memory buffers are * fixed-size 4k pages (or larger if used with an increment * higher than 1). Use set_fixmap(idx,phys) to associate * physical memory with fixmap indices. * * TLB entries of such buffers will not be flushed across * task switches. */ enum fixed_addresses { #ifdef CONFIG_X86_32 FIX_HOLE, #else VSYSCALL_PAGE = (FIXADDR_TOP - VSYSCALL_ADDR) >> PAGE_SHIFT, #ifdef CONFIG_PARAVIRT_CLOCK PVCLOCK_FIXMAP_BEGIN, PVCLOCK_FIXMAP_END = PVCLOCK_FIXMAP_BEGIN+PVCLOCK_VSYSCALL_NR_PAGES-1, #endif #endif FIX_DBGP_BASE, FIX_EARLYCON_MEM_BASE, #ifdef CONFIG_PROVIDE_OHCI1394_DMA_INIT FIX_OHCI1394_BASE, #endif #ifdef CONFIG_X86_LOCAL_APIC FIX_APIC_BASE, /* local (CPU) APIC) -- required for SMP or not */ #endif #ifdef CONFIG_X86_IO_APIC FIX_IO_APIC_BASE_0, FIX_IO_APIC_BASE_END = FIX_IO_APIC_BASE_0 + MAX_IO_APICS - 1, #endif FIX_RO_IDT, /* Virtual mapping for read-only IDT */ #ifdef CONFIG_X86_32 FIX_KMAP_BEGIN, /* reserved pte's for temporary kernel mappings */ FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1, #ifdef CONFIG_PCI_MMCONFIG FIX_PCIE_MCFG, #endif #endif #ifdef CONFIG_PARAVIRT FIX_PARAVIRT_BOOTMAP, #endif FIX_TEXT_POKE1, /* reserve 2 pages for text_poke() */ FIX_TEXT_POKE0, /* first page is last, because allocation is backward */ #ifdef CONFIG_X86_INTEL_MID FIX_LNW_VRTC, #endif __end_of_permanent_fixed_addresses, /* * 512 temporary boot-time mappings, used by early_ioremap(), * before ioremap() is functional. * * If necessary we round it up to the next 512 pages boundary so * that we can have a single pgd entry and a single pte table: */ #define NR_FIX_BTMAPS 64 #define FIX_BTMAPS_SLOTS 8 #define TOTAL_FIX_BTMAPS (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS) FIX_BTMAP_END = (__end_of_permanent_fixed_addresses ^ (__end_of_permanent_fixed_addresses + TOTAL_FIX_BTMAPS - 1)) & -PTRS_PER_PTE ? __end_of_permanent_fixed_addresses + TOTAL_FIX_BTMAPS - (__end_of_permanent_fixed_addresses & (TOTAL_FIX_BTMAPS - 1)) : __end_of_permanent_fixed_addresses, FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1, #ifdef CONFIG_X86_32 FIX_WP_TEST, #endif #ifdef CONFIG_INTEL_TXT FIX_TBOOT_BASE, #endif __end_of_fixed_addresses };
内核提供了fix_to_virt函数,用于计算固定映射常数的虚拟地址:
//"arch/arm/include/asm/fixmap.h" 28L, 724C
Cscope tag: fix_to_virt
#ifndef _ASM_FIXMAP_H #define _ASM_FIXMAP_H #define FIXADDR_START 0xffc00000UL #define FIXADDR_TOP 0xffe00000UL #define FIXADDR_SIZE (FIXADDR_TOP - FIXADDR_START) #define FIX_KMAP_NR_PTES (FIXADDR_SIZE >> PAGE_SHIFT) #define __fix_to_virt(x) (FIXADDR_START + ((x) << PAGE_SHIFT)) #define __virt_to_fix(x) (((x) - FIXADDR_START) >> PAGE_SHIFT) extern void __this_fixmap_does_not_exist(void); static inline unsigned long fix_to_virt(const unsigned int idx) { if (idx >= FIX_KMAP_NR_PTES) __this_fixmap_does_not_exist(); return __fix_to_virt(idx); } static inline unsigned int virt_to_fix(const unsigned long vaddr) { BUG_ON(vaddr >= FIXADDR_TOP || vaddr < FIXADDR_START); return __virt_to_fix(vaddr); } #endif
~
编译器优化机制会完全消除if语句,因为该函数是内联函数,而且其参数都是常数。这样的优化是必要的,否则固定映射地址实际上并不优于普通指针。形式上的检查确保了所需的固定映射地址在有效区域中。__end_of_fixed_addresses是fixed_addresses的最后一个成员,定义了最大的可能数字。如果内核访问该地址无效,则调用伪函数__this_fixmap_does_not_exist,该函数未定义。在内核链接时,这会导致错误信息,表明由于存在未定义符号而无法生成映像文件。因此,这种内核故障在编译时即可检测。
在引用有效的固定映射地址时,if语句中的比较总是会通过。由于比较的两个操作数都是常数,该条件判断语句实际上不会执行,在编译器优化时会直接消除。
__fix_to_virt定义为宏。由于fix_to_virt是内联函数,其实现代码会直接复制到查询固定映射地址的代码处。
#define __fix_to_virt(x) (FIXADDR_START + ((x) << PAGE_SHIFT))
从顶部开始,内核回退n页,以确保第n个固定映射顶的虚拟地址。这个计算同样只使用了常数,编译器能够在编译时间计算结果。固定映射虚拟地址与物理内存页之间的关联是有setup_fixmap和setup_fixmap_nocache建立的。这两个函数只是将页表中的对应项与物理内存中的一页关联起来。