第一章: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 内核内存管理的整体架构:
✅ 核心概念回顾
- 虚拟内存:为每个进程提供独立、连续的地址空间
- 物理内存组织:Node → Zone → Page 的层次结构
- 页表:实现虚拟地址到物理地址的转换
- 核心数据结构 :
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 思考题
📝 基础概念题
-
什么是虚拟内存?Linux 为什么要使用虚拟内存?
-
Linux 内核内存管理的主要职责有哪些?
-
用户空间和内核空间的区别是什么?它们如何划分?
-
什么是页 (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 │ 只有实际使用的才分配
└─────┘
优势:
- 按需分配:只为实际使用的地址范围分配页表
- 内存节省:稀疏地址空间大大减少页表内存
- 灵活性:支持更大的地址空间(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. 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)
🧠 深度思考题
-
如果一个进程的虚拟地址空间是 128TB(64 位系统),为什么实际使用的物理内存通常很少?这依赖哪些机制?
-
在中断处理函数中使用
GFP_KERNEL分配内存会发生什么?内核如何检测这种错误? -
vm_area_struct和struct page分别管理什么?它们之间有什么关系? -
为什么
kmalloc分配的最大内存通常限制在 4MB 左右?如何分配更大的连续物理内存? -
Linux 内核是如何实现进程间内存隔离的?一个进程能否访问另一个进程的内存?
💻 实践题
-
编写一个简单程序,验证 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; } -
使用
/proc/[pid]/maps分析进程的内存布局:bash# 查看某进程的内存区域 cat /proc/$(pidof bash)/maps # 分析各区域的用途: # - [heap]:堆 # - [stack]:栈 # - libc.so:共享库 # - [vdso]:虚拟动态共享对象 -
观察 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
📊 综合分析题
-
场景分析 :一个应用程序调用
malloc(1GB)后立即memset()填充数据,系统变得非常卡顿。请分析可能的原因和优化方案。💡 分析思路
原因分析:
malloc(1GB)只分配虚拟地址,不分配物理页memset()顺序访问每个页面- 每 4KB 触发一次 Page Fault(约 262,144 次)
- 大量 Page Fault 导致 CPU 频繁陷入内核
- 如果物理内存不足,还会触发内存回收/交换
优化方案:
方案 说明 MAP_POPULATEmmap 时预先分配所有页面 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);