内存管理-第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 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 为什么要使用虚拟内存?

  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】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 ✅ 是 ✅ 是 为用户空间分配

详细解释:

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_struct 和 struct 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);
相关推荐
广州浮点FLOATLIC16 分钟前
许可证服务器迁移后软件打不开:研发 IT 怎样定位连接问题
linux·服务器·数据库
IT研究所35 分钟前
AI-ITR平台如何减少客户问题反复升级?
大数据·运维·人工智能·低代码·自然语言处理·安全架构·企微
BLUcoding38 分钟前
接口服务公网超时排查记录:SecureRandom 阻塞问题分析与解决
java·linux·springboot·aes·securerandom
AIgorithmGEEK41 分钟前
[Linux]从手写报头到内核套路:序列化、反序列化与自定义协议全链路
linux·运维·服务器·网络·序列化·反序列化
Nil2081 小时前
leetcode 139单词拆分
linux·运维·服务器
Starry-sky(jing)1 小时前
BUG: unable to handle kernel paging request 完整排查:dmesg 四要素与三路定罪
linux·运维·服务器·内核·排障
mounter6251 小时前
从硬件互连到操作系统变革:CXL 技术演进与 Linux 内核工程挑战
linux·运维·服务器
zhengqweasd1 小时前
机房托管服务器怎么选硬盘,机械盘和 SSD 托管场景差异
运维·服务器·github
xh didida2 小时前
Linux -- 基础IO
linux·服务器·开发语言·c++