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

第一章: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) 组成。

c 复制代码
// 源码位置: 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 结构来描述。

c 复制代码
// 源码位置: include/linux/mm_types.h

struct page {
    unsigned long flags;        /* 原子标志位,如 PG_locked, PG_dirty 等 */
    
    union {
        struct {    /* Page cache 和匿名页 */
            struct list_head lru;           /* LRU 链表 */
            struct address_space *mapping;  /* 映射的地址空间 */
            pgoff_t index;                  /* 在映射中的偏移 */
            unsigned long private;          /* 私有数据 */
        };
        struct {    /* Slab 分配器使用 */
            struct list_head slab_list;
            struct kmem_cache *slab_cache;
            void *freelist;
            /* ... */
        };
        struct {    /* 复合页 (Compound Page) */
            unsigned long compound_head;
            unsigned char compound_dtor;
            unsigned char compound_order;
            /* ... */
        };
        /* ... 其他用途 ... */
    };
    
    atomic_t _refcount;         /* 引用计数 */
    atomic_t _mapcount;         /* 映射计数 */
    /* ... */
};

🔷 struct zone - 内存区域

物理内存被划分为不同的区域(Zone),以满足不同类型的内存分配需求。

c 复制代码
// 源码位置: include/linux/mmzone.h

enum zone_type {
    ZONE_DMA,       /* DMA 内存区域,用于 ISA 设备 */
    ZONE_DMA32,     /* 32位 DMA 设备可访问 */
    ZONE_NORMAL,    /* 常规内存,内核可直接映射 */
    ZONE_HIGHMEM,   /* 高端内存 (仅32位系统) */
    ZONE_MOVABLE,   /* 可移动内存,用于内存热插拔 */
    ZONE_DEVICE,    /* 设备内存 */
    __MAX_NR_ZONES
};

struct zone {
    unsigned long watermark[NR_WMARK];  /* 水位线: min, low, high */
    unsigned long nr_reserved_highatomic;
    
    struct pglist_data *zone_pgdat;     /* 所属节点 */
    struct per_cpu_pageset __percpu *pageset;  /* Per-CPU 页面缓存 */
    
    /* 空闲区域,伙伴系统的核心 */
    struct free_area free_area[MAX_ORDER];
    
    unsigned long zone_start_pfn;       /* 起始页框号 */
    unsigned long spanned_pages;        /* 跨越的页数 */
    unsigned long present_pages;        /* 实际存在的页数 */
    
    const char *name;                   /* 区域名称 */
    /* ... */
};

🔷 struct pglist_data (pg_data_t) - NUMA 节点

在 NUMA 系统中,每个节点有一个 pg_data_t 结构。

c 复制代码
// 源码位置: include/linux/mmzone.h

typedef struct pglist_data {
    struct zone node_zones[MAX_NR_ZONES];   /* 该节点的所有 zone */
    struct zonelist node_zonelists[MAX_ZONELISTS];  /* 分配回退列表 */
    
    int nr_zones;                           /* zone 数量 */
    unsigned long node_start_pfn;           /* 节点起始页框号 */
    unsigned long node_present_pages;       /* 节点中的页数 */
    unsigned long node_spanned_pages;       /* 跨越的页数 */
    
    int node_id;                            /* 节点 ID */
    
    /* 页面回收相关 */
    wait_queue_head_t kswapd_wait;
    struct task_struct *kswapd;             /* kswapd 守护进程 */
    /* ... */
} pg_data_t;

🔷 struct mm_struct - 进程内存描述符

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

c 复制代码
// 源码位置: include/linux/mm_types.h

struct mm_struct {
    struct vm_area_struct *mmap;        /* VMA 链表 */
    struct rb_root mm_rb;               /* VMA 红黑树 */
    
    pgd_t *pgd;                         /* 页全局目录 */
    
    atomic_t mm_users;                  /* 用户计数 */
    atomic_t mm_count;                  /* 引用计数 */
    
    unsigned long start_code, end_code; /* 代码段 */
    unsigned long start_data, end_data; /* 数据段 */
    unsigned long start_brk, brk;       /* 堆 */
    unsigned long start_stack;          /* 栈起始 */
    
    unsigned long total_vm;             /* 总页数 */
    unsigned long locked_vm;            /* 锁定页数 */
    
    /* ... */
};

🔷 struct vm_area_struct - 虚拟内存区域

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

c 复制代码
// 源码位置: include/linux/mm_types.h

struct vm_area_struct {
    unsigned long vm_start;             /* 起始地址 */
    unsigned long vm_end;               /* 结束地址 */
    
    struct vm_area_struct *vm_next, *vm_prev;  /* VMA 链表 */
    struct rb_node vm_rb;               /* 红黑树节点 */
    
    struct mm_struct *vm_mm;            /* 所属的 mm_struct */
    pgprot_t vm_page_prot;              /* 访问权限 */
    unsigned long vm_flags;             /* 标志位 */
    
    struct file *vm_file;               /* 映射的文件 (如果是文件映射) */
    void *vm_private_data;              /* 私有数据 */
    
    const struct vm_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) 标志控制内存分配的行为:

c 复制代码
// 源码位置: 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 pagestruct zonepg_data_tmm_structvm_area_struct

✅ 关键子系统

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

📚 下一章预告

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

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

1.11 思考题

📝 基础概念题

  1. 什么是虚拟内存?Linux 为什么要使用虚拟内存?

  2. Linux 内核内存管理的主要职责有哪些?

  3. 用户空间和内核空间的区别是什么?它们如何划分?

  4. 什么是页 (Page)?为什么 Linux 选择以页为单位管理内存?


🔥 高频面试题

【面试题 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 bits
L1       PUD        9 bits    9 bits
L2       PMD        9 bits    9 bits
L3       PTE        9 bits    9 bits
───────────────────────────────────
         Page Offset 12 bits   12 bits

【面试题 3】kmallocvmalloc__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_KERNELGFP_ATOMIC 的区别是什么?什么情况下必须用 GFP_ATOMIC

💡 参考答案

GFP 标志对比

标志 可睡眠 可回收 使用场景
GFP_KERNEL ✅ 是 ✅ 是 进程上下文,普通分配
GFP_ATOMIC ❌ 否 ❌ 否 中断上下文,不能睡眠
GFP_NOIO ✅ 是 不发起 I/O 块设备驱动
GFP_NOFS ✅ 是 不进入文件系统 文件系统代码
GFP_USER ✅ 是 ✅ 是 为用户空间分配

详细解释

c 复制代码
// GFP_KERNEL:最常用的标志
// - 可以睡眠等待内存
// - 可以触发内存回收(kswapd、直接回收)
// - 可以触发交换
void *ptr = kmalloc(size, GFP_KERNEL);

// GFP_ATOMIC:紧急分配
// - 绝对不能睡眠
// - 使用紧急预留内存池
// - 分配失败概率较高
void *ptr = kmalloc(size, GFP_ATOMIC);

必须使用 GFP_ATOMIC 的场景

c 复制代码
// 1. 中断处理函数
irqreturn_t my_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 / tasklet
void my_tasklet_func(unsigned long 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 15
M = 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)?有哪些类型?

💡 参考答案

缺页异常定义

  • 访问的虚拟地址对应的物理页不在内存中,或访问权限不匹配
  • CPU 无法完成地址转换,产生异常(x86: #PF,ARM: Data Abort)
  • 内核介入处理,根据情况分配页面、建立映射或终止进程

缺页异常分类

按处理开销分类
类型 原因 处理方式 开销
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)
c 复制代码
// 用户代码
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 后写入)
c 复制代码
// 父进程
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)
c 复制代码
// 内存紧张时,某页面被换出到 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 文件)
c 复制代码
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:栈自动扩展
c 复制代码
void deep_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)
c 复制代码
int *ptr = NULL;
*ptr = 42;  // 访问地址 0,触发 Page Fault

内核处理

复制代码
地址 0 (或很低的地址)
    │
    ├── find_vma() 找不到覆盖此地址的 VMA
    │
    ├── 检查是否可以栈扩展:不可以(地址太低)
    │
    └── bad_area() → 发送 SIGSEGV

结果:进程收到 SIGSEGV 信号,默认终止并产生 core dump


代码路径总结

c 复制代码
// arch/x86/mm/fault.c 或 arch/arm64/mm/fault.c
do_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

调试与监控

bash 复制代码
# 查看进程的缺页统计
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 ./program
perf 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

示例

c 复制代码
// 分配 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 位系统),为什么实际使用的物理内存通常很少?这依赖哪些机制?

  2. 在中断处理函数中使用 GFP_KERNEL 分配内存会发生什么?内核如何检测这种错误?

  3. vm_area_structstruct page 分别管理什么?它们之间有什么关系?

  4. 为什么 kmalloc 分配的最大内存通常限制在 4MB 左右?如何分配更大的连续物理内存?

  5. Linux 内核是如何实现进程间内存隔离的?一个进程能否访问另一个进程的内存?


💻 实践题

  1. 编写一个简单程序,验证 Demand Paging

    c 复制代码
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/mman.h>
    #include <unistd.h>
    
    int main() {
        size_t size = 100 * 1024 * 1024;  // 100MB
        
        printf("Before mmap, press Enter...\n");
        getchar();  // 查看 /proc/[pid]/status 的 VmRSS
        
        void *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);
        return 0;
    }
  2. 使用 /proc/[pid]/maps 分析进程的内存布局

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

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

📊 综合分析题

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

    💡 分析思路

    原因分析

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

    优化方案

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

    代码示例

    c 复制代码
    // 使用 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);
相关推荐
lzhailb1 小时前
LVS(Linux virual server)
运维·服务器·网络
cws2004011 小时前
智能化弱电工程桥架、支吊架、线管、线盒安装要求-2
运维·网络·桥架
nxb5562 小时前
云原生keepalived实验设定
linux·运维·云原生
luoshanxuli20102 小时前
Linux UVC Camera的介绍与实践应用(二)
linux
xianyudx2 小时前
Linux 服务器 DNS 配置指南 (CentOS 7 / 麒麟 V10)
linux·服务器·centos
grrrr_12 小时前
【Linux】内网穿透 FTP 终极复现手册 (2026 版)--cpolar
linux·网络·内网穿透·ftp·cpolar
Mikowoo0072 小时前
VMware Tools 与 共享主机文件夹
运维·服务器
文静小土豆2 小时前
CentOS 7 升级 OpenSSL 3.5.4 详细指南
linux·运维·centos·ssl
weixin_444579302 小时前
Ubuntu 22.04 服务器安装教程(二)——桌面版系统
linux·服务器·ubuntu