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 高频问题
虚拟内存和物理内存的区别?
- 虚拟内存是进程看到的地址空间,是抽象层
- 物理内存是实际的 RAM
- 通过页表建立映射关系
RSS 和 PSS 的区别?
- RSS 包含完整的共享库大小
- PSS 按共享比例分摊
- PSS 之和 = 实际物理内存使用
Page Cache 的作用?
- 缓存文件数据,加速读写
- 写入是异步的(先写 Cache)
- 内存紧张时可以回收
如何判断内存是否充足?
- 看 available 而非 free
- 检查 swap 使用和活动
- 观察 kswapd 是否频繁活动
7.2 进阶问题
页表为什么是多级的?
- 节省内存:不需要的页表项不分配
- 64 位地址空间太大,单级页表不现实
TLB 是什么?为什么重要?
- Translation Lookaside Buffer
- 缓存页表项,加速地址转换
- TLB Miss 代价高(需要遍历页表)
内存回收的优先级?
- 先回收 Page Cache(文件页)
- 再考虑 Swap(匿名页)
- 最后触发 OOM Killer
相关链接
- 03-CGroup资源控制 - 资源控制基础
- 20-OOM-Killer机制详解 - OOM 详细分析
- 21-Cgroup内存控制深度 - 容器内存限制
下一步:让我们深入了解 OOM Killer 是如何工作的!