Linux内存管理 (3)内核内存的布局图
专题:Linux内存管理专题
关键词:内核内存布局图、lowmem线性映射区、kernel image、ZONE_NORMAL、ZONE_HIGHMEM、swapper_pg_dir、fixmap、vector、pkmap。
内核内存布局图对于理解内存管理至关重要,有了布局图对于理解内存管理初始化,以及虚拟内存,各种内存分配都有辅助作用。
所以可以用一张图来总领,然后逐个介绍每一段的来历,作用等等。
内核内存布局图和内存管理框架图是不同视角的内存管理框图,还包括后面介绍的用户空间内存布局图。
1. 内核内存布局图框图
2. 内核内存布局打印
在内核基本完成内存初始化工作,整体布局稳定之后,start_kernel-->mm_init-->mem_init打印了一段内存layout。
Vexpress平台打印如下:
Memory: 1031428K/1048576K available (4787K kernel code, 156K rwdata, 1364K rodata, 1348K init, 166K bss, 17148K reserved, 0K cma-reserved, 270336K highmem) Virtual kernel memory layout: vector : 0xffff0000 - 0xffff1000 ( 4 kB) fixmap : 0xffc00000 - 0xfff00000 (3072 kB) vmalloc : 0xf0000000 - 0xff000000 ( 240 MB) lowmem : 0xc0000000 - 0xef800000 ( 760 MB) pkmap : 0xbfe00000 - 0xc0000000 ( 2 MB) modules : 0xbf000000 - 0xbfe00000 ( 14 MB) .text : 0xc0008000 - 0xc060a09c (6153 kB) .init : 0xc060b000 - 0xc075c000 (1348 kB) .data : 0xc075c000 - 0xc07833c0 ( 157 kB) .bss : 0xc07833c0 - 0xc07acbf0 ( 167 kB)
这里面的地址都是虚拟地址,其中lowmem是线性映射区。
线性映射区的意思是0xc0000000 - 0xef800000这段虚拟地址,和0x60000000 - 0x8f800000这段物理地址是一一对应的。
.text、.init、.data、.bss都属于lowmem区域,也即ZONE_NORMAL;vector、fixmap、vmalloc属于ZONE_HIGHMEM区域。
pkmap、modules属于用户空间。
void __init mem_init(void) { #ifdef CONFIG_HAVE_TCM /* These pointers are filled in on TCM detection */ extern u32 dtcm_end; extern u32 itcm_end; #endif set_max_mapnr(pfn_to_page(max_pfn) - mem_map); /* this will put all unused low memory onto the freelists */ free_unused_memmap(); free_all_bootmem(); #ifdef CONFIG_SA1111 /* now that our DMA memory is actually so designated, we can free it */ free_reserved_area(__va(PHYS_OFFSET), swapper_pg_dir, -1, NULL); #endif free_highpages(); mem_init_print_info(NULL); #define MLK(b, t) b, t, ((t) - (b)) >> 10 #define MLM(b, t) b, t, ((t) - (b)) >> 20 #define MLK_ROUNDUP(b, t) b, t, DIV_ROUND_UP(((t) - (b)), SZ_1K) pr_notice("Virtual kernel memory layout:\n" " vector : 0x%08lx - 0x%08lx (%4ld kB)\n" #ifdef CONFIG_HAVE_TCM " DTCM : 0x%08lx - 0x%08lx (%4ld kB)\n" " ITCM : 0x%08lx - 0x%08lx (%4ld kB)\n" #endif " fixmap : 0x%08lx - 0x%08lx (%4ld kB)\n" " vmalloc : 0x%08lx - 0x%08lx (%4ld MB)\n" " lowmem : 0x%08lx - 0x%08lx (%4ld MB)\n" #ifdef CONFIG_HIGHMEM " pkmap : 0x%08lx - 0x%08lx (%4ld MB)\n" #endif #ifdef CONFIG_MODULES " modules : 0x%08lx - 0x%08lx (%4ld MB)\n" #endif " .text : 0x%p" " - 0x%p" " (%4td kB)\n" " .init : 0x%p" " - 0x%p" " (%4td kB)\n" " .data : 0x%p" " - 0x%p" " (%4td kB)\n" " .bss : 0x%p" " - 0x%p" " (%4td kB)\n", MLK(UL(CONFIG_VECTORS_BASE), UL(CONFIG_VECTORS_BASE) + (PAGE_SIZE)), #ifdef CONFIG_HAVE_TCM MLK(DTCM_OFFSET, (unsigned long) dtcm_end), MLK(ITCM_OFFSET, (unsigned long) itcm_end), #endif MLK(FIXADDR_START, FIXADDR_END), MLM(VMALLOC_START, VMALLOC_END), MLM(PAGE_OFFSET, (unsigned long)high_memory), #ifdef CONFIG_HIGHMEM MLM(PKMAP_BASE, (PKMAP_BASE) + (LAST_PKMAP) * (PAGE_SIZE)), #endif #ifdef CONFIG_MODULES MLM(MODULES_VADDR, MODULES_END), #endif MLK_ROUNDUP(_text, _etext), MLK_ROUNDUP(__init_begin, __init_end), MLK_ROUNDUP(_sdata, _edata), MLK_ROUNDUP(__bss_start, __bss_stop)); #undef MLK #undef MLM #undef MLK_ROUNDUP /* * Check boundaries twice: Some fundamental inconsistencies can * be detected at build time already. */ #ifdef CONFIG_MMU BUILD_BUG_ON(TASK_SIZE > MODULES_VADDR); BUG_ON(TASK_SIZE > MODULES_VADDR); #endif #ifdef CONFIG_HIGHMEM BUILD_BUG_ON(PKMAP_BASE + LAST_PKMAP * PAGE_SIZE > PAGE_OFFSET); BUG_ON(PKMAP_BASE + LAST_PKMAP * PAGE_SIZE > PAGE_OFFSET); #endif if (PAGE_SIZE >= 16384 && get_num_physpages() <= 128) { extern int sysctl_overcommit_memory; /* * On a machine this small we won't get * anywhere without overcommit, so turn * it on by default. */ sysctl_overcommit_memory = OVERCOMMIT_ALWAYS; } }
3. 各部分框图详解
3.1 内核空间用户空间划分
内核和用户空间的分界点是由PAGE_OFFSET决定的,即内核image的起始地址为0xc0000000。
/* PAGE_OFFSET - the virtual address of the start of the kernel image */ #define PAGE_OFFSET UL(CONFIG_PAGE_OFFSET) #define CONFIG_PAGE_OFFSET 0xC0000000
3.2 kernel image空间
为什么kernel image空间从0xc0008000开始?
0xc0008000是由两部分组成的PAGE_OFFSET + TEXT_OFFSET。在arch/arm/kernel/vmlinux.lds.S中进行了明确定义。
生成的文件vmlinux.lds中,可以看出.text即从0xc000800开始。
OUTPUT_ARCH(arm) ENTRY(stext) jiffies = jiffies_64; SECTIONS { /* * XXX: The linker does not define how output sections are * assigned to input sections when there are multiple statements * matching the same input section name. There is no documented * order of matching. * * unwind exit sections must be discarded before the rest of the * unwind sections get included. */ ... . = 0xC0000000 + 0x00008000; .head.text : { _text = .; *(.head.text) } .text : { /* Real text segment */ _stext = .; /* Text and read-only data */... }
在了解.text其实地址来历之后,通过查看System.map就可以确定其余段的地址了。
从mem_init中打印的log,可以看出:_text - _etext,0xc060a09c - 0xc0008000=6153KB,采用1K对齐。
kernel image空间从_text开始,到_end结束。
c0008000 T _text c0008000 T stext ... c060a09c T _etext c060b000 T __init_begin ... c075c000 D __init_end c075c000 D _data c075c000 D _sdata c075c000 D init_thread_union c075e000 D __nosave_begin ... c07833c0 B __bss_start c07833c0 D _edata .. c07acbf0 B __bss_stop c07acbf0 B _end
3.3 vmalloc空间
vmalloc的区域用于给vmalloc/ioremap动态分配内存。
vmalloc的空间确定较简单,首先确定vmalloc终点0xff000000。
#define VMALLOC_OFFSET (8*1024*1024) #define VMALLOC_START (((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1)) #define VMALLOC_END 0xff000000UL
然后是确定vmalloc区域的起点,其中vmalloc和lowmem之间需要一个8MB空间的Gap。
static void * __initdata vmalloc_min = (void *)(VMALLOC_END - (240 << 20) - VMALLOC_OFFSET);----------------------------vmalloc_min为VMALLOC_END向下偏移240MB。 void __init sanity_check_meminfo(void) { phys_addr_t memblock_limit = 0; int highmem = 0; phys_addr_t vmalloc_limit = __pa(vmalloc_min - 1) + 1; struct memblock_region *reg; for_each_memblock(memory, reg) { phys_addr_t block_start = reg->base; phys_addr_t block_end = reg->base + reg->size; phys_addr_t size_limit = reg->size; if (reg->base >= vmalloc_limit) highmem = 1; else size_limit = vmalloc_limit - reg->base; ... if (!highmem) { if (block_end > arm_lowmem_limit) { if (reg->size > size_limit) arm_lowmem_limit = vmalloc_limit;------------------------------此种情况arm_lowmem_limit等于vmalloc_min else arm_lowmem_limit = block_end; } ... } } high_memory = __va(arm_lowmem_limit - 1) + 1;---------------------------------所以high_memory也即vmalloc_min。 ... memblock_set_current_limit(memblock_limit);-----------------------------------根据arm_lowmem_limit来作为ZONE_NORMAL的终点。 }
最终VMALLOC_START即为VMALLOC_END向下偏移240MB,即VMALLOC_START=0xf0000000
关于vmalloc 8MB hole
vmalloc区域和lowmem区域之间有一个8MB的hole。
lowmem区域是线性映射,可以虚拟地址和物理地址是1:1对应的。
这个8MB由于捕获虚拟地址的越界访问。
3.4 ZONE_NORMAL和ZONE_HIGHMEM划分
zone的划分在bootmem_init-->find_limits中确定。其中memblock.current_limit是在sanity_check_meminfo中确定。
max_low即作为ZONE_NORMAL和ZONE_HIGHMEM分界点,即0xef800000。
ZONE_NORMAL大小760MB,从0xc0000000 - 0xef800000,ZONE_HIGHMEM大小264MB,从0xef800000 - 0xffffffff。
ZONE_HIGHMEM并不等同于vmalloc,还有8MB hole和末尾16MB空间。所以vmalloc=264-8-16=240MB。
static void __init find_limits(unsigned long *min, unsigned long *max_low, unsigned long *max_high) { *max_low = PFN_DOWN(memblock_get_current_limit()); *min = PFN_UP(memblock_start_of_DRAM()); *max_high = PFN_DOWN(memblock_end_of_DRAM()); } void __init_memblock memblock_set_current_limit(phys_addr_t limit) { memblock.current_limit = limit; } phys_addr_t __init_memblock memblock_get_current_limit(void) { return memblock.current_limit; }
对于ZONE_NORMAL线性映射区域,虚拟地址和物理地址转换比较简单。由于Vexpress的RAM起始地址映射在0x60000000。
所以计算的时候需要去除这部分偏移PHYS_OFFSET。
static inline phys_addr_t __virt_to_phys(unsigned long x) { return (phys_addr_t)x - PAGE_OFFSET + PHYS_OFFSET; } static inline unsigned long __phys_to_virt(phys_addr_t x) { return x - PHYS_OFFSET + PAGE_OFFSET; }
3.5 swapper_pg_dir
swapper_pg_dir用于存放内核PGD页表的地方,赋给init_mm.pgd。
swapper_pg_dir被定义了绝对地址,在arch/arm/kernel/head.S中有如下定义。
swapper_pd_dir的大小为16KB,对应的虚拟地址空间是从0xc0004000 - 0xc0008000,物理地址空间是0x6000400~0x60008000。
arch/arm/kernel/head.S: /* * swapper_pg_dir is the virtual address of the initial page table. * We place the page tables 16K below KERNEL_RAM_VADDR. Therefore, we must * make sure that KERNEL_RAM_VADDR is correctly set. Currently, we expect * the least significant 16 bits to be 0x8000, but we could probably * relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0x4000. */ #define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET) #if (KERNEL_RAM_VADDR & 0xffff) != 0x8000--------------------------------------KERNEL_RAM_VADDR也确实是0xc0008000 #error KERNEL_RAM_VADDR must start at 0xXXXX8000 #endif #ifdef CONFIG_ARM_LPAE /* LPAE requires an additional page for the PGD */ #define PG_DIR_SIZE 0x5000 #define PMD_ORDER 3 #else #define PG_DIR_SIZE 0x4000 #define PMD_ORDER 2 #endif .globl swapper_pg_dir .equ swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE-------------------.equ定义swapper_pg_dir的绝对地址,所以swapper_pg_dir=0xc0008000-0x4000=0xc0004000 mm/init-mm.c: struct mm_struct init_mm = { .mm_rb = RB_ROOT, .pgd = swapper_pg_dir, ... INIT_MM_CONTEXT(init_mm) };
3.6 fixmap
fixmap是固定映射的意思,固定指的是固定虚拟地址。
那么这些固定的虚拟地址谁在用?都对应哪些物理地址?
fixmap区域很固定,大小为3MB,从0xffc00000 - 0xfff00000。
#define FIXADDR_START 0xffc00000UL #define FIXADDR_END 0xfff00000UL #define FIXADDR_TOP (FIXADDR_END - PAGE_SIZE)-------------保留一页Hole
延伸阅读:《Fix-Mapped Addresses》
3.7 vector
vector区域用于映射CPU vector page,大小一页4KB,从0xffff0000 - 0xffff1000。
#define CONFIG_VECTORS_BASE 0xffff0000
在系统编译的时候,arch/arm/kernel/vmlinux.ld.S决定__vectors_start、__stubs_start起始地址。
__vectors_start = .; .vectors 0 : AT(__vectors_start) { *(.vectors) } . = __vectors_start + SIZEOF(.vectors); __vectors_end = .; __stubs_start = .; .stubs 0x1000 : AT(__stubs_start) { *(.stubs) } . = __stubs_start + SIZEOF(.stubs); __stubs_end = .;
这两部分的生成结果在System.map中也可以看出:
00000000 t __vectors_start 00000024 A cpu_v7_suspend_size 0000002c A cpu_ca9mp_suspend_size 00001000 t __stubs_start 00001004 t vector_rst 00001020 t vector_irq 000010a0 t vector_dabt 00001120 t vector_pabt 000011a0 t vector_und 00001220 t vector_addrexcptn 00001240 t vector_fiq 00001240 T vector_fiq_offset ...
这两部分在early_trap_init中拷贝,拷贝到了0xffff0000。__vextors_start占据一页,__stubs_start占据一页。
void __init early_trap_init(void *vectors_base) { ... memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start); memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start); kuser_init(vectors_base); flush_icache_range(vectors, vectors + PAGE_SIZE * 2); modify_domain(DOMAIN_USER, DOMAIN_CLIENT); ...}
3.8 pkmap
pkmap的意思是Permanent Kernel MAPping,永久内核内存映射。
是一种将Highmem页面映射到内核空间的技术。所以就需要定义CONFIG_HIGHMEM,才会存在这个区域。
所以pkmap区域是2MB大小,从0xbfe00000 - 0xc0000000。
#define PKMAP_BASE (PAGE_OFFSET - PMD_SIZE)---------------------0xc0000000-0x200000=0xbfe00000 #define LAST_PKMAP PTRS_PER_PTE #define PTRS_PER_PTE 512 #define PMD_SHIFT 21 #define PGDIR_SHIFT 21 #define PMD_SIZE (1UL << PMD_SHIFT) Documentation/arm/memory.txt: PKMAP_BASE PAGE_OFFSET-1 Permanent kernel mappings One way of mapping HIGHMEM pages into kernel space.
延伸阅读:《高端内存永久映射分析》
3.9 modules
如果定义了CONFIG_MODULES功能,则需要在用户空间开辟一段空间给insmod插入的模块。
这部分空间是动态映射的,在定义CONFIG_HIGHMEM情况下为16MB-2MB=14MB,从0xbf00000 - 0xbfe00000。
/* * The module space lives between the addresses given by TASK_SIZE * and PAGE_OFFSET - it must be within 32MB of the kernel text. */ #ifndef CONFIG_THUMB2_KERNEL #define MODULES_VADDR (PAGE_OFFSET - SZ_16M)------------------0xc0000000-0x1000000=0xbf000000 #else /* smaller range for Thumb-2 symbols relocation (2^24)*/ #define MODULES_VADDR (PAGE_OFFSET - SZ_8M) #endif #if TASK_SIZE > MODULES_VADDR #error Top of user space clashes with start of module space #endif /* * The highmem pkmap virtual space shares the end of the module area. */ #ifdef CONFIG_HIGHMEM #define MODULES_END (PAGE_OFFSET - PMD_SIZE)----------------0xc0000000-0x200000=0xbfe00000 #else #define MODULES_END (PAGE_OFFSET) #endif