内核内存布局图是理解Linux内存管理系统的一把钥匙。无论是内存管理的初始化流程,还是后续的虚拟内存操作、各种内存分配机制,这张图都能提供一个清晰的宏观视角。
因此,我们首先通过一张图来总览全局,然后逐一剖析图中每个内存区域的来历和作用。这里需要区分几个概念:内核内存布局图、内存管理框架图以及用户空间内存布局图,它们分别从不同视角描绘了内存管理的结构。
一、内核内存布局总览
下图清晰地展示了内核虚拟内存的典型布局划分:

二、内核启动时的内存布局打印
在内核基本完成内存初始化,整体布局稳定之后,会通过 start_kernel -> mm_init -> mem_init 调用链打印出详细的内存布局信息。以ARM 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 这段虚拟地址,与某段物理地址(例如在Vexpress平台上是 0x60000000 - 0x8f800000)是一一对应的。
进一步分析,.text、.init、.data、.bss 这些内核镜像的段都属于 lowmem 区域,即 ZONE_NORMAL。而 vector、fixmap、vmalloc 则属于 ZONE_HIGHMEM 区域。pkmap 和 modules 区域则被划分在用户空间的地址范围内。
打印这段信息的核心函数是 mem_init(),其关键代码如下:
void __init mem_init(void)
{
...
free_all_bootmem();
...
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%08lx (%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));
...
}
三、各内存区域详解
3.1 内核空间与用户空间的划分
内核空间和用户空间的分界点由 PAGE_OFFSET 宏决定,它定义了内核镜像的起始虚拟地址。在ARM 32位架构中,通常设置为 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 内核镜像 (.text, .init, .data, .bss) 空间
内核镜像的 .text 段并非从 PAGE_OFFSET (0xC0000000) 开始,而是从 0xc0008000 开始。这个地址由两部分组成:PAGE_OFFSET + TEXT_OFFSET。具体定义在链接脚本 arch/arm/kernel/vmlinux.lds.S 中:
OUTPUT_ARCH(arm)
ENTRY(stext)
jiffies = jiffies_64;
SECTIONS
{
/* ... */
. = 0xC0000000 + 0x00008000;
.head.text : {
_text = .;
*(.head.text)
}
.text : { /* Real text segment */
_stext = .; /* Text and read-only data */...
}
}
了解了 .text 的起始地址来源后,通过查看 System.map 文件可以确定其他段(如 .init、.data、.bss)的地址。
从 mem_init 打印的日志可以看到 .text 段的范围是 _text 到 _etext,计算其大小为 0xc060a09c - 0xc0008000 = 6153KB,并做了1KB对齐。整个内核镜像空间从 _text 开始,到 _end 结束。在 System.map 中通常如下所示:
c0008000 T _text
c0008000 T stext
...
c060a09c T _etext
c060b000 T __init_begin
...
c075c000 D __init_end
c075c000 D _data
c075c000 D _sdata
...
c07833c0 B __bss_start
c07833c0 D _edata
...
c07acbf0 B __bss_stop
c07acbf0 B _end
3.3 vmalloc 空间
vmalloc 区域用于 vmalloc() 和 ioremap() 等函数进行动态内存映射。其范围的确定相对直接:终点固定为 0xff000000。
#define VMALLOC_OFFSET (8*1024*1024)
#define VMALLOC_START (((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1))
#define VMALLOC_END 0xff000000UL
起点的确定则涉及 vmalloc 和 lowmem 区域之间一个8MB的间隙(Gap)。相关逻辑在 sanity_check_meminfo() 函数中,通过 vmalloc_min 变量(定义为 VMALLOC_END 向下偏移240MB)和物理内存布局来决定 lowmem 的上限 (arm_lowmem_limit),最终 high_memory 被设置为该上限的虚拟地址。因此,VMALLOC_START 就是 VMALLOC_END 向下偏移240MB,即 0xf0000000。
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 vmalloc_limit = __pa(vmalloc_min - 1) + 1;
...
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 的 8MB Hole
vmalloc 区域和 lowmem 区域之间有一个8MB的“空洞”(hole)。lowmem 是线性映射区,其虚拟地址与物理地址一一对应。这个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空间(用于其他用途,如 fixmap 和 vector)。所以,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。它在汇编文件 arch/arm/kernel/head.S 中被定义为绝对地址。
其大小为16KB,对应的虚拟地址空间是 0xc0004000 - 0xc0008000,物理地址空间则是 0x60004000 - 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
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
...
00001000 t __stubs_start
00001004 t vector_rst
00001020 t vector_irq
...
在 early_trap_init() 函数中,这两部分内容被拷贝到固定的虚拟地址 0xffff0000。其中 __vectors_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,范围从 0xbf000000 到 0xbfe00000(紧邻 pkmap 区域)。
/*
* 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
本系列关于Linux内存管理的前两篇文章可参考:
理解内核内存布局是深入Linux操作系统内核,特别是其计算机体系结构中内存子系统如何运作的基石。希望本文的梳理能帮助你建立起清晰的图景。如果你想深入探讨或在实际编码中遇到相关问题,欢迎在云栈社区与更多开发者交流。
原文作者:ArnoldLu
原文地址:https://www.cnblogs.com/arnoldlu/p/8068286.html