HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 手撸容器系统

    • 完整手撸容器技术文档系列
    • 01-容器本质与基础概念
    • 02-Namespace隔离机制
    • 03-CGroup资源控制
    • 04-Capabilities与安全机制
    • 05-容器网络原理
    • 06-网络模式与实现
    • 07-CNI插件开发
    • 08-RootFS与文件系统隔离
    • 09-OverlayFS镜像分层
    • 10-命令行手撸容器
    • 11-Go实现最小容器
    • 12-Go实现完整容器
    • 13-容器生命周期管理
    • 14-调试技术与工具
    • 15-OCI规范与标准化
    • 16-进阶场景与优化
    • 常见问题与故障排查
    • 参考资料与延伸阅读

19-Linux 内存管理深度剖析

学习目标

  • 深入理解 Linux 虚拟内存与物理内存的映射机制
  • 掌握 RSS/PSS/USS 等内存指标的含义与计算方式
  • 理解 Page Cache 的工作原理与回收机制
  • 能够分析内核源码中的关键数据结构
  • 掌握内存问题的排查方法

前置知识

  • Linux 进程基础
  • 基本的 C 语言阅读能力
  • 了解计算机组成原理

一、内存管理概述

1.1 为什么需要虚拟内存?

在早期计算机中,程序直接访问物理内存,这带来了几个严重问题:

问题 1: 地址冲突
┌─────────────────────────────────────┐
│           物理内存                   │
├─────────────────────────────────────┤
│  程序 A: 0x1000 - 0x2000            │
│  程序 B: 0x1500 - 0x2500  ← 冲突!   │
└─────────────────────────────────────┘

问题 2: 内存不足
┌─────────────────────────────────────┐
│  物理内存: 512 MB                   │
│  程序需要: 1 GB  ← 无法运行!        │
└─────────────────────────────────────┘

问题 3: 安全隔离
┌─────────────────────────────────────┐
│  程序 A 可以直接读写程序 B 的内存    │
│  ← 无法保证安全!                    │
└─────────────────────────────────────┘

虚拟内存通过地址映射解决了这些问题。

1.2 虚拟内存架构

┌─────────────────────────────────────────────────────────────────────┐
│                        进程视角                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │              Virtual Address Space (虚拟地址空间)             │   │
│  │                                                             │   │
│  │  0x0000000000000000  ┌────────────────────┐                 │   │
│  │                      │      NULL 区域      │ (不可访问)      │   │
│  │  0x0000000000001000  ├────────────────────┤                 │   │
│  │                      │       代码段        │ (.text)        │   │
│  │                      ├────────────────────┤                 │   │
│  │                      │       数据段        │ (.data/.bss)   │   │
│  │                      ├────────────────────┤                 │   │
│  │                      │         堆         │ (向上增长 ↑)    │   │
│  │                      │         ...        │                 │   │
│  │                      ├────────────────────┤                 │   │
│  │                      │      内存映射       │ (mmap 区域)     │   │
│  │                      ├────────────────────┤                 │   │
│  │                      │         ...        │                 │   │
│  │                      │         栈         │ (向下增长 ↓)    │   │
│  │  0x00007FFFFFFFFFFF  ├────────────────────┤                 │   │
│  │                      │      内核空间       │ (用户不可访问)  │   │
│  │  0xFFFFFFFFFFFFFFFF  └────────────────────┘                 │   │
│  │                                                             │   │
│  │  64位 Linux 用户空间: 0 ~ 0x00007FFFFFFFFFFF (128 TB)       │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

1.3 地址转换流程

┌────────────────────────────────────────────────────────────────────────┐
│                         地址转换流程                                    │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│    虚拟地址 (VA)                                                        │
│        │                                                               │
│        ▼                                                               │
│   ┌─────────┐     ┌─────────┐                                         │
│   │   TLB   │────▶│  命中?  │                                         │
│   │ (快表)  │     └────┬────┘                                         │
│   └─────────┘          │                                              │
│                   Yes  │  No                                          │
│                   ┌────┴────┐                                         │
│                   ▼         ▼                                         │
│            直接获取PA   ┌─────────┐                                    │
│                        │  页表   │                                    │
│                        │ (MMU)  │                                     │
│                        └────┬────┘                                    │
│                             │                                         │
│                             ▼                                         │
│                      ┌──────────────┐                                 │
│                      │  页表项存在?  │                                │
│                      └──────┬───────┘                                 │
│                        Yes  │  No                                     │
│                        ┌────┴────┐                                    │
│                        ▼         ▼                                    │
│                   物理地址    Page Fault                              │
│                    (PA)      (缺页异常)                               │
│                        │         │                                    │
│                        ▼         ▼                                    │
│                   访问物理    分配物理页                               │
│                     内存     或从磁盘换入                              │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

二、页表与地址映射

2.1 四级页表结构 (x86-64)

┌────────────────────────────────────────────────────────────────────────┐
│                    64位虚拟地址结构                                     │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│  63    48 47    39 38    30 29    21 20    12 11         0            │
│  ├──────┼────────┼────────┼────────┼────────┼────────────┤            │
│  │ 保留  │  PML4  │  PDPT  │   PD   │   PT   │  Offset    │            │
│  │ 16位  │  9位   │  9位   │  9位   │  9位   │   12位     │            │
│  └──────┴────────┴────────┴────────┴────────┴────────────┘            │
│                                                                        │
│  PML4: Page Map Level 4 (页映射四级表)                                 │
│  PDPT: Page Directory Pointer Table (页目录指针表)                     │
│  PD:   Page Directory (页目录)                                         │
│  PT:   Page Table (页表)                                               │
│  Offset: 页内偏移 (4KB = 2^12)                                         │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────┐
│                    四级页表查找过程                                     │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│   CR3 寄存器                                                           │
│       │                                                                │
│       ▼                                                                │
│   ┌─────────┐                                                         │
│   │  PML4   │ ──[PML4 索引]──▶ ┌─────────┐                            │
│   │  Table  │                  │  PDPT   │                            │
│   └─────────┘                  │  Entry  │                            │
│                                └────┬────┘                            │
│                                     │                                  │
│                          [PDPT 索引]│                                  │
│                                     ▼                                  │
│                                ┌─────────┐                            │
│                                │   PD    │                            │
│                                │  Entry  │                            │
│                                └────┬────┘                            │
│                                     │                                  │
│                           [PD 索引] │                                  │
│                                     ▼                                  │
│                                ┌─────────┐                            │
│                                │   PT    │                            │
│                                │  Entry  │                            │
│                                └────┬────┘                            │
│                                     │                                  │
│                           [PT 索引] │                                  │
│                                     ▼                                  │
│                                ┌─────────┐                            │
│                                │ 物理页框 │ + Offset = 物理地址        │
│                                └─────────┘                            │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

2.2 页表项 (PTE) 结构

// Linux 内核中的页表项定义 (arch/x86/include/asm/pgtable_types.h)
typedef struct { pteval_t pte; } pte_t;

/*
 * x86-64 PTE 格式 (64位):
 *
 * 63    62  52 51        12 11   9  8   7   6   5   4   3   2   1   0
 * ├─────┼─────┼────────────┼──────┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
 * │ NX  │ AVL │ 物理页框号  │ AVL  │ G │PAT│ D │ A │PCD│PWT│U/S│R/W│ P │
 * └─────┴─────┴────────────┴──────┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
 *
 * P   (bit 0):  Present - 页面是否在内存中
 * R/W (bit 1):  Read/Write - 0=只读, 1=可读写
 * U/S (bit 2):  User/Supervisor - 0=仅内核, 1=用户可访问
 * PWT (bit 3):  Page-level Write-Through
 * PCD (bit 4):  Page-level Cache Disable
 * A   (bit 5):  Accessed - 页面是否被访问过
 * D   (bit 6):  Dirty - 页面是否被修改过
 * PAT (bit 7):  Page Attribute Table
 * G   (bit 8):  Global - 全局页面 (不刷新 TLB)
 * NX  (bit 63): No Execute - 不可执行
 */

2.3 内核源码:页表操作

// mm/memory.c - 页表查找
pte_t *__follow_pte(struct mm_struct *mm, unsigned long address)
{
    pgd_t *pgd;
    p4d_t *p4d;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *pte;

    // 从 mm_struct 获取 PGD (PML4)
    pgd = pgd_offset(mm, address);
    if (pgd_none(*pgd) || pgd_bad(*pgd))
        return NULL;

    // 获取 P4D (在 4 级页表中等同于 PGD)
    p4d = p4d_offset(pgd, address);
    if (p4d_none(*p4d) || p4d_bad(*p4d))
        return NULL;

    // 获取 PUD
    pud = pud_offset(p4d, address);
    if (pud_none(*pud) || pud_bad(*pud))
        return NULL;

    // 获取 PMD
    pmd = pmd_offset(pud, address);
    if (pmd_none(*pmd) || pmd_bad(*pmd))
        return NULL;

    // 获取 PTE
    pte = pte_offset_kernel(pmd, address);
    if (pte_none(*pte))
        return NULL;

    return pte;
}

三、内存指标详解:RSS/PSS/USS

3.1 指标概述

┌────────────────────────────────────────────────────────────────────────┐
│                    进程内存指标                                         │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│  ┌─────────────────────────────────────────────────────────────────┐  │
│  │                    Virtual Size (VSZ/VIRT)                       │  │
│  │  所有映射的虚拟内存总和,包括未分配物理内存的部分                    │  │
│  │  ┌─────────────────────────────────────────────────────────┐    │  │
│  │  │               Resident Set Size (RSS)                    │    │  │
│  │  │  实际驻留在物理内存中的页面 (包括共享)                     │    │  │
│  │  │  ┌─────────────────────────────────────────────────┐    │    │  │
│  │  │  │     Proportional Set Size (PSS)                  │    │    │  │
│  │  │  │  RSS 按共享比例分摊后的大小                        │    │    │  │
│  │  │  │  ┌───────────────────────────────────────┐      │    │    │  │
│  │  │  │  │    Unique Set Size (USS)               │      │    │    │  │
│  │  │  │  │  进程独占的物理内存                      │      │    │    │  │
│  │  │  │  └───────────────────────────────────────┘      │    │    │  │
│  │  │  └─────────────────────────────────────────────────┘    │    │  │
│  │  └─────────────────────────────────────────────────────────┘    │  │
│  └─────────────────────────────────────────────────────────────────┘  │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

3.2 共享内存计算示例

场景:libc.so (10 MB) 被 3 个进程共享

┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│  Process A          Process B          Process C                   │
│  ┌─────────┐       ┌─────────┐        ┌─────────┐                  │
│  │ Private │       │ Private │        │ Private │                  │
│  │  5 MB   │       │  8 MB   │        │  3 MB   │                  │
│  └────┬────┘       └────┬────┘        └────┬────┘                  │
│       │                 │                  │                       │
│       └────────────────┬┴──────────────────┘                       │
│                        │                                           │
│                        ▼                                           │
│                   ┌─────────┐                                      │
│                   │ libc.so │                                      │
│                   │  10 MB  │  (共享)                               │
│                   └─────────┘                                      │
│                                                                     │
│  各进程的内存指标:                                                   │
│  ┌───────────┬─────────┬─────────┬─────────┐                       │
│  │  指标     │ Process A│ Process B│ Process C│                       │
│  ├───────────┼─────────┼─────────┼─────────┤                       │
│  │ USS       │   5 MB  │   8 MB  │   3 MB  │                       │
│  │ RSS       │  15 MB  │  18 MB  │  13 MB  │  (包含整个 libc)      │
│  │ PSS       │ 8.33 MB │ 11.33 MB│ 6.33 MB │  (libc/3 = 3.33MB)    │
│  └───────────┴─────────┴─────────┴─────────┘                       │
│                                                                     │
│  PSS 计算:                                                         │
│  Process A PSS = 5 MB + (10 MB / 3) = 5 + 3.33 = 8.33 MB           │
│  Process B PSS = 8 MB + (10 MB / 3) = 8 + 3.33 = 11.33 MB          │
│  Process C PSS = 3 MB + (10 MB / 3) = 3 + 3.33 = 6.33 MB           │
│                                                                     │
│  所有进程 PSS 之和 = 8.33 + 11.33 + 6.33 = 26 MB                   │
│  = 实际物理内存使用 = 5 + 8 + 3 + 10 = 26 MB ✓                     │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

3.3 查看进程内存指标

# 方法 1: /proc/[pid]/status
$ cat /proc/$$/status | grep -E "^(Vm|Rss|Pss)"
VmPeak:    234560 kB    # 虚拟内存峰值
VmSize:    234560 kB    # 当前虚拟内存大小
VmRSS:      12340 kB    # RSS
RssAnon:     8000 kB    # 匿名 RSS
RssFile:     4340 kB    # 文件映射 RSS
RssShmem:       0 kB    # 共享内存 RSS

# 方法 2: /proc/[pid]/smaps (更详细)
$ cat /proc/$$/smaps | grep -E "^(Size|Rss|Pss|Private|Shared)"

# 方法 3: /proc/[pid]/smaps_rollup (汇总)
$ cat /proc/$$/smaps_rollup
Rss:               12340 kB
Pss:                9876 kB
Pss_Anon:           8000 kB
Pss_File:           1876 kB
Pss_Shmem:             0 kB
Private_Clean:      1000 kB
Private_Dirty:      7000 kB
Shared_Clean:       4000 kB
Shared_Dirty:        340 kB

# 方法 4: 使用 smem 工具
$ smem -P nginx
  PID User     Command                         Swap      USS      PSS      RSS
 1234 nginx    nginx: worker process              0    15000    18000    25000
 1235 nginx    nginx: worker process              0    15000    18000    25000

3.4 内核源码:RSS 计算

// mm/memory.c
// 遍历进程页表,统计 RSS
unsigned long get_mm_rss(struct mm_struct *mm)
{
    return get_mm_counter(mm, MM_FILEPAGES) +    // 文件映射页
           get_mm_counter(mm, MM_ANONPAGES) +    // 匿名页
           get_mm_counter(mm, MM_SHMEMPAGES);    // 共享内存页
}

// include/linux/mm_types.h
struct mm_struct {
    // ...
    atomic_long_t rss_stat[NR_MM_COUNTERS];
    // ...
};

enum {
    MM_FILEPAGES,    // 文件映射页数
    MM_ANONPAGES,    // 匿名页数
    MM_SWAPENTS,     // swap 条目数
    MM_SHMEMPAGES,   // 共享内存页数
    NR_MM_COUNTERS
};

四、Page Cache 深度剖析

4.1 Page Cache 概述

┌────────────────────────────────────────────────────────────────────────┐
│                         Page Cache 架构                                │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│   用户空间                                                              │
│   ┌─────────────────────────────────────────────────────────────┐     │
│   │  read() / write() / mmap()                                   │     │
│   └────────────────────────────┬────────────────────────────────┘     │
│                                │                                       │
│   ──────────────────────────── │ ────────────────────────────────      │
│                                ▼                                       │
│   内核空间                      │                                       │
│   ┌─────────────────────────────────────────────────────────────┐     │
│   │                    VFS (虚拟文件系统)                         │     │
│   └────────────────────────────┬────────────────────────────────┘     │
│                                │                                       │
│                                ▼                                       │
│   ┌─────────────────────────────────────────────────────────────┐     │
│   │                     Page Cache                               │     │
│   │  ┌─────────────────────────────────────────────────────┐    │     │
│   │  │   address_space (每个文件一个)                       │    │     │
│   │  │   ┌─────────┐ ┌─────────┐ ┌─────────┐              │    │     │
│   │  │   │ Page 0  │ │ Page 1  │ │ Page 2  │ ...          │    │     │
│   │  │   │ 4 KB    │ │ 4 KB    │ │ 4 KB    │              │    │     │
│   │  │   └─────────┘ └─────────┘ └─────────┘              │    │     │
│   │  └─────────────────────────────────────────────────────┘    │     │
│   └────────────────────────────┬────────────────────────────────┘     │
│                                │                                       │
│                                ▼                                       │
│   ┌─────────────────────────────────────────────────────────────┐     │
│   │                    块设备层 / 文件系统                        │     │
│   └────────────────────────────┬────────────────────────────────┘     │
│                                │                                       │
│   ──────────────────────────── │ ────────────────────────────────      │
│                                ▼                                       │
│   硬件                                                                 │
│   ┌─────────────────────────────────────────────────────────────┐     │
│   │                    磁盘 / SSD / NVMe                         │     │
│   └─────────────────────────────────────────────────────────────┘     │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

4.2 读写流程

┌────────────────────────────────────────────────────────────────────────┐
│                          读取流程                                       │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│  read(fd, buf, 4096)                                                   │
│       │                                                                │
│       ▼                                                                │
│  ┌─────────────────────────────┐                                      │
│  │ 1. 查找 Page Cache          │                                      │
│  │    find_get_page()          │                                      │
│  └────────────┬────────────────┘                                      │
│               │                                                        │
│       ┌───────┴───────┐                                               │
│       │               │                                                │
│       ▼               ▼                                                │
│   Cache Hit       Cache Miss                                          │
│       │               │                                                │
│       │               ▼                                                │
│       │      ┌────────────────────────┐                               │
│       │      │ 2. 分配新页面           │                               │
│       │      │    __page_cache_alloc() │                               │
│       │      └───────────┬────────────┘                               │
│       │                  │                                             │
│       │                  ▼                                             │
│       │      ┌────────────────────────┐                               │
│       │      │ 3. 从磁盘读取数据       │                               │
│       │      │    a]_readpage()        │                               │
│       │      └───────────┬────────────┘                               │
│       │                  │                                             │
│       │                  ▼                                             │
│       │      ┌────────────────────────┐                               │
│       │      │ 4. 加入 Page Cache     │                               │
│       │      │    add_to_page_cache() │                               │
│       │      └───────────┬────────────┘                               │
│       │                  │                                             │
│       └────────┬─────────┘                                            │
│                │                                                       │
│                ▼                                                       │
│       ┌────────────────────────┐                                      │
│       │ 5. 复制数据到用户空间   │                                      │
│       │    copy_to_user()      │                                      │
│       └────────────────────────┘                                      │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────┐
│                          写入流程                                       │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│  write(fd, buf, 4096)                                                  │
│       │                                                                │
│       ▼                                                                │
│  ┌────────────────────────────────┐                                   │
│  │ 1. 复制数据到 Page Cache        │                                   │
│  │    copy_from_user()             │                                   │
│  └────────────┬───────────────────┘                                   │
│               │                                                        │
│               ▼                                                        │
│  ┌────────────────────────────────┐                                   │
│  │ 2. 标记页面为 Dirty             │                                   │
│  │    set_page_dirty()             │                                   │
│  └────────────┬───────────────────┘                                   │
│               │                                                        │
│               ▼                                                        │
│  ┌────────────────────────────────┐                                   │
│  │ 3. write() 立即返回             │   ← 写入是异步的!                │
│  └────────────┬───────────────────┘                                   │
│               │                                                        │
│               │    (后台进行)                                          │
│               ▼                                                        │
│  ┌────────────────────────────────┐                                   │
│  │ 4. 定期刷新 (flush/writeback)   │                                   │
│  │    - dirty_expire_centisecs    │   默认 3000 (30秒)                │
│  │    - dirty_writeback_centisecs │   默认 500 (5秒)                  │
│  └────────────┬───────────────────┘                                   │
│               │                                                        │
│               ▼                                                        │
│  ┌────────────────────────────────┐                                   │
│  │ 5. 写入磁盘                     │                                   │
│  │    块设备层提交 I/O             │                                   │
│  └────────────────────────────────┘                                   │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

4.3 查看 Page Cache 状态

# 方法 1: /proc/meminfo
$ cat /proc/meminfo | grep -E "^(Cached|Buffers|Dirty|Writeback)"
Buffers:          123456 kB    # 块设备缓冲
Cached:          2345678 kB    # Page Cache
Dirty:              1234 kB    # 脏页 (待写入)
Writeback:             0 kB    # 正在写入的页

# 方法 2: free 命令
$ free -h
              total        used        free      shared  buff/cache   available
Mem:           31Gi        10Gi       5.0Gi       256Mi        16Gi        20Gi
#                                                          ↑ Page Cache + Buffers

# 方法 3: vmstat
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 1  0      0 5242880 123456 2345678   0    0    10    20  100  200  5  2 93  0  0

# 方法 4: 查看特定文件的缓存状态
$ vmtouch /path/to/file
           Files: 1
     Directories: 0
  Resident Pages: 256/256  1M/1M  100%
         Elapsed: 0.0001 seconds

# 方法 5: 清除 Page Cache (危险操作!)
$ sync; echo 3 > /proc/sys/vm/drop_caches
# 1 = 清除 pagecache
# 2 = 清除 dentries + inodes
# 3 = 清除 all

4.4 内核源码:address_space 结构

// include/linux/fs.h
struct address_space {
    struct inode            *host;           // 所属的 inode
    struct xarray           i_pages;         // 页面的基数树 (radix tree)
    gfp_t                   gfp_mask;        // 分配页面的标志
    atomic_t                i_mmap_writable; // 可写映射计数
    struct rb_root_cached   i_mmap;          // mmap 映射的红黑树
    struct rw_semaphore     i_mmap_rwsem;    // mmap 的读写信号量
    unsigned long           nrpages;         // 页面总数
    unsigned long           nrexceptional;   // 特殊页面数
    pgoff_t                 writeback_index; // 写回起始索引
    const struct address_space_operations *a_ops; // 操作函数表
    unsigned long           flags;           // 标志
    errseq_t                wb_err;          // 写回错误序列号
    spinlock_t              private_lock;    // 私有锁
    struct list_head        private_list;    // 私有列表
    void                    *private_data;   // 私有数据
} __attribute__((aligned(sizeof(long))));

// 页面查找
struct page *find_get_page(struct address_space *mapping, pgoff_t offset)
{
    return pagecache_get_page(mapping, offset, 0, 0);
}

// 添加页面到 Page Cache
int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
                          pgoff_t offset, gfp_t gfp_mask)
{
    // ...
    __SetPageLocked(page);
    error = __add_to_page_cache_locked(page, mapping, offset, gfp_mask, &shadow);
    // ...
    lru_cache_add(page);  // 加入 LRU 列表
    return error;
}

五、内存回收机制

5.1 LRU 列表

┌────────────────────────────────────────────────────────────────────────┐
│                        LRU (Least Recently Used) 列表                   │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│   ┌─────────────────────────────────────────────────────────────────┐ │
│   │                     Active List (活跃列表)                       │ │
│   │  ┌──────┬──────┬──────┬──────┬──────┐                           │ │
│   │  │ Page │ Page │ Page │ Page │ Page │  ← 最近被访问的页面        │ │
│   │  │  1   │  2   │  3   │  4   │  5   │                           │ │
│   │  └──────┴──────┴──────┴──────┴──────┘                           │ │
│   │      ▲                          │                                │ │
│   │      │ 提升                      │ 降级                          │ │
│   │      │                          ▼                                │ │
│   │  ┌──────┬──────┬──────┬──────┬──────┐                           │ │
│   │  │ Page │ Page │ Page │ Page │ Page │  ← 不活跃页面              │ │
│   │  │  A   │  B   │  C   │  D   │  E   │                           │ │
│   │  └──────┴──────┴──────┴──────┴──────┘                           │ │
│   │                     Inactive List (不活跃列表)                   │ │
│   │                                    │                             │ │
│   │                                    │ 回收                        │ │
│   │                                    ▼                             │ │
│   │                              ┌──────────┐                        │ │
│   │                              │   回收    │                        │ │
│   │                              │ (或 Swap) │                        │ │
│   │                              └──────────┘                        │ │
│   └─────────────────────────────────────────────────────────────────┘ │
│                                                                        │
│   Linux 内核维护多个 LRU 列表:                                         │
│   - LRU_INACTIVE_ANON: 不活跃匿名页                                    │
│   - LRU_ACTIVE_ANON:   活跃匿名页                                      │
│   - LRU_INACTIVE_FILE: 不活跃文件页                                    │
│   - LRU_ACTIVE_FILE:   活跃文件页                                      │
│   - LRU_UNEVICTABLE:   不可回收页                                      │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

5.2 内存回收触发条件

┌────────────────────────────────────────────────────────────────────────┐
│                        内存回收触发条件                                 │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│   内存使用情况                                                          │
│   ┌─────────────────────────────────────────────────────────────────┐ │
│   │                                                                 │ │
│   │  ████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │
│   │  ←────── Used ──────────────→←─────────── Free ──────────────→ │ │
│   │                              │                                  │ │
│   │                     pages_high                                  │ │
│   │                              │                                  │ │
│   │                              │    pages_low                     │ │
│   │                              │       │                          │ │
│   │                              │       │    pages_min             │ │
│   │                              │       │       │                  │ │
│   └──────────────────────────────┴───────┴───────┴──────────────────┘ │
│                                                                        │
│   水位线说明:                                                          │
│   - pages_high: 高水位,超过此值停止回收                                │
│   - pages_low:  低水位,低于此值开始后台回收 (kswapd)                   │
│   - pages_min:  最小水位,低于此值触发直接回收 (direct reclaim)         │
│                                                                        │
│   回收方式:                                                            │
│   1. kswapd (后台回收)                                                  │
│      - 周期性检查内存状态                                               │
│      - 当 free < low 时唤醒                                            │
│      - 回收到 high 水位为止                                            │
│                                                                        │
│   2. Direct Reclaim (直接回收)                                          │
│      - 分配内存时 free < min                                           │
│      - 同步回收,会阻塞进程                                             │
│      - 性能影响较大                                                     │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

5.3 查看内存水位

# 查看各 zone 的水位
$ cat /proc/zoneinfo | grep -E "^Node|pages free|min|low|high"
Node 0, zone      DMA
  pages free     3968
        min      32
        low      40
        high     48
Node 0, zone    DMA32
  pages free     234567
        min      3456
        low      4320
        high     5184
Node 0, zone   Normal
  pages free     789012
        min      12345
        low      15432
        high     18519

# 查看 kswapd 活动
$ cat /proc/vmstat | grep pgscan
pgscan_kswapd 12345678       # kswapd 扫描的页面数
pgscan_direct 123456         # 直接回收扫描的页面数
pgscan_direct_throttle 0     # 被限流的直接回收

# 调整水位比例 (谨慎!)
$ cat /proc/sys/vm/watermark_scale_factor
10  # 默认值,表示 0.1%

5.4 内核源码:页面回收

// mm/vmscan.c
// kswapd 主循环
static int kswapd(void *p)
{
    pg_data_t *pgdat = (pg_data_t *)p;

    for ( ; ; ) {
        // ...
        kswapd_try_to_sleep(pgdat, alloc_order, reclaim_order);

        // 检查是否需要回收
        if (!balanced) {
            balance_pgdat(pgdat, order, classzone_idx);
        }
    }
    return 0;
}

// 页面回收入口
static unsigned long shrink_lruvec(struct lruvec *lruvec,
                                   struct scan_control *sc)
{
    unsigned long nr_reclaimed = 0;

    // 扫描不活跃匿名页
    if (get_nr_swap_pages() > 0) {
        nr_reclaimed += shrink_list(LRU_INACTIVE_ANON, lruvec, sc);
    }

    // 扫描不活跃文件页 (Page Cache)
    nr_reclaimed += shrink_list(LRU_INACTIVE_FILE, lruvec, sc);

    return nr_reclaimed;
}

// 回收单个页面
static unsigned int shrink_page_list(struct list_head *page_list,
                                     struct scan_control *sc)
{
    // ...
    while (!list_empty(page_list)) {
        page = list_entry(page_list->prev, struct page, lru);

        // 检查页面是否可以回收
        if (!page_evictable(page)) {
            // 移动到 unevictable 列表
            continue;
        }

        // 尝试回收
        if (page_mapped(page)) {
            // 解除所有映射
            try_to_unmap(page, TTU_BATCH_FLUSH);
        }

        if (PageDirty(page)) {
            // 脏页需要先写回
            pageout(page, mapping);
        }

        // 释放页面
        __remove_mapping(mapping, page);
        nr_reclaimed++;
    }
    // ...
}

六、实战:内存分析与排查

6.1 实验 1:观察 Page Cache 行为

#!/bin/bash
# page_cache_test.sh

# 创建测试文件
dd if=/dev/zero of=/tmp/testfile bs=1M count=100

# 清空 Page Cache
sync
echo 3 > /proc/sys/vm/drop_caches

echo "=== 清空 Page Cache 后 ==="
free -m

echo "=== 首次读取 (Cache Miss) ==="
time dd if=/tmp/testfile of=/dev/null bs=1M

echo "=== 读取后内存状态 ==="
free -m

echo "=== 再次读取 (Cache Hit) ==="
time dd if=/tmp/testfile of=/dev/null bs=1M

# 查看文件缓存状态
vmtouch /tmp/testfile

# 清理
rm /tmp/testfile

6.2 实验 2:分析进程内存

#!/usr/bin/env python3
# analyze_memory.py

import os
import sys

def parse_smaps(pid):
    """解析 /proc/[pid]/smaps 获取详细内存信息"""

    smaps_path = f"/proc/{pid}/smaps"

    if not os.path.exists(smaps_path):
        print(f"Process {pid} not found")
        return

    total_size = 0
    total_rss = 0
    total_pss = 0
    total_private = 0
    total_shared = 0

    mappings = []
    current_mapping = None

    with open(smaps_path) as f:
        for line in f:
            if line[0] not in ' \t':
                # 新的映射区域
                if current_mapping:
                    mappings.append(current_mapping)
                parts = line.split()
                addr_range = parts[0]
                perms = parts[1]
                path = parts[-1] if len(parts) > 5 else "[anonymous]"
                current_mapping = {
                    'range': addr_range,
                    'perms': perms,
                    'path': path,
                    'size': 0,
                    'rss': 0,
                    'pss': 0,
                    'private_clean': 0,
                    'private_dirty': 0,
                    'shared_clean': 0,
                    'shared_dirty': 0,
                }
            else:
                # 属性行
                parts = line.strip().split(':')
                if len(parts) == 2:
                    key = parts[0].strip()
                    value = int(parts[1].strip().split()[0])

                    if key == 'Size':
                        current_mapping['size'] = value
                        total_size += value
                    elif key == 'Rss':
                        current_mapping['rss'] = value
                        total_rss += value
                    elif key == 'Pss':
                        current_mapping['pss'] = value
                        total_pss += value
                    elif key == 'Private_Clean':
                        current_mapping['private_clean'] = value
                        total_private += value
                    elif key == 'Private_Dirty':
                        current_mapping['private_dirty'] = value
                        total_private += value
                    elif key == 'Shared_Clean':
                        current_mapping['shared_clean'] = value
                        total_shared += value
                    elif key == 'Shared_Dirty':
                        current_mapping['shared_dirty'] = value
                        total_shared += value

    if current_mapping:
        mappings.append(current_mapping)

    # 打印汇总
    print(f"\n{'='*60}")
    print(f"Process {pid} Memory Analysis")
    print(f"{'='*60}")
    print(f"{'Metric':<20} {'Value (KB)':<15} {'Value (MB)':<15}")
    print(f"{'-'*60}")
    print(f"{'Virtual Size':<20} {total_size:<15} {total_size/1024:<15.2f}")
    print(f"{'RSS':<20} {total_rss:<15} {total_rss/1024:<15.2f}")
    print(f"{'PSS':<20} {total_pss:<15} {total_pss/1024:<15.2f}")
    print(f"{'Private':<20} {total_private:<15} {total_private/1024:<15.2f}")
    print(f"{'Shared':<20} {total_shared:<15} {total_shared/1024:<15.2f}")
    print(f"{'USS (≈Private)':<20} {total_private:<15} {total_private/1024:<15.2f}")

    # 打印 Top 10 内存区域
    print(f"\n{'='*60}")
    print("Top 10 Memory Regions by PSS:")
    print(f"{'='*60}")

    mappings.sort(key=lambda x: x['pss'], reverse=True)
    for i, m in enumerate(mappings[:10]):
        print(f"{i+1:2}. PSS: {m['pss']:>8} KB | {m['path'][:50]}")

if __name__ == "__main__":
    pid = sys.argv[1] if len(sys.argv) > 1 else os.getpid()
    parse_smaps(pid)

6.3 常用排查命令

# 系统整体内存
free -h
cat /proc/meminfo

# 按内存排序的进程
ps aux --sort=-%mem | head -20

# 详细的进程内存
pmap -x <pid>
cat /proc/<pid>/smaps_rollup

# 内存使用趋势
vmstat 1 10
sar -r 1 10

# Page Cache 使用
cat /proc/meminfo | grep -E "^(Cached|Buffers|Dirty)"

# 内存回收统计
cat /proc/vmstat | grep -E "^(pgscan|pgsteal|pgactivate)"

# 检查是否有 Swap 活动
vmstat 1 | awk '{print $7, $8}'  # si, so 列

# OOM 日志
dmesg | grep -i "oom\|killed"
journalctl -k | grep -i "oom\|killed"

七、面试要点

7.1 高频问题

  1. 虚拟内存和物理内存的区别?

    • 虚拟内存是进程看到的地址空间,是抽象层
    • 物理内存是实际的 RAM
    • 通过页表建立映射关系
  2. RSS 和 PSS 的区别?

    • RSS 包含完整的共享库大小
    • PSS 按共享比例分摊
    • PSS 之和 = 实际物理内存使用
  3. Page Cache 的作用?

    • 缓存文件数据,加速读写
    • 写入是异步的(先写 Cache)
    • 内存紧张时可以回收
  4. 如何判断内存是否充足?

    • 看 available 而非 free
    • 检查 swap 使用和活动
    • 观察 kswapd 是否频繁活动

7.2 进阶问题

  1. 页表为什么是多级的?

    • 节省内存:不需要的页表项不分配
    • 64 位地址空间太大,单级页表不现实
  2. TLB 是什么?为什么重要?

    • Translation Lookaside Buffer
    • 缓存页表项,加速地址转换
    • TLB Miss 代价高(需要遍历页表)
  3. 内存回收的优先级?

    • 先回收 Page Cache(文件页)
    • 再考虑 Swap(匿名页)
    • 最后触发 OOM Killer

相关链接

  • 03-CGroup资源控制 - 资源控制基础
  • 20-OOM-Killer机制详解 - OOM 详细分析
  • 21-Cgroup内存控制深度 - 容器内存限制

下一步:让我们深入了解 OOM Killer 是如何工作的!