本章将带你从宏观视角理解 Linux 内存管理子系统的设计哲学、整体架构和核心组件。
内存管理是操作系统内核最核心、最复杂的子系统之一。它直接影响着:
┌─────────────────────────────────────────────────────────────────────┐│ 应用程序 (Application) │├─────────────────────────────────────────────────────────────────────┤│ 用户空间内存管理 (User Space) ││ malloc/free, mmap, brk, 共享内存, 内存映射文件 │├─────────────────────────────────────────────────────────────────────┤│ 系统调用接口 (System Call) ││ mmap, munmap, mprotect, brk, madvise... │├─────────────────────────────────────────────────────────────────────┤│ ││ Linux 内核内存管理子系统 ││ ┌──────────────────────────────────────────────────────────────┐ ││ │ 虚拟内存管理 │ 物理内存管理 │ 页面回收 │ 内存映射 │ 交换系统 │ ││ └──────────────────────────────────────────────────────────────┘ ││ │├─────────────────────────────────────────────────────────────────────┤│ 硬件抽象层 (HAL) ││ MMU, TLB, Cache, 物理内存 │└─────────────────────────────────────────────────────────────────────┘每个进程都拥有独立的、连续的虚拟地址空间,与物理内存解耦。
进程A的视角 进程B的视角┌──────────────┐ ┌──────────────┐│ 0xFFFFFFFF │ │ 0xFFFFFFFF ││ 内核空间 │ │ 内核空间 │├──────────────┤ ├──────────────┤│ │ │ ││ 栈 Stack │ │ 栈 Stack ││ ↓ │ │ ↓ ││ │ │ ││ ↑ │ │ ↑ ││ 堆 Heap/mmap │ │ 堆 Heap/mmap ││ │ │ │├──────────────┤ ├──────────────┤│ 数据段 │ │ 数据段 │├──────────────┤ ├──────────────┤│ 代码段 │ │ 代码段 │├──────────────┤ ├──────────────┤│ 0x00000000 │ │ 0x00000000 │└──────────────┘ └──────────────┘ │ │ └────────────┬───────────────┘ ↓ ┌────────────────┐ │ 物理内存 │ │ (被多个进程共享) │ └────────────────┘不立即分配物理内存,而是在真正访问时才分配,节省内存资源。
fork 时不复制页面,只在写入时才真正复制,提高 fork 效率。
相同内容(如共享库)只在物理内存中保存一份。
当内存紧张时,可以回收不常用的页面或将其交换到磁盘。
物理内存是计算机实际的 RAM 硬件,由 页框 (Page Frame) 组成。
// 源码位置: arch/arm64/include/asm/page-def.h#define PAGE_SHIFT CONFIG_ARM64_PAGE_SHIFT#define PAGE_SIZE (_AC(1, UL) << PAGE_SHIFT) // 通常为 4KB#define PAGE_MASK (~(PAGE_SIZE-1))页框大小 (ARM64 架构支持):
虚拟内存是操作系统提供的抽象,每个进程看到的都是独立的地址空间。
虚拟地址 物理地址 ┌────────────┐ ┌────────────┐ │ 0x7fff0000 │ ──── MMU 转换 ───→ │ 0x12340000 │ ├────────────┤ (页表) ├────────────┤ │ 0x7fff1000 │ ──────────────────→│ 0x56780000 │ ├────────────┤ ├────────────┤ │ 0x7fff2000 │ ──────────────────→│ 0x9abc0000 │ └────────────┘ └────────────┘┌─────────────────────────────────────────────────────────────────────────┐│ 虚拟地址 (64位) │├─────────┬─────────┬─────────┬─────────┬─────────┬───────────────────────┤│ 未使用 │ PGD │ PUD │ PMD │ PTE │ 页内偏移 ││ [63:48] │ [47:39] │ [38:30] │ [29:21] │ [20:12] │ [11:0] │└─────────┴────┬────┴────┬────┴────┬────┴────┬────┴───────────┬───────────┘ │ │ │ │ │ ↓ ↓ ↓ ↓ │ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ │ PGD │→│ PUD │→│ PMD │→│ PTE │→ 物理页框 ──┘ │ Table │ │ Table │ │ Table │ │ Table │ 基址 + 偏移 └───────┘ └───────┘ └───────┘ └───────┘Linux 内存管理子系统是一个复杂的层次化结构:
┌─────────────────────────────────────────────────────────────────────────┐│ 用户空间 ││ malloc/free, mmap, brk 等 │└────────────────────────────────┬────────────────────────────────────────┘ │ 系统调用 ↓┌─────────────────────────────────────────────────────────────────────────┐│ 进程地址空间管理 ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ mm_struct │ │vm_area_struct│ │ mmap() │ │ 缺页处理 │ ││ │ (进程内存 │ │ (VMA 虚拟 │ │ munmap() │ │ Page Fault │ ││ │ 描述符) │ │ 内存区域) │ │ mprotect │ │ Handler │ ││ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ││ mm/mmap.c, mm/memory.c │└────────────────────────────────┬────────────────────────────────────────┘ │ ↓┌─────────────────────────────────────────────────────────────────────────┐│ 页表管理 ││ ┌─────────────────────────────────────────────────────────────────┐ ││ │ PGD → P4D → PUD → PMD → PTE (多级页表) │ ││ │ 页表分配、释放、遍历、TLB 刷新 │ ││ └─────────────────────────────────────────────────────────────────┘ ││ arch/*/mm/, mm/pgtable-generic.c │└────────────────────────────────┬────────────────────────────────────────┘ │ ↓┌─────────────────────────────────────────────────────────────────────────┐│ 物理内存分配 ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ 伙伴系统 │ │ Slab/Slub │ │ vmalloc │ │ Per-CPU │ ││ │ Buddy │ │ 分配器 │ │ 分配器 │ │ 分配器 │ ││ │ System │ │ kmalloc() │ │ vmalloc() │ │ alloc_percpu│ ││ │alloc_pages()│ │ kfree() │ │ vfree() │ │ │ ││ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ││ mm/page_alloc.c mm/slub.c mm/vmalloc.c mm/percpu.c │└────────────────────────────────┬────────────────────────────────────────┘ │ ↓┌─────────────────────────────────────────────────────────────────────────┐│ 物理内存组织 ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ Node │ │ Zone │ │ Page │ │ Memblock │ ││ │ (NUMA节点) │ │ (内存区域) │ │ (页描述符) │ │ (早期内存) │ ││ │ pg_data_t │ │ struct zone │ │ struct page │ │ │ ││ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ││ include/linux/mmzone.h, mm_types.h │└────────────────────────────────┬────────────────────────────────────────┘ │ ↓┌─────────────────────────────────────────────────────────────────────────┐│ 内存回收与交换 ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ 页面回收 │ │ 交换 │ │ 内存压缩 │ │ OOM Killer │ ││ │ kswapd │ │ Swap │ │ Compaction │ │ │ ││ │ LRU 链表 │ │ 换入/换出 │ │ 碎片整理 │ │ 内存耗尽处理 │ ││ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ││ mm/vmscan.c mm/swapfile.c mm/compaction.c mm/oom_kill.c │└─────────────────────────────────────────────────────────────────────────┘内存管理涉及几个关键数据结构,它们构成了整个子系统的骨架:
每个物理页框都有一个 struct page 结构来描述。
// 源码位置: include/linux/mm_types.hstructpage {unsignedlong flags; /* 原子标志位,如 PG_locked, PG_dirty 等 */union {struct {/* Page cache 和匿名页 */structlist_headlru;/* LRU 链表 */structaddress_space *mapping;/* 映射的地址空间 */pgoff_t index; /* 在映射中的偏移 */unsignedlongprivate; /* 私有数据 */ };struct {/* Slab 分配器使用 */structlist_headslab_list;structkmem_cache *slab_cache;void *freelist;/* ... */ };struct {/* 复合页 (Compound Page) */unsignedlong compound_head;unsignedchar compound_dtor;unsignedchar compound_order;/* ... */ };/* ... 其他用途 ... */ };atomic_t _refcount; /* 引用计数 */atomic_t _mapcount; /* 映射计数 *//* ... */};物理内存被划分为不同的区域(Zone),以满足不同类型的内存分配需求。
// 源码位置: include/linux/mmzone.henum zone_type { ZONE_DMA, /* DMA 内存区域,用于 ISA 设备 */ ZONE_DMA32, /* 32位 DMA 设备可访问 */ ZONE_NORMAL, /* 常规内存,内核可直接映射 */ ZONE_HIGHMEM, /* 高端内存 (仅32位系统) */ ZONE_MOVABLE, /* 可移动内存,用于内存热插拔 */ ZONE_DEVICE, /* 设备内存 */ __MAX_NR_ZONES};structzone {unsignedlong watermark[NR_WMARK]; /* 水位线: min, low, high */unsignedlong nr_reserved_highatomic;structpglist_data *zone_pgdat;/* 所属节点 */structper_cpu_pageset __percpu *pageset;/* Per-CPU 页面缓存 *//* 空闲区域,伙伴系统的核心 */structfree_areafree_area[MAX_ORDER];unsignedlong zone_start_pfn; /* 起始页框号 */unsignedlong spanned_pages; /* 跨越的页数 */unsignedlong present_pages; /* 实际存在的页数 */constchar *name; /* 区域名称 *//* ... */};在 NUMA 系统中,每个节点有一个 pg_data_t 结构。
// 源码位置: include/linux/mmzone.htypedefstructpglist_data {structzonenode_zones[MAX_NR_ZONES];/* 该节点的所有 zone */structzonelistnode_zonelists[MAX_ZONELISTS];/* 分配回退列表 */int nr_zones; /* zone 数量 */unsignedlong node_start_pfn; /* 节点起始页框号 */unsignedlong node_present_pages; /* 节点中的页数 */unsignedlong node_spanned_pages; /* 跨越的页数 */int node_id; /* 节点 ID *//* 页面回收相关 */wait_queue_head_t kswapd_wait;structtask_struct *kswapd;/* kswapd 守护进程 *//* ... */} pg_data_t;描述一个进程的整个地址空间。
// 源码位置: include/linux/mm_types.hstructmm_struct {structvm_area_struct *mmap;/* VMA 链表 */structrb_rootmm_rb;/* VMA 红黑树 */pgd_t *pgd; /* 页全局目录 */atomic_t mm_users; /* 用户计数 */atomic_t mm_count; /* 引用计数 */unsignedlong start_code, end_code; /* 代码段 */unsignedlong start_data, end_data; /* 数据段 */unsignedlong start_brk, brk; /* 堆 */unsignedlong start_stack; /* 栈起始 */unsignedlong total_vm; /* 总页数 */unsignedlong locked_vm; /* 锁定页数 *//* ... */};描述进程地址空间中的一个连续区域。
// 源码位置: include/linux/mm_types.hstructvm_area_struct {unsignedlong vm_start; /* 起始地址 */unsignedlong vm_end; /* 结束地址 */structvm_area_struct *vm_next, *vm_prev;/* VMA 链表 */structrb_nodevm_rb;/* 红黑树节点 */structmm_struct *vm_mm;/* 所属的 mm_struct */pgprot_t vm_page_prot; /* 访问权限 */unsignedlong vm_flags; /* 标志位 */structfile *vm_file;/* 映射的文件 (如果是文件映射) */void *vm_private_data; /* 私有数据 */conststructvm_operations_struct *vm_ops;/* VMA 操作函数 *//* ... */}; ┌─────────────┐ │ 系统 │ └──────┬──────┘ │ ┌────────────────────────────┼────────────────────────────┐ │ │ │ ↓ ↓ ↓ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ pg_data_t │ │ pg_data_t │ │ pg_data_t │ │ (Node 0) │ │ (Node 1) │ │ (Node N) │ └──────┬──────┘ └──────┬──────┘ └─────────────┘ │ │ ┌─────────┼─────────┐ ┌─────────┼─────────┐ │ │ │ │ │ │ ↓ ↓ ↓ ↓ ↓ ↓┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐│ZONE_ │ │ZONE_ │ │ZONE_ │ │ZONE_ │ │ZONE_ │ │ZONE_ ││DMA │ │NORMAL │ │MOVABLE│ │DMA │ │NORMAL │ │MOVABLE│└───┬───┘ └───┬───┘ └───┬───┘ └───────┘ └───────┘ └───────┘ │ │ │ └─────────┼─────────┘ │ ↓ ┌─────────────────────┐ │ free_area[0..10] │ │ (伙伴系统空闲链表) │ └─────────────────────┘ │ ↓ ┌─────────────────────┐ │ struct page │ │ struct page │ │ ... │ │ (每个物理页框) │ └─────────────────────┘ ===== 进程视角 ===== ┌──────────────┐ │ task_struct │ │ (进程) │ └───────┬──────┘ │ ↓ ┌──────────────┐ │ mm_struct │ │ (地址空间) │ └───────┬──────┘ │ ┌──────────┼──────────┬──────────┐ │ │ │ │ ↓ ↓ ↓ ↓┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐│ VMA │ │ VMA │ │ VMA │ │ VMA ││(代码段)│ │(数据段)│ │ (堆) │ │ (栈) │└───────┘ └───────┘ └───────┘ └───────┘mm/ # 内存管理核心代码├── page_alloc.c # ⭐ 伙伴系统 (物理页分配)├── slub.c # ⭐ SLUB 分配器 (小对象分配)├── slab.c # SLAB 分配器├── slab_common.c # SLAB/SLUB 公共代码├── vmalloc.c # ⭐ vmalloc 实现├── mmap.c # ⭐ 进程地址空间管理, mmap 实现├── memory.c # ⭐ 缺页异常处理├── vmscan.c # ⭐ 页面回收 (kswapd)├── rmap.c # 反向映射├── compaction.c # 内存压缩├── migrate.c # 页面迁移├── oom_kill.c # OOM Killer├── swapfile.c # 交换文件管理├── swap_state.c # 交换缓存├── filemap.c # 文件映射, Page Cache├── shmem.c # 共享内存 (tmpfs)├── hugetlb.c # 大页支持├── huge_memory.c # 透明大页 (THP)├── khugepaged.c # 大页守护进程├── memcontrol.c # Memory Cgroup├── mempolicy.c # NUMA 内存策略├── percpu.c # Per-CPU 分配器├── memblock.c # 早期内存分配├── mm_init.c # 内存初始化├── internal.h # 内部头文件└── kasan/ # 内存调试工具 ├── kasan.c └── ...include/linux/ # 头文件├── mm.h # ⭐ 主要内存管理头文件├── mm_types.h # ⭐ 核心数据结构定义├── mmzone.h # ⭐ Zone 和 Node 定义├── gfp.h # GFP 分配标志├── page-flags.h # 页面标志定义├── slab.h # Slab 接口├── vmalloc.h # vmalloc 接口└── swap.h # 交换相关arch/arm64/ # ARM64 架构相关├── include/asm/│ ├── page.h # 页面定义│ ├── pgtable.h # 页表定义│ ├── memory.h # 内存布局│ └── ...└── mm/ ├── fault.c # 缺页异常处理 ├── mmu.c # MMU 初始化 └── ...mm/page_alloc.c | __alloc_pages_nodemask()__free_pages() | |
mm/slub.c | kmalloc()kfree(), kmem_cache_alloc() | |
mm/vmalloc.c | vmalloc()vfree() | |
mm/mmap.c | do_mmap()do_munmap(), find_vma() | |
mm/memory.c | handle_mm_fault()do_page_fault() | |
mm/vmscan.c | shrink_page_list()kswapd() | |
mm/rmap.c | page_referenced()try_to_unmap() | |
mm/compaction.c | compact_zone()migrate_pages() |
Linux 提供了多层次的内存分配接口:
┌─────────────────────────────────────────────────────────────────────────┐│ 用户空间 API ││ malloc/free (glibc) mmap/munmap brk/sbrk posix_memalign │└─────────────────────────────────────────────────────────────────────────┘ │ │ 系统调用 ↓┌─────────────────────────────────────────────────────────────────────────┐│ 内核高层 API ││ ││ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││ │ kmalloc() │ │ vmalloc() │ │ get_free │ ││ │ kzalloc() │ │ vzalloc() │ │ _pages() │ ││ │ krealloc() │ │ │ │ │ ││ │ kfree() │ │ vfree() │ │ free_pages()│ ││ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ ││ │ │ │ ││ 物理连续,小内存 虚拟连续,大内存 物理连续,按页分配 │└─────────┼─────────────────┼─────────────────┼──────────────────────────┘ │ │ │ ↓ ↓ ↓┌─────────────────────────────────────────────────────────────────────────┐│ 内核底层 API ││ ││ ┌────────────────────┐ ┌────────────────────┐ ││ │ Slab/Slub 分配器 │ │ 伙伴系统 │ ││ │ kmem_cache_alloc() │ │ alloc_pages() │ ││ │ kmem_cache_free() │ │ __free_pages() │ ││ └─────────┬──────────┘ └──────────┬─────────┘ ││ │ │ ││ └────────────────────────────┘ ││ │ ││ ↓ ││ ┌────────────────────────┐ ││ │ __alloc_pages_nodemask │ ││ │ (核心分配函数) │ ││ └────────────────────────┘ │└─────────────────────────────────────────────────────────────────────────┘kmalloc()kzalloc() | ||
kmem_cache_create() | ||
vmalloc() | ||
alloc_pages()__get_free_pages() | ||
dma_alloc_coherent() | ||
alloc_percpu() |
GFP (Get Free Pages) 标志控制内存分配的行为:
// 源码位置: include/linux/gfp.h/* 基本标志 */#define __GFP_DMA /* 从 ZONE_DMA 分配 */#define __GFP_HIGHMEM /* 可以使用高端内存 */#define __GFP_MOVABLE /* 可移动的页面 *//* 行为修饰符 */#define __GFP_WAIT /* 可以睡眠等待 */#define __GFP_IO /* 可以启动 I/O */#define __GFP_FS /* 可以调用文件系统 */#define __GFP_NOWARN /* 失败时不打印警告 */#define __GFP_RETRY_MAYFAIL /* 可以重试,但可能失败 */#define __GFP_NOFAIL /* 不允许失败,无限重试 */#define __GFP_ZERO /* 分配后清零 *//* 常用组合 */#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS)/* 最常用,可睡眠,可回收 */#define GFP_ATOMIC (__GFP_HIGH | __GFP_ATOMIC)/* 原子上下文,不可睡眠 */#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)/* 用户空间分配 */#define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM)/* 用户空间,可用高端内存 */ ┌─────────────────┐ │ 需要分配内存? │ └────────┬────────┘ │ ┌──────────────┴──────────────┐ │ │ ↓ ↓ ┌─────────────────┐ ┌─────────────────┐ │ 可以睡眠? │ │ 不可睡眠 │ │ (进程上下文) │ │ (中断/软中断) │ └────────┬────────┘ └────────┬────────┘ │ │ ↓ ↓ ┌─────────────────┐ ┌─────────────────┐ │ GFP_KERNEL │ │ GFP_ATOMIC │ │ GFP_USER │ │ GFP_NOWAIT │ └─────────────────┘ └─────────────────┘系统启动时,内存管理子系统按以下顺序初始化:
启动流程 (start_kernel) │ ↓┌───────────────────┐│ 1. setup_arch() │ 架构相关初始化│ - 解析内存信息 ││ - memblock 初始化│└─────────┬─────────┘ │ ↓┌───────────────────┐│ 2. mm_init() │ 内存管理初始化│ - mem_init() │ 释放空闲内存到伙伴系统│ - kmem_cache_ │ 初始化 Slab 分配器│ init() │└─────────┬─────────┘ │ ↓┌───────────────────┐│ 3. vmalloc_init() │ vmalloc 初始化└─────────┬─────────┘ │ ↓┌───────────────────┐│ 4. pagecache_init()│ Page Cache 初始化└─────────┬─────────┘ │ ↓┌───────────────────┐│ 5. kswapd 启动 │ 页面回收守护进程└───────────────────┘本章介绍了 Linux 内核内存管理的整体架构:
struct page、struct zone、pg_data_t、mm_struct、vm_area_structmm/page_alloc.c | ||
mm/slub.c | ||
mm/mmap.c | ||
mm/memory.c | ||
mm/vmscan.c |
第二章:物理内存组织 将深入讲解:
pg_data_t、struct zone、struct page 的完整解析虚拟内存是一种内存管理技术,为每个进程提供一个独立的、连续的虚拟地址空间,与物理内存解耦。
Linux 使用虚拟内存的原因:
Linux 内核内存管理的主要职责包括:
物理内存管理:
虚拟内存管理:
内存分配器:
内存回收与交换:
内存保护:
主要区别:
划分方式(ARM64 为例):
0x0000_0000_0000_0000 ~ 0x0000_FFFF_FFFF_FFFF (低 256TB)0xFFFF_0000_0000_0000 ~ 0xFFFF_FFFF_FFFF_FFFF (高 256TB)实现机制:
**页 (Page)**:内存管理的基本单位,典型大小为 4KB (4096 字节)。
以页为单位管理的原因:
struct page 的作用:内核为每个物理页框维护一个 struct page 结构,记录页面状态、引用计数、映射信息等。
好处:
| 进程隔离 | |
| 内存保护 | |
| 虚拟地址连续 | |
| 按需分配 | |
| 共享内存 | |
| 交换空间 | |
| 写时复制 (COW) |
代价:
| 地址转换开销 | |
| TLB Miss 惩罚 | |
| 页表内存占用 | |
| Page Fault 开销 | |
| 内存碎片 |
单级页表的问题:
以 32 位系统、4KB 页大小为例:
虚拟地址空间:4GB = 2^32 字节页大小:4KB = 2^12 字节页表项数:2^32 / 2^12 = 2^20 = 1M 个每个 PTE 4 字节 → 页表大小 = 4MB / 进程问题:
多级页表的优势:
单级页表 多级页表 ┌────────────────┐ ┌─────────┐ │ PTE 0 │ │ PGD │ 4KB │ PTE 1 │ └────┬────┘ │ PTE 2 │ │ │ ... │ ┌─────────┼─────────┐ │ (大量未使用) │ ↓ ↓ ↓ │ ... │ ┌─────┐ ┌─────┐ NULL │ PTE 1M │ │PMD 0│ │PMD 1│ (节省!) └────────────────┘ └──┬──┘ └─────┘ 4MB/进程 │ ↓ ┌─────┐ │ PTE │ 只有实际使用的才分配 └─────┘优势:
64 位系统的多级页表:
Level 名称 ARM64 x86-64───────────────────────────────────L0 PGD 9 bits 9 bitsL1 PUD 9 bits 9 bitsL2 PMD 9 bits 9 bitsL3 PTE 9 bits 9 bits─────────────────────────────────── Page Offset 12 bits 12 bitskmalloc、vmalloc、__get_free_pages 有什么区别?分别在什么场景使用?kmalloc | ||||
vmalloc | ||||
__get_free_pages |
详细对比:
┌──────────────────────────────────────────────────────────┐│ kmalloc (Slab 分配器) │├──────────────────────────────────────────────────────────┤│ • 物理连续,虚拟连续 ││ • 基于伙伴系统 + Slab 缓存 ││ • 分配粒度:8, 16, 32, ... 字节 ││ • 适用:内核数据结构、DMA buffer ││ • 示例:kmalloc(sizeof(struct task_struct), GFP_KERNEL) │└──────────────────────────────────────────────────────────┘┌──────────────────────────────────────────────────────────┐│ vmalloc (非连续分配) │├──────────────────────────────────────────────────────────┤│ • 物理不连续,虚拟连续 ││ • 需要修改页表建立映射 ││ • 开销较大,但可分配大块内存 ││ • 适用:模块加载、大数组 ││ • 示例:vmalloc(1 * 1024 * 1024) // 1MB │└──────────────────────────────────────────────────────────┘┌──────────────────────────────────────────────────────────┐│ __get_free_pages (直接页分配) │├──────────────────────────────────────────────────────────┤│ • 直接使用伙伴系统 ││ • 分配 2^order 个连续物理页 ││ • 适用:需要精确控制页数 ││ • 示例:__get_free_pages(GFP_KERNEL, 2) // 4 页 │└──────────────────────────────────────────────────────────┘调用层次:
kmalloc() │ ├── size <= KMALLOC_MAX_CACHE_SIZE │ └── Slab/Slub 分配器 │ └── size > KMALLOC_MAX_CACHE_SIZE └── __get_free_pages() │ └── alloc_pages() → 伙伴系统GFP_KERNEL 和 GFP_ATOMIC 的区别是什么?什么情况下必须用 GFP_ATOMIC?GFP 标志对比:
GFP_KERNEL | |||
GFP_ATOMIC | |||
GFP_NOIO | |||
GFP_NOFS | |||
GFP_USER |
详细解释:
// GFP_KERNEL:最常用的标志// - 可以睡眠等待内存// - 可以触发内存回收(kswapd、直接回收)// - 可以触发交换void *ptr = kmalloc(size, GFP_KERNEL);// GFP_ATOMIC:紧急分配// - 绝对不能睡眠// - 使用紧急预留内存池// - 分配失败概率较高void *ptr = kmalloc(size, GFP_ATOMIC);必须使用 GFP_ATOMIC 的场景:
// 1. 中断处理函数irqreturn_tmy_irq_handler(int irq, void *dev_id){// 中断上下文不能睡眠!void *buf = kmalloc(size, GFP_ATOMIC);// ...}// 2. 持有自旋锁时spin_lock(&my_lock);// 持有自旋锁时禁止睡眠void *ptr = kmalloc(size, GFP_ATOMIC);spin_unlock(&my_lock);// 3. softirq / taskletvoidmy_tasklet_func(unsignedlong data){// 软中断上下文void *buf = kmalloc(size, GFP_ATOMIC);}// 4. 禁用抢占的代码段preempt_disable();void *ptr = kmalloc(size, GFP_ATOMIC);preempt_enable();判断规则:
当前上下文能否睡眠? │ ├── 能睡眠 → GFP_KERNEL │ (进程上下文,无自旋锁) │ └── 不能睡眠 → GFP_ATOMIC (中断、自旋锁、禁用抢占)内存碎片类型:
┌─────────────────────────────────────────────────────────┐│ 内部碎片 │├─────────────────────────────────────────────────────────┤│ 分配的内存块 > 实际需要的内存 ││ ││ 示例:申请 100 字节,Slab 分配 128 字节 ││ ┌────────────────────────────────────┐ ││ │██████████████████████│░░░░░░░░░░░░│ ││ │ 实际使用 100B │ 浪费 28B │ ││ └────────────────────────────────────┘ │└─────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────┐│ 外部碎片 │├─────────────────────────────────────────────────────────┤│ 空闲内存总量足够,但不连续 ││ ││ 空闲总量:12 页,但最大连续只有 2 页 ││ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ ││ │░░│██│░░│░░│██│██│░░│░░│██│░░│░░│░░│██│░░│░░│██│ ││ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ ││ ░░ = 空闲 ██ = 已用 ││ ││ 无法分配连续 4 页的请求! │└─────────────────────────────────────────────────────────┘Linux 的解决方案:
| Slab 分配器 | ||
| 伙伴系统 | ||
| 内存压缩 | ||
| 迁移类型 | ||
| 大页 (Huge Page) |
伙伴系统的合并:
释放页 A 检查伙伴 B │ │ ▼ ▼┌───────┐ ┌───────┐│ A (空)│ │ B (空)│ 都空闲?└───┬───┘ └───┬───┘ │ │ └────────┬────────┘ ▼ ┌─────────────┐ │ 合并为 2 页 │ 继续检查更大的伙伴... └─────────────┘**内存规整 (Compaction)**:
压缩只能移动 MOVABLE 页面,UNMOVABLE 页面位置不变:
规整前:┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐│M │U │░░│M │░░│░░│M │░░│░░│M │░░│M │░░│░░│░░│░░│└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15M = Movable, U = Unmovable, ░░ = Free规整后(M 向左聚集,U 位置不变):┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐│M │U │M │M │M │M │░░│░░│░░│░░│░░│░░│░░│░░│░░│░░│└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ↑ ↑ U 位置不变 连续 10 页空闲!注意:正因为 UNMOVABLE 页面无法移动,所以:
COW 原理:
fork() 中的 COW:
fork() 调用前: fork() 调用后(使用 COW):┌─────────────────┐ ┌─────────────────┐│ 父进程页表 │ │ 父进程页表 ││ ┌───────────┐ │ │ ┌───────────┐ ││ │ VA → PA │───┼──┐ │ │ VA → PA │───┼──┐│ │ (R/W) │ │ │ │ │ (R only) │ │ │ 改为只读│ └───────────┘ │ │ │ └───────────┘ │ │└─────────────────┘ │ └─────────────────┘ │ │ │ ▼ │ 共享! ┌──────────┐ │ │ 物理页面 │ ◄──────────────────────┘ │ Data │ ◄──────────────────────┐ └──────────┘ │ │ ┌─────────────────┐ │ │ 子进程页表 │ │ │ ┌───────────┐ │ │ │ │ VA → PA │───┼──┘ │ │ (R only) │ │ 只读 │ └───────────┘ │ └─────────────────┘写入触发复制:
子进程尝试写入: ┌─────────────────┐1. 写入只读页 ─────────────► │ Page Fault! │ └────────┬────────┘ │ ▼ ┌─────────────────┐2. 分配新物理页 ──────────► │ new_page = alloc│ └────────┬────────┘ │ ▼ ┌─────────────────┐3. 复制原页内容 ──────────► │ copy(new, old) │ └────────┬────────┘ │ ▼ ┌─────────────────┐4. 更新页表(可写)─────────► │ pte = new | RW │ └─────────────────┘结果:
父进程页表 子进程页表 ┌───────────┐ ┌───────────┐ │ VA → PA1 │ │ VA → PA2 │ │ (R/W) │ │ (R/W) │ └─────┬─────┘ └─────┬─────┘ │ │ ▼ ▼ ┌──────────┐ ┌──────────┐ │ 物理页 1 │ │ 物理页 2 │ │ (原数据) │ │ (复制) │ └──────────┘ └──────────┘COW 的好处:
缺页异常定义:
| Minor Fault | |||
| Major Fault | |||
| Invalid Fault |
| Demand Paging | ||
| Copy-On-Write | ||
| Swap In | ||
| Page Cache Miss | ||
| Stack Growth | ||
| Lazy Allocation |
访问虚拟地址 VA │ ▼ ┌─────────────────┐ │ MMU 查找页表 PTE │ └────────┬────────┘ │ ┌───────────────────┼───────────────────┐ │ │ │ ▼ ▼ ▼ PTE 有效 & 权限OK PTE 无效/不存在 PTE 有效但权限不符 │ │ │ ▼ ▼ ▼ 正常访问完成 ┌──── Page Fault! ────┐ │ │ ▼ ▼ 进入内核态 写只读页? do_page_fault() │ │ ▼ ▼ COW 处理 ┌──────────────┐ do_wp_page() │ 查找 VMA │ │ find_vma() │ └──────┬───────┘ │ ┌───────────────┼───────────────┐ │ │ │ ▼ ▼ ▼ VA < vma->start VA 在 VMA 内 无 VMA 覆盖 │ │ │ ▼ ▼ ▼ 检查栈扩展? 检查权限 SIGSEGV │ │ (段错误) ▼ │ expand_stack() │ │ │ └───────┬───────┘ │ ▼ ┌───────────────┐ │ 权限检查通过? │ └───────┬───────┘ │ ┌───────┴───────┐ │ │ ▼ ▼ 通过 不通过 │ │ ▼ ▼ handle_mm_fault SIGSEGV │ ▼ ┌─────────────────────────────────────────┐ │ 根据页面状态分发 │ └─────────────────────────────────────────┘ │ ├─── PTE 不存在 ──────────────────────┐ │ │ │ ┌────────────────┬───────────────┤ │ │ │ │ │ ▼ ▼ ▼ │ 匿名映射 文件映射 共享映射 │ │ │ │ │ ▼ ▼ ▼ │ do_anonymous_page do_fault do_shared_fault │ │ │ │ │ ▼ ▼ ▼ │ 分配零页 读取文件 检查权限 │ 到 Page Cache 建立映射 │ ├─── PTE 存在但不在内存 (Swap) ───────┐ │ │ │ ▼ │ │ do_swap_page │ │ │ │ │ ▼ │ │ 从交换空间读入 │ │ │ └─── PTE 存在但只读 (写操作) ─────────┐ │ │ ▼ │ do_wp_page (COW) │ │ │ ▼ │ 复制页面,建立可写映射 │ │ ▼ ┌─────────────────┐ │ 更新页表,返回 │ │ 用户态重试访问 │ └─────────────────┘// 用户代码void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);// 此时:VMA 已创建,但 PTE 未建立,物理页未分配ptr[0] = 'A'; // 触发 Page Fault!内核处理:
Page Fault → do_anonymous_page() │ ├── 1. alloc_zeroed_user_highpage() 分配零页 │ ├── 2. 设置 PTE: pte = mk_pte(page, vma->vm_page_prot) │ └── 3. set_pte_at() 写入页表结果:Minor Fault,微秒级完成
// 父进程int *data = malloc(4096);*data = 100;pid_t pid = fork(); // 子进程创建,页面标记为只读共享if (pid == 0) {// 子进程 *data = 200; // 触发 COW Page Fault!}内核处理:
写入只读页 → do_wp_page() │ ├── 1. 检查是否为 COW 页(page_mapcount > 1 或 PageKsm) │ ├── 2. alloc_page() 分配新页 │ ├── 3. copy_user_highpage() 复制内容 │ ├── 4. 减少原页引用计数 │ └── 5. 设置新 PTE 为可写结果:Minor Fault,父子进程各有独立页面
// 内存紧张时,某页面被换出到 swap// 之后程序再次访问该地址int value = *some_swapped_ptr; // 触发 Major Page Fault!内核处理:
PTE 显示页面在 swap → do_swap_page() │ ├── 1. 从 PTE 获取 swap entry │ ├── 2. lookup_swap_cache() 检查 swap cache │ │ │ ├── 命中:直接使用缓存页 │ │ │ └── 未命中: │ │ │ ├── alloc_page() 分配页面 │ │ │ └── swap_readpage() 从磁盘读取 │ │ │ └── 阻塞等待 I/O 完成 │ ├── 3. 更新 PTE 指向物理页 │ └── 4. swap_free() 释放 swap 槽位(如果不再需要)结果:Major Fault,毫秒级(涉及磁盘 I/O)
int fd = open("data.txt", O_RDONLY);char *file = mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 0);char c = file[0]; // 触发 Page Fault!内核处理:
文件映射缺页 → do_fault() → do_read_fault() │ ├── 1. 查找 Page Cache │ │ │ ├── 命中:Minor Fault,直接映射 │ │ │ └── 未命中: │ │ │ ├── alloc_page() 分配页面 │ │ │ ├── add_to_page_cache() 加入缓存 │ │ │ └── readpage() 从文件读取 │ │ │ └── 阻塞等待 I/O │ └── 2. 建立 PTE 映射到 Page Cache 页面结果:可能是 Minor(缓存命中)或 Major(需要读文件)
voiddeep_recursion(int n){char buffer[4096]; // 栈上分配if (n > 0) deep_recursion(n - 1); // 递归,栈不断增长}// 当栈增长超出当前映射范围时,触发 Page Fault内核处理:
访问地址 < vma->vm_start(栈向下增长) │ ├── 检查是否可以扩展栈 │ │ │ ├── 地址在 vma->vm_start - 栈保护页 范围内? │ │ │ ├── 未超过 RLIMIT_STACK 限制? │ │ │ └── 未与其他 VMA 冲突? │ ├── expand_stack() │ │ │ └── 更新 vma->vm_start │ └── 继续正常的缺页处理(分配页面)结果:Minor Fault(或 SIGSEGV 如果超出限制)
int *ptr = NULL;*ptr = 42; // 访问地址 0,触发 Page Fault内核处理:
地址 0 (或很低的地址) │ ├── find_vma() 找不到覆盖此地址的 VMA │ ├── 检查是否可以栈扩展:不可以(地址太低) │ └── bad_area() → 发送 SIGSEGV结果:进程收到 SIGSEGV 信号,默认终止并产生 core dump
// arch/x86/mm/fault.c 或 arch/arm64/mm/fault.cdo_page_fault() │ └── handle_mm_fault() // mm/memory.c │ └── __handle_mm_fault() │ └── handle_pte_fault() │ ├── do_anonymous_page() // 匿名页首次访问 │ ├── do_fault() // 文件映射缺页 │ ├── do_read_fault() │ ├── do_cow_fault() │ └── do_shared_fault() │ ├── do_swap_page() // 换入页面 │ ├── do_wp_page() // COW 写保护 │ └── do_numa_page() // NUMA 迁移# 查看进程的缺页统计cat /proc/[pid]/stat | awk '{print "minflt:", $10, "majflt:", $12}'# 使用 time 命令/usr/bin/time -v ./program 2>&1 | grep "page faults"# 使用 perf 分析perf stat -e page-faults,minor-faults,major-faults ./program# 实时监控系统缺页vmstat 1 # 查看 si/so (swap in/out) 和 bi/bo (block I/O)# 详细跟踪(需要 root)perf record -e page-faults ./programperf report概念:
优点:
工作流程:
1. mmap() 调用:只创建 VMA,不分配物理页 ┌─────────────────────────────────────┐ │ 进程地址空间 │ │ ┌────────────────────────────────┐ │ │ │ VMA: 0x7f000000 - 0x7f100000 │ │ 虚拟地址已分配 │ │ 权限: R/W │ │ │ │ 物理页: 无 │ │ 物理页未分配! │ └────────────────────────────────┘ │ └─────────────────────────────────────┘2. 首次访问:触发 Page Fault ┌────────────────────────────────────────┐ │ CPU: 访问 0x7f000000 │ │ │ │ │ ▼ │ │ 页表查找: PTE 无效! │ │ │ │ │ ▼ │ │ Page Fault → 内核处理 │ └────────────────────────────────────────┘3. 内核处理:分配物理页,建立映射 ┌────────────────────────────────────────┐ │ 1. 检查 VMA:地址合法 │ │ 2. 分配物理页:alloc_page() │ │ 3. 初始化页面:清零或从文件读取 │ │ 4. 更新页表:建立 VA → PA 映射 │ │ 5. 返回用户空间:重新执行访问指令 │ └────────────────────────────────────────┘4. 后续访问:正常进行,无 Page Fault示例:
// 分配 1GB 虚拟内存void *ptr = mmap(NULL, 1GB, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);// 此时物理内存使用:0 字节// 访问第一个页面ptr[0] = 'A';// Page Fault → 分配 1 页(4KB)物理内存// 访问第 1000 个页面ptr[4096 * 1000] = 'B';// Page Fault → 再分配 1 页物理内存// 实际使用:8KB(而不是 1GB)根本原因:虚拟地址空间只是"潜在可用"的范围,实际物理内存是按需分配的。
依赖的关键机制:
按需分页(Demand Paging):
延迟分配(Lazy Allocation):
// malloc(1GB) 后VmSize: 1GB+ // 虚拟内存增加VmRSS: ~0// 物理内存几乎不变**Copy-on-Write (COW)**:
内存回收与交换:
内存映射复用:
具体示例:
进程视角:- 代码段: 10MB- 堆: 声明 1GB,实际用 50MB- mmap: 映射 2GB 文件,只访问 100MB- 栈: 预留 8MB,实际用 1MB虚拟内存: ~3GB物理内存: ~160MB (仅访问过的页)GFP_KERNEL 分配内存会发生什么?内核如何检测这种错误?问题分析:
GFP_KERNEL 包含 __GFP_RECLAIM 和 __GFP_IO,允许:
中断上下文的限制:
可能发生的问题:
内核检测机制:
might_sleep() 检测:
voidmight_sleep(void){if (in_atomic() || irqs_disabled()) {// 打印警告和调用栈 WARN_ON(1); }}__GFP_DIRECT_RECLAIM 检查:
if (gfp_mask & __GFP_DIRECT_RECLAIM) { might_sleep_if(true);}CONFIG_DEBUG_ATOMIC_SLEEP:
正确做法:
// 中断上下文必须使用ptr = kmalloc(size, GFP_ATOMIC);// 或ptr = kmalloc(size, GFP_NOWAIT);vm_area_struct 和 struct page 分别管理什么?它们之间有什么关系?vm_area_struct(VMA):
struct page:
两者的关系:
用户视角 (VMA): 内核视角 (page):┌─────────────────┐ ┌─────────────────┐│ VMA: 0x1000-0x3000 │ │ page[pfn=0x100]││ 虚拟地址范围 │ │ 物理页框 ││ 权限、映射类型 │ │ 状态、引用计数 │└────────┬────────┘ └────────┬────────┘ │ │ │ 通过页表 (PTE) 建立关联 │ └──────────────────────────────────┘VMA (多个) ──页表映射──> page (一个) 一对多关系(共享页面)一个 page 可被多个 VMA 映射:- 共享库被多个进程映射- fork 后父子进程共享页面- 共享内存关键操作中的协作:
kmalloc 分配的最大内存通常限制在 4MB 左右?如何分配更大的连续物理内存?4MB 限制的原因:
伙伴系统限制:
#define MAX_ORDER 11 // 最大阶数// 最大分配 = 2^10 * 4KB = 4MB碎片化问题:
设计权衡:
分配更大连续物理内存的方法:
vmalloc(虚拟连续,物理不连续):
void *p = vmalloc(16 * 1024 * 1024); // 16MB// 适用于不需要物理连续的场景CMA(Contiguous Memory Allocator):
// 设备树或 bootargs 预留cma=256M// 分配page = cma_alloc(cma_dev, count, align, gfp);// 适用于 DMA 设备大页内存(HugeTLB):
// 预留大页echo 100 > /proc/sys/vm/nr_hugepages// 分配mmap(NULL, 2*1024*1024, ..., MAP_HUGETLB, ...);启动时预留(memblock):
// 在内核启动早期预留memblock_reserve(base, size);// 内存碎片化之前预留DMA API:
dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);// 自动选择合适的分配方式内存隔离实现机制:
独立的页表:
进程 A: mm_struct_A -> pgd_A -> 私有页表进程 B: mm_struct_B -> pgd_B -> 私有页表相同虚拟地址 -> 不同物理页地址空间切换:
switch_mm(old_mm, new_mm, next) {// 更新 TTBR0(ARM64)或 CR3(x86)// 切换到新进程的页表 load_new_mm_cr3(next->mm->pgd);}MMU 硬件保护:
用户/内核空间隔离:
能否访问另一个进程的内存?
正常情况下不能,但有合法途径:
共享内存:
// 两个进程都 mmap 同一 shmmmap(NULL, size, ..., MAP_SHARED, shm_fd, 0);ptrace 系统调用:
// 调试器可以读写目标进程内存ptrace(PTRACE_PEEKDATA, pid, addr, NULL);ptrace(PTRACE_POKEDATA, pid, addr, data);/proc/[pid]/mem:
# root 可以读取其他进程内存cat /proc/1234/memprocess_vm_readv/writev:
// 跨进程内存读写(需要权限)process_vm_readv(pid, local_iov, ..., remote_iov, ...);内核模块:
安全机制:
编写一个简单程序,验证 Demand Paging:
#include<stdio.h>#include<stdlib.h>#include<sys/mman.h>#include<unistd.h>intmain(){size_t size = 100 * 1024 * 1024; // 100MBprintf("Before mmap, press Enter...\n"); getchar(); // 查看 /proc/[pid]/status 的 VmRSSvoid *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);printf("After mmap, before access, press Enter...\n"); getchar(); // VmRSS 几乎不变// 访问每个页面for (size_t i = 0; i < size; i += 4096) { ((char *)ptr)[i] = 'A'; }printf("After access, press Enter...\n"); getchar(); // VmRSS 增加约 100MB munmap(ptr, size);return0;}使用 /proc/[pid]/maps 分析进程的内存布局:
# 查看某进程的内存区域cat /proc/$(pidof bash)/maps# 分析各区域的用途:# - [heap]:堆# - [stack]:栈# - libc.so:共享库# - [vdso]:虚拟动态共享对象观察 Page Fault 统计:
# 方法 1:使用 time 命令/usr/bin/time -v ls# 查看 "Minor page faults" 和 "Major page faults"# 方法 2:使用 perfperf stat -e page-faults ./your_program场景分析:一个应用程序调用 malloc(1GB) 后立即 memset() 填充数据,系统变得非常卡顿。请分析可能的原因和优化方案。
原因分析:
优化方案:
MAP_POPULATE | |
mlock() | |
madvise(MADV_WILLNEED) | |
代码示例:
// 使用 MAP_POPULATE 预分配void *ptr = mmap(NULL, 1GB, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE,-1, 0);// 或使用大页void *ptr = mmap(NULL, 1GB, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,-1, 0);malloc(1GB) 只分配虚拟地址,不分配物理页memset() 顺序访问每个页面