找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3948

积分

0

好友

554

主题
发表于 9 小时前 | 查看: 4| 回复: 0

内核内存布局图是理解Linux内存管理系统的一把钥匙。无论是内存管理的初始化流程,还是后续的虚拟内存操作、各种内存分配机制,这张图都能提供一个清晰的宏观视角。

因此,我们首先通过一张图来总览全局,然后逐一剖析图中每个内存区域的来历和作用。这里需要区分几个概念:内核内存布局图、内存管理框架图以及用户空间内存布局图,它们分别从不同视角描绘了内存管理的结构。

一、内核内存布局总览

下图清晰地展示了内核虚拟内存的典型布局划分:

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。而 vectorfixmapvmalloc 则属于 ZONE_HIGHMEM 区域。pkmapmodules 区域则被划分在用户空间的地址范围内。

打印这段信息的核心函数是 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

起点的确定则涉及 vmalloclowmem 区域之间一个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_NORMALZONE_HIGHMEM 的分界点,对应虚拟地址 0xef800000。因此:

  • ZONE_NORMAL:大小760MB,范围 0xc0000000 - 0xef800000
  • ZONE_HIGHMEM:大小264MB,范围 0xef800000 - 0xffffffff

需要注意的是,ZONE_HIGHMEM 并不完全等同于 vmalloc 区域。它还包括开头的8MB hole和末尾的16MB空间(用于其他用途,如 fixmapvector)。所以,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,范围从 0xffc000000xfff00000

#define FIXADDR_START        0xffc00000UL
#define FIXADDR_END        0xfff00000UL
#define FIXADDR_TOP        (FIXADDR_END - PAGE_SIZE) // 保留一页Hole

3.7 vector

vector 区域用于映射CPU的异常向量表(vector page),大小为4KB(一页),范围从 0xffff00000xffff1000

#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,范围从 0xbfe000000xc0000000

#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,范围从 0xbf0000000xbfe00000(紧邻 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




上一篇:基于Go语言与RAG技术:从0到1实战构建企业级知识库智能问答系统
下一篇:麦当劳全球业务的高并发支撑:事件驱动架构(EDA)在AWS MSK上的实践详解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-1 20:22 , Processed in 0.426478 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表