当前位置:首页>Linux>内存管理-第1章-Linux 内核内存管理概述

内存管理-第1章-Linux 内核内存管理概述

  • 2026-10-12 09:38:11
内存管理-第1章-Linux 内核内存管理概述

本章将带你从宏观视角理解 Linux 内存管理子系统的设计哲学、整体架构和核心组件。


1.1 为什么内存管理如此重要?

内存管理是操作系统内核最核心、最复杂的子系统之一。它直接影响着:

  • 系统性能:内存分配效率决定了程序运行速度
  • 系统稳定性:错误的内存管理会导致系统崩溃
  • 资源利用率:高效的内存管理能让有限的物理内存发挥最大价值
  • 安全性:内存隔离是进程保护的基础
┌─────────────────────────────────────────────────────────────────────┐│                         应用程序 (Application)                        │├─────────────────────────────────────────────────────────────────────┤│                    用户空间内存管理 (User Space)                       ││         malloc/free, mmap, brk, 共享内存, 内存映射文件                  │├─────────────────────────────────────────────────────────────────────┤│                    系统调用接口 (System Call)                          ││              mmap, munmap, mprotect, brk, madvise...                 │├─────────────────────────────────────────────────────────────────────┤│                                                                     ││                   Linux 内核内存管理子系统                             ││   ┌──────────────────────────────────────────────────────────────┐  ││   │  虚拟内存管理 │ 物理内存管理 │ 页面回收 │ 内存映射 │ 交换系统  │  ││   └──────────────────────────────────────────────────────────────┘  ││                                                                     │├─────────────────────────────────────────────────────────────────────┤│                         硬件抽象层 (HAL)                              ││                    MMU, TLB, Cache, 物理内存                          │└─────────────────────────────────────────────────────────────────────┘

1.2 Linux 内存管理的设计哲学

💡 核心设计原则

1. 虚拟内存抽象

每个进程都拥有独立的、连续的虚拟地址空间,与物理内存解耦。

进程A的视角                    进程B的视角┌──────────────┐              ┌──────────────┐│ 0xFFFFFFFF   │              │ 0xFFFFFFFF   ││    内核空间   │              │    内核空间   │├──────────────┤              ├──────────────┤│              │              │              ││    栈 Stack  │              │    栈 Stack  ││      ↓       │              │      ↓       ││              │              │              ││      ↑       │              │      ↑       ││ 堆 Heap/mmap │              │ 堆 Heap/mmap ││              │              │              │├──────────────┤              ├──────────────┤│   数据段     │              │   数据段      │├──────────────┤              ├──────────────┤│   代码段     │              │   代码段      │├──────────────┤              ├──────────────┤│ 0x00000000   │              │ 0x00000000   │└──────────────┘              └──────────────┘        │                            │        └────────────┬───────────────┘                     ↓            ┌────────────────┐            │   物理内存      │            │ (被多个进程共享) │            └────────────────┘

2. 按需分配 (Demand Paging)

不立即分配物理内存,而是在真正访问时才分配,节省内存资源。

3. 写时复制 (Copy-on-Write)

fork 时不复制页面,只在写入时才真正复制,提高 fork 效率。

4. 页面共享

相同内容(如共享库)只在物理内存中保存一份。

5. 内存回收与交换

当内存紧张时,可以回收不常用的页面或将其交换到磁盘。


1.3 物理内存 vs 虚拟内存

物理内存 (Physical Memory)

物理内存是计算机实际的 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 架构支持):

  • 4KB (最常用)
  • 16KB
  • 64KB

虚拟内存 (Virtual Memory)

虚拟内存是操作系统提供的抽象,每个进程看到的都是独立的地址空间。

                虚拟地址                          物理地址            ┌────────────┐                    ┌────────────┐            │ 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 │   基址 + 偏移           └───────┘ └───────┘ └───────┘ └───────┘

1.4 内存管理子系统架构全景图

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   │└─────────────────────────────────────────────────────────────────────────┘

1.5 核心数据结构概览

内存管理涉及几个关键数据结构,它们构成了整个子系统的骨架:

🔷 struct page - 页描述符

每个物理页框都有一个 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;         /* 映射计数 *//* ... */};

🔷 struct zone - 内存区域

物理内存被划分为不同的区域(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;                   /* 区域名称 *//* ... */};

🔷 struct pglist_data (pg_data_t) - NUMA 节点

在 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;

🔷 struct mm_struct - 进程内存描述符

描述一个进程的整个地址空间。

// 源码位置: 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;            /* 锁定页数 *//* ... */};

🔷 struct vm_area_struct - 虚拟内存区域

描述进程地址空间中的一个连续区域。

// 源码位置: 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   ││(代码段)│  │(数据段)│  │ (堆)  │  │ (栈)  │└───────┘  └───────┘  └───────┘  └───────┘

1.6 核心源文件导航指南

📁 主要目录结构

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()

1.7 内存分配 API 层次

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 │                                ││              │   (核心分配函数)         │                                ││              └────────────────────────┘                                │└─────────────────────────────────────────────────────────────────────────┘

API 使用选择指南

场景
推荐 API
说明
小于 PAGE_SIZE 的小对象
kmalloc()
 / kzalloc()
物理连续,效率最高
频繁分配释放的同类对象
kmem_cache_create()
专用缓存,减少碎片
需要大块虚拟连续内存
vmalloc()
物理可不连续
需要整页分配
alloc_pages()
 / __get_free_pages()
直接使用伙伴系统
DMA 缓冲区
dma_alloc_coherent()
满足设备 DMA 要求
Per-CPU 数据
alloc_percpu()
避免缓存行竞争

1.8 GFP 标志:分配行为控制

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 使用决策流程

                    ┌─────────────────┐                    │  需要分配内存?   │                    └────────┬────────┘                             │              ┌──────────────┴──────────────┐              │                             │              ↓                             ↓    ┌─────────────────┐          ┌─────────────────┐    │   可以睡眠?      │          │   不可睡眠       │    │ (进程上下文)      │          │ (中断/软中断)    │    └────────┬────────┘          └────────┬────────┘             │                            │             ↓                            ↓    ┌─────────────────┐          ┌─────────────────┐    │   GFP_KERNEL    │          │   GFP_ATOMIC    │    │   GFP_USER      │          │   GFP_NOWAIT    │    └─────────────────┘          └─────────────────┘

1.9 内存管理初始化流程

系统启动时,内存管理子系统按以下顺序初始化:

启动流程 (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 启动     │  页面回收守护进程└───────────────────┘

1.10 本章小结

本章介绍了 Linux 内核内存管理的整体架构:

✅ 核心概念回顾

  1. 虚拟内存:为每个进程提供独立、连续的地址空间
  2. 物理内存组织:Node → Zone → Page 的层次结构
  3. 页表:实现虚拟地址到物理地址的转换
  4. 核心数据结构:struct page、struct zone、pg_data_t、mm_struct、vm_area_struct

✅ 关键子系统

子系统
功能
核心文件
伙伴系统
物理页分配
mm/page_alloc.c
Slab/Slub
小对象分配
mm/slub.c
进程地址空间
VMA 管理
mm/mmap.c
缺页处理
按需分配
mm/memory.c
页面回收
内存回收
mm/vmscan.c

📚 下一章预告

第二章:物理内存组织 将深入讲解:

  • NUMA 架构详解
  • pg_data_t、struct zone、struct page 的完整解析
  • 内存模型 (FLATMEM/SPARSEMEM)
  • 伙伴系统的数据结构

1.11 思考题

📝 基础概念题

  1. 什么是虚拟内存?Linux 为什么要使用虚拟内存?
💡 参考答案

虚拟内存是一种内存管理技术,为每个进程提供一个独立的、连续的虚拟地址空间,与物理内存解耦。

Linux 使用虚拟内存的原因:

  1. 进程隔离:每个进程有独立的地址空间,一个进程的错误不会影响其他进程
  2. 内存保护:通过页表权限位防止非法访问内核空间或其他进程内存
  3. 简化编程:程序员可以假设连续的地址空间,无需关心物理内存分布
  4. 共享机制:多进程可以映射同一物理页(共享库、共享内存)
  5. 按需分配:Demand Paging 延迟分配物理内存,提高利用率
  6. 交换扩展:可将不活跃页换出到磁盘,支持大于物理内存的工作集
  1. Linux 内核内存管理的主要职责有哪些?
💡 参考答案

Linux 内核内存管理的主要职责包括:

  1. 物理内存管理:

    • 追踪物理页框的使用状态
    • 伙伴系统管理页面分配/释放
    • 处理 NUMA 节点和内存 Zone
  2. 虚拟内存管理:

    • 管理进程地址空间 (mm_struct、VMA)
    • 页表创建、修改和销毁
    • 处理缺页异常
  3. 内存分配器:

    • Slab/SLUB 分配小对象
    • vmalloc 分配大块虚拟连续内存
    • Per-CPU 变量管理
  4. 内存回收与交换:

    • 页面回收 (kswapd、直接回收)
    • Swap 管理
    • OOM Killer
  5. 内存保护:

    • 地址空间隔离
    • 权限控制
    • ASLR 随机化
  1. 用户空间和内核空间的区别是什么?它们如何划分?
💡 参考答案

主要区别:

特性
用户空间
内核空间
权限
低特权级 (EL0)
高特权级 (EL1)
访问范围
只能访问自己的地址空间
可访问所有内存
独立性
每进程独立
所有进程共享
硬件访问
不能直接访问
可直接访问
异常处理
被保护,可恢复
可能导致系统崩溃

划分方式(ARM64 为例):

  • 用户空间:0x0000_0000_0000_0000 ~ 0x0000_FFFF_FFFF_FFFF (低 256TB)
  • 内核空间:0xFFFF_0000_0000_0000 ~ 0xFFFF_FFFF_FFFF_FFFF (高 256TB)

实现机制:

  • TTBR0 寄存器指向用户空间页表
  • TTBR1 寄存器指向内核空间页表
  • 地址高位为 0 时使用 TTBR0,高位为全 1 时使用 TTBR1
  1. 什么是页 (Page)?为什么 Linux 选择以页为单位管理内存?
💡 参考答案

**页 (Page)**:内存管理的基本单位,典型大小为 4KB (4096 字节)。

以页为单位管理的原因:

  1. 硬件支持:MMU 以页为单位进行地址转换和权限检查
  2. 减少管理开销:
    • 按字节:需要 16G 个管理单元
    • 按 4KB 页:只需 4M 个管理单元
    • 管理 16GB 内存:
  3. 页表效率:页表项数量可控,多级页表可行
  4. 内存保护:权限以页为粒度,简化硬件设计
  5. 内外碎片平衡:
    • 太小:外部碎片增加,管理复杂
    • 太大:内部碎片增加,浪费内存
    • 4KB 是经过工程实践的合理折中
  6. 磁盘 I/O 对齐:页大小通常是磁盘扇区大小的倍数

struct page 的作用:内核为每个物理页框维护一个 struct page 结构,记录页面状态、引用计数、映射信息等。


🔥 高频面试题

【面试题 1】虚拟内存带来了哪些好处?有什么代价?

💡 参考答案

好处:

优势
说明
进程隔离
每个进程有独立的地址空间,互不干扰
内存保护
页表包含权限位,防止非法访问
虚拟地址连续
即使物理内存不连续,虚拟地址也可连续
按需分配
Demand Paging,只在真正访问时才分配物理内存
共享内存
多进程可映射同一物理页(如共享库)
交换空间
可将不活跃页面换出到磁盘,扩展可用内存
写时复制 (COW)
fork() 时共享页面,写入时才复制

代价:

代价
说明
地址转换开销
每次内存访问都需要页表查询
TLB Miss 惩罚
页表未缓存时需要多次内存访问
页表内存占用
多级页表本身需要内存存储
Page Fault 开销
缺页异常处理涉及磁盘 I/O
内存碎片
内部碎片(页内浪费)和外部碎片

【面试题 2】为什么 Linux 要使用多级页表而不是单级页表?

💡 参考答案

单级页表的问题:

以 32 位系统、4KB 页大小为例:

虚拟地址空间:4GB = 2^32 字节页大小:4KB = 2^12 字节页表项数:2^32 / 2^12 = 2^20 = 1M 个每个 PTE 4 字节 → 页表大小 = 4MB / 进程

问题:

  • 每个进程都需要 4MB 连续的页表空间
  • 即使进程只使用很少内存,也需要完整页表
  • 100 个进程 = 400MB 仅用于页表

多级页表的优势:

                    单级页表                    多级页表              ┌────────────────┐         ┌─────────┐              │ PTE 0          │         │ PGD     │ 4KB              │ PTE 1          │         └────┬────┘              │ PTE 2          │              │              │ ...            │    ┌─────────┼─────────┐              │ (大量未使用)    │    ↓         ↓         ↓              │ ...            │ ┌─────┐  ┌─────┐   NULL              │ PTE 1M         │ │PMD 0│  │PMD 1│  (节省!)              └────────────────┘ └──┬──┘  └─────┘                   4MB/进程          │                                    ↓                                ┌─────┐                                │ PTE │ 只有实际使用的才分配                                └─────┘

优势:

  1. 按需分配:只为实际使用的地址范围分配页表
  2. 内存节省:稀疏地址空间大大减少页表内存
  3. 灵活性:支持更大的地址空间(64位系统)

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 bits

【面试题 3】kmalloc、vmalloc、__get_free_pages 有什么区别?分别在什么场景使用?

💡 参考答案
函数
物理连续
虚拟连续
大小限制
适用场景
kmalloc
✅ 是
✅ 是
≤ 4MB (通常 128KB)
小对象,需要物理连续
vmalloc
❌ 否
✅ 是
很大
大内存块,无需物理连续
__get_free_pages
✅ 是
✅ 是
≤ 4MB
整页分配,物理连续

详细对比:

┌──────────────────────────────────────────────────────────┐│                    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() → 伙伴系统

【面试题 4】GFP_KERNEL 和 GFP_ATOMIC 的区别是什么?什么情况下必须用 GFP_ATOMIC?

💡 参考答案

GFP 标志对比:

标志
可睡眠
可回收
使用场景
GFP_KERNEL
✅ 是
✅ 是
进程上下文,普通分配
GFP_ATOMIC
❌ 否
❌ 否
中断上下文,不能睡眠
GFP_NOIO
✅ 是
不发起 I/O
块设备驱动
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        (中断、自旋锁、禁用抢占)

【面试题 5】什么是内存碎片?Linux 如何解决内存碎片问题?

💡 参考答案

内存碎片类型:

┌─────────────────────────────────────────────────────────┐│                     内部碎片                             │├─────────────────────────────────────────────────────────┤│ 分配的内存块 > 实际需要的内存                             ││                                                         ││ 示例:申请 100 字节,Slab 分配 128 字节                   ││ ┌────────────────────────────────────┐                  ││ │██████████████████████│░░░░░░░░░░░░│                  ││ │     实际使用 100B    │  浪费 28B   │                  ││ └────────────────────────────────────┘                  │└─────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────┐│                     外部碎片                             │├─────────────────────────────────────────────────────────┤│ 空闲内存总量足够,但不连续                               ││                                                         ││ 空闲总量:12 页,但最大连续只有 2 页                      ││ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐        ││ │░░│██│░░│░░│██│██│░░│░░│██│░░│░░│░░│██│░░│░░│██│        ││ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘        ││  ░░ = 空闲   ██ = 已用                                  ││                                                         ││ 无法分配连续 4 页的请求!                                │└─────────────────────────────────────────────────────────┘

Linux 的解决方案:

方案
原理
对应碎片
Slab 分配器
相同大小对象缓存,减少浪费
内部碎片
伙伴系统
2^n 页分配,相邻合并
外部碎片
内存压缩
迁移可移动页面,整理碎片
外部碎片
迁移类型
UNMOVABLE/MOVABLE/RECLAIMABLE 隔离
外部碎片
大页 (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 页面无法移动,所以:

  • 内核会尽量将不同迁移类型的页面分开管理
  • UNMOVABLE 页面过多会限制规整效果
  • 这也是为什么内存热插拔需要 MOVABLE zone

【面试题 6】什么是 Copy-On-Write (COW)?它在 fork() 中是如何工作的?

💡 参考答案

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 的好处:

  • fork() 非常快,只复制页表
  • 节省内存,很多页从不被写入
  • exec() 后子进程会替换地址空间,无需复制

【面试题 7】什么是缺页异常 (Page Fault)?有哪些类型?

💡 参考答案

缺页异常定义:


缺页异常分类

按处理开销分类

类型
原因
处理方式
开销
Minor Fault
页面在内存,但页表未建立
只需更新页表
微秒级
Major Fault
页面不在内存,需要磁盘 I/O
从磁盘读取 + 更新页表
毫秒级
Invalid Fault
非法访问
发送 SIGSEGV 信号
进程终止

按触发场景分类

场景
描述
Minor/Major
Demand Paging
首次访问 mmap 区域
Minor(匿名)/ Major(文件)
Copy-On-Write
fork 后首次写入共享页
Minor
Swap In
访问被换出的页面
Major
Page Cache Miss
文件映射但不在缓存
Major
Stack Growth
栈自动扩展
Minor
Lazy Allocation
malloc 后首次访问
Minor

详细处理流程

                           访问虚拟地址 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)                     │             │                                │             ▼                                │         复制页面,建立可写映射                   │                                              │                                              ▼                                    ┌─────────────────┐                                    │ 更新页表,返回   │                                    │ 用户态重试访问   │                                    └─────────────────┘

常见缺页场景详解

场景 1:首次访问匿名映射 (Demand Paging)

// 用户代码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,微秒级完成


场景 2:Copy-On-Write (fork 后写入)

// 父进程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,父子进程各有独立页面


场景 3:访问被换出的页面 (Swap In)

// 内存紧张时,某页面被换出到 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)


场景 4:文件映射缺页 (mmap 文件)

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(需要读文件)


场景 5:栈自动扩展

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 如果超出限制)


场景 6:空指针访问 (Invalid Fault)

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 迁移

性能影响

场景
典型延迟
原因
Minor Fault (匿名)
1-10 μs
分配页面 + 更新页表
Minor Fault (COW)
2-20 μs
复制页面 + 更新页表
Minor Fault (Page Cache 命中)
1-5 μs
只需建立映射
Major Fault (文件读取)
1-10 ms
磁盘 I/O (HDD)
Major Fault (SSD)
50-200 μs
SSD I/O
Major Fault (Swap)
1-10 ms
交换空间 I/O

调试与监控

# 查看进程的缺页统计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

【面试题 8】请解释 Linux 内存管理中的"按需分配 (Demand Paging)"

💡 参考答案

概念:

  • 虚拟内存分配时不立即分配物理页
  • 只有在首次访问时才分配物理页
  • 通过缺页异常 (Page Fault) 触发分配

优点:

  1. 节省内存:未使用的页面不占用物理内存
  2. 启动快:程序启动时只加载必要页面
  3. 支持超额分配:可以分配超过物理内存的虚拟地址

工作流程:

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)

🧠 深度思考题

  1. 如果一个进程的虚拟地址空间是 128TB(64 位系统),为什么实际使用的物理内存通常很少?这依赖哪些机制?
💡 参考答案

根本原因:虚拟地址空间只是"潜在可用"的范围,实际物理内存是按需分配的。

依赖的关键机制:

  1. 按需分页(Demand Paging):

    • 只有访问到的虚拟页才会分配物理页
    • mmap/malloc 只分配虚拟地址,不分配物理内存
    • 首次访问触发缺页异常才真正分配
  2. 延迟分配(Lazy Allocation):

    // malloc(1GB) 后VmSize: 1GB+   // 虚拟内存增加VmRSS:  ~0// 物理内存几乎不变
  3. **Copy-on-Write (COW)**:

    • fork 后父子共享物理页
    • 只有写入时才复制
  4. 内存回收与交换:

    • 不活跃页面可被换出到 swap
    • 物理内存可被重复利用
  5. 内存映射复用:

    • 共享库只加载一份到物理内存
    • 多个进程共享同一物理页

具体示例:

进程视角:- 代码段: 10MB- 堆: 声明 1GB,实际用 50MB- mmap: 映射 2GB 文件,只访问 100MB- 栈: 预留 8MB,实际用 1MB虚拟内存: ~3GB物理内存: ~160MB (仅访问过的页)
  1. 在中断处理函数中使用 GFP_KERNEL 分配内存会发生什么?内核如何检测这种错误?
💡 参考答案

问题分析:

GFP_KERNEL 包含 __GFP_RECLAIM 和 __GFP_IO,允许:

  • 直接内存回收(可能睡眠)
  • 文件系统操作(可能睡眠)
  • 等待 I/O 完成(会睡眠)

中断上下文的限制:

  • 不能睡眠/调度
  • 不能持有可睡眠的锁
  • 必须快速执行完毕

可能发生的问题:

  1. 内核 BUG/警告:如果启用了调试选项
  2. 死锁:中断中睡眠可能导致系统挂起
  3. 调度器混乱:in_interrupt() 上下文调用 schedule()

内核检测机制:

  1. might_sleep() 检测:

    voidmight_sleep(void){if (in_atomic() || irqs_disabled()) {// 打印警告和调用栈        WARN_ON(1);    }}
  2. __GFP_DIRECT_RECLAIM 检查:

    if (gfp_mask & __GFP_DIRECT_RECLAIM) {    might_sleep_if(true);}
  3. CONFIG_DEBUG_ATOMIC_SLEEP:

    • 检测原子上下文中的睡眠
    • 打印警告信息和调用栈

正确做法:

// 中断上下文必须使用ptr = kmalloc(size, GFP_ATOMIC);// 或ptr = kmalloc(size, GFP_NOWAIT);
  1. vm_area_struct 和 struct page 分别管理什么?它们之间有什么关系?
💡 参考答案

vm_area_struct(VMA):

  • 管理对象:进程的虚拟地址空间区域
  • 粒度:任意大小的连续虚拟地址范围
  • 归属:每个进程独有(mm_struct)
  • 信息:起止地址、权限、映射文件等

struct page:

  • 管理对象:物理页框
  • 粒度:固定 4KB(或大页)
  • 归属:全局唯一,描述所有物理内存
  • 信息:引用计数、标志、LRU 链表等

两者的关系:

用户视角 (VMA):                    内核视角 (page):┌─────────────────┐               ┌─────────────────┐│ VMA: 0x1000-0x3000 │             │ page[pfn=0x100]││ 虚拟地址范围     │               │ 物理页框       ││ 权限、映射类型   │               │ 状态、引用计数 │└────────┬────────┘               └────────┬────────┘         │                                  │         │     通过页表 (PTE) 建立关联      │         └──────────────────────────────────┘VMA (多个) ──页表映射──> page (一个)          一对多关系(共享页面)一个 page 可被多个 VMA 映射:- 共享库被多个进程映射- fork 后父子进程共享页面- 共享内存

关键操作中的协作:

  • 缺页处理:VMA 提供策略,page 提供物理存储
  • 页面回收:通过反向映射从 page 找到所有 VMA
  • mmap:创建 VMA,延迟分配 page
  1. 为什么 kmalloc 分配的最大内存通常限制在 4MB 左右?如何分配更大的连续物理内存?
💡 参考答案

4MB 限制的原因:

  1. 伙伴系统限制:

    #define MAX_ORDER 11  // 最大阶数// 最大分配 = 2^10 * 4KB = 4MB
  2. 碎片化问题:

    • 系统运行后,高阶连续物理内存稀缺
    • 请求 4MB 连续内存经常失败
  3. 设计权衡:

    • 大多数内核分配需求 < 1 页
    • 连续大内存需求应使用其他方案

分配更大连续物理内存的方法:

  1. vmalloc(虚拟连续,物理不连续):

    void *p = vmalloc(16 * 1024 * 1024);  // 16MB// 适用于不需要物理连续的场景
  2. CMA(Contiguous Memory Allocator):

    // 设备树或 bootargs 预留cma=256M// 分配page = cma_alloc(cma_dev, count, align, gfp);// 适用于 DMA 设备
  3. 大页内存(HugeTLB):

    // 预留大页echo 100 > /proc/sys/vm/nr_hugepages// 分配mmap(NULL, 2*1024*1024, ..., MAP_HUGETLB, ...);
  4. 启动时预留(memblock):

    // 在内核启动早期预留memblock_reserve(base, size);// 内存碎片化之前预留
  5. DMA API:

    dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);// 自动选择合适的分配方式
  1. Linux 内核是如何实现进程间内存隔离的?一个进程能否访问另一个进程的内存?
💡 参考答案

内存隔离实现机制:

  1. 独立的页表:

    进程 A:  mm_struct_A -> pgd_A -> 私有页表进程 B:  mm_struct_B -> pgd_B -> 私有页表相同虚拟地址 -> 不同物理页
  2. 地址空间切换:

    switch_mm(old_mm, new_mm, next) {// 更新 TTBR0(ARM64)或 CR3(x86)// 切换到新进程的页表    load_new_mm_cr3(next->mm->pgd);}
  3. MMU 硬件保护:

    • 进程只能访问自己页表中映射的地址
    • 访问未映射地址触发缺页异常
  4. 用户/内核空间隔离:

    • 用户态无法访问内核地址
    • 页表项权限位控制

能否访问另一个进程的内存?

正常情况下不能,但有合法途径:

  1. 共享内存:

    // 两个进程都 mmap 同一 shmmmap(NULL, size, ..., MAP_SHARED, shm_fd, 0);
  2. ptrace 系统调用:

    // 调试器可以读写目标进程内存ptrace(PTRACE_PEEKDATA, pid, addr, NULL);ptrace(PTRACE_POKEDATA, pid, addr, data);
  3. /proc/[pid]/mem:

    # root 可以读取其他进程内存cat /proc/1234/mem
  4. process_vm_readv/writev:

    // 跨进程内存读写(需要权限)process_vm_readv(pid, local_iov, ..., remote_iov, ...);
  5. 内核模块:

    • 内核代码可以访问任何物理内存
    • 可以临时映射其他进程的页

安全机制:

  • 上述方式都需要特权或特定权限
  • SELinux/AppArmor 可进一步限制
  • 地址空间随机化 (ASLR) 增加攻击难度

💻 实践题

  1. 编写一个简单程序,验证 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;}
  2. 使用 /proc/[pid]/maps 分析进程的内存布局:

    # 查看某进程的内存区域cat /proc/$(pidof bash)/maps# 分析各区域的用途:# - [heap]:堆# - [stack]:栈# - libc.so:共享库# - [vdso]:虚拟动态共享对象
  3. 观察 Page Fault 统计:

    # 方法 1:使用 time 命令/usr/bin/time -v ls# 查看 "Minor page faults" 和 "Major page faults"# 方法 2:使用 perfperf stat -e page-faults ./your_program

📊 综合分析题

  1. 场景分析:一个应用程序调用 malloc(1GB) 后立即 memset() 填充数据,系统变得非常卡顿。请分析可能的原因和优化方案。

    💡 分析思路

    原因分析:

    优化方案:

    方案
    说明
    MAP_POPULATE
    mmap 时预先分配所有页面
    mlock()
    锁定页面,防止换出
    大页 (Huge Page)
    减少 Page Fault 次数(2MB 页)
    madvise(MADV_WILLNEED)
    提示内核预读
    分块处理
    不要一次性处理 1GB

    代码示例:

    // 使用 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);
    1. malloc(1GB) 只分配虚拟地址,不分配物理页
    2. memset() 顺序访问每个页面
    3. 每 4KB 触发一次 Page Fault(约 262,144 次)
    4. 大量 Page Fault 导致 CPU 频繁陷入内核
    5. 如果物理内存不足,还会触发内存回收/交换

最新文章

随机文章