内存管理与虚拟内存
章节概述
内存管理是操作系统的核心功能之一,它负责为进程分配和管理内存资源。本章将深入探讨Linux的内存管理机制,包括虚拟内存、页表、缺页中断等核心概念,以及如何通过工具观测和优化内存使用。
学习目标:
- 理解虚拟内存的基本原理和实现机制
- 掌握页表、TLB、缺页中断的工作原理
- 学会使用pmap、smem等工具分析内存使用
- 能够诊断和解决内存相关的性能问题
核心概念
1. 虚拟内存架构
虚拟地址空间布局:
高地址 0xFFFFFFFF
┌─────────────────┐
│ 内核空间 │ ← 内核态访问
├─────────────────┤
│ 用户栈 │ ← 向下增长
├─────────────────┤
│ ↓ │
│ ↑ │
├─────────────────┤
│ 用户堆 │ ← 向上增长
├─────────────────┤
│ BSS段 │ ← 未初始化全局变量
├─────────────────┤
│ 数据段 │ ← 已初始化全局变量
├─────────────────┤
│ 代码段 │ ← 程序代码
└─────────────────┘
低地址 0x00000000
关键概念:
- 虚拟地址(VA):进程看到的地址空间
- 物理地址(PA):实际内存中的地址
- 页表(Page Table):虚拟地址到物理地址的映射
- 页(Page):内存管理的基本单位(通常4KB)
2. 页表机制
多级页表结构:
graph TD
A[虚拟地址] --> B[页目录项]
B --> C[页表项]
C --> D[物理页]
A1[31-22位] --> B
A2[21-12位] --> C
A3[11-0位] --> D
页表项结构:
31-12位: 物理页号
11-0位: 标志位
├─ P位: 页是否存在
├─ R/W位: 读写权限
├─ U/S位: 用户/系统权限
└─ 其他控制位
3. TLB(Translation Lookaside Buffer)
TLB工作原理:
- 缓存最近使用的页表项
- 减少页表查找的开销
- 提高地址转换速度
TLB Miss处理:
- 硬件触发TLB Miss异常
- 软件查找页表
- 更新TLB
- 重新执行指令
源码解析
1. 页表管理
关键文件: arch/x86/mm/pgtable.c
// 页表项结构定义
typedef struct {
unsigned long pte;
} pte_t;
// 页目录项结构定义
typedef struct {
unsigned long pde;
} pde_t;
// 页表项操作
static inline pte_t pte_mkwrite(pte_t pte)
{
pte.pte |= _PAGE_RW;
return pte;
}
static inline pte_t pte_mkread(pte_t pte)
{
pte.pte |= _PAGE_USER;
return pte;
}
// 页表查找
static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long address)
{
return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address);
}
2. 缺页中断处理
关键文件: arch/x86/mm/fault.c
// 缺页中断处理函数
dotraplinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
unsigned long address = read_cr2(); /* 获取触发缺页的地址 */
struct vm_area_struct *vma;
struct mm_struct *mm;
int fault;
mm = current->mm;
// 查找虚拟内存区域
vma = find_vma(mm, address);
if (!vma) {
bad_area(regs, error_code, address);
return;
}
// 检查访问权限
if (vma->vm_start <= address) {
good_area(regs, error_code, address);
return;
}
// 处理缺页
fault = handle_mm_fault(vma, address, flags);
if (fault & VM_FAULT_ERROR) {
if (fault & VM_FAULT_OOM)
pagefault_out_of_memory();
else if (fault & VM_FAULT_SIGBUS)
do_sigbus(regs, error_code, address);
return;
}
}
3. 内存分配器
关键文件: mm/slab.c
// SLAB分配器结构
struct kmem_cache {
struct array_cache *array[NR_CPUS];
unsigned int batchcount;
unsigned int limit;
unsigned int shared;
unsigned int size;
unsigned int align;
unsigned int flags;
const char *name;
struct list_head next;
struct kobj_ns_type_operations *kobj_ns_type_operations;
};
// 内存分配函数
void *kmalloc(size_t size, gfp_t flags)
{
struct kmem_cache *cachep;
void *ret;
if (unlikely(size > KMALLOC_MAX_CACHE_SIZE))
return kmalloc_large(size, flags);
cachep = kmalloc_slab(size, flags);
if (unlikely(ZERO_OR_NULL_PTR(cachep)))
return cachep;
ret = slab_alloc(cachep, flags, _RET_IP_);
trace_kmalloc(_RET_IP_, ret, size, cachep->size, flags);
return ret;
}
️ 实用命令
1. 内存使用观测
free - 系统内存使用情况
# 显示内存使用情况
free -h
# 持续监控内存使用
watch -n 1 free -h
# 输出解释
# total: 总内存
# used: 已使用内存
# free: 空闲内存
# shared: 共享内存
# buff/cache: 缓冲区和缓存
# available: 可用内存
示例输出:
total used free shared buff/cache available
Mem: 31Gi 12Gi 2.3Gi 1.2Gi 16Gi 19Gi
Swap: 2.0Gi 0B 2.0Gi
2. 进程内存分析
pmap - 进程内存映射
# 显示进程内存映射
pmap -x <pid>
# 显示详细信息
pmap -XX <pid>
# 输出解释
# Address: 虚拟地址范围
# Kbytes: 内存大小(KB)
# RSS: 实际物理内存
# Dirty: 脏页数量
# Mode: 访问权限
# Mapping: 映射类型
示例输出:
Address Kbytes RSS Dirty Mode Mapping
0000000000400000 4 4 0 r-xp /bin/cat
0000000000600000 4 4 4 rw-p /bin/cat
0000000000601000 132 4 4 rw-p [heap]
00007f7b7c000000 1024 512 0 rw-p [anon]
3. 详细内存统计
smem - 精确内存统计
# 安装smem
sudo apt install smem
# 显示进程内存使用
smem -r
# 按内存使用排序
smem -r -s pss
# 输出解释
# PSS: 比例集大小(Proportional Set Size)
# USS: 唯一集大小(Unique Set Size)
# RSS: 常驻集大小(Resident Set Size)
4. 内存分配观测
查看内存分配统计
# 查看内存分配统计
cat /proc/meminfo
# 查看页表信息
cat /proc/zoneinfo
# 查看内存碎片
cat /proc/buddyinfo
代码示例
1. 内存映射示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
void *addr;
size_t size = 4096; // 4KB
// 创建匿名内存映射
addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
perror("mmap");
exit(1);
}
printf("映射地址: %p\n", addr);
// 使用映射的内存
char *str = (char*)addr;
sprintf(str, "Hello, Memory Mapping!");
printf("内容: %s\n", str);
// 解除映射
if (munmap(addr, size) == -1) {
perror("munmap");
exit(1);
}
return 0;
}
2. 内存分配器测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void test_malloc() {
printf("=== malloc测试 ===\n");
// 分配内存
char *ptr = malloc(1024);
if (!ptr) {
perror("malloc");
return;
}
printf("分配地址: %p\n", ptr);
// 使用内存
strcpy(ptr, "Hello, malloc!");
printf("内容: %s\n", ptr);
// 释放内存
free(ptr);
printf("内存已释放\n");
}
void test_mmap() {
printf("\n=== mmap测试 ===\n");
// 创建文件
int fd = open("/tmp/test_mmap", O_CREAT | O_RDWR, 0644);
if (fd == -1) {
perror("open");
return;
}
// 设置文件大小
if (ftruncate(fd, 4096) == -1) {
perror("ftruncate");
close(fd);
return;
}
// 内存映射
void *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
perror("mmap");
close(fd);
return;
}
printf("映射地址: %p\n", addr);
// 使用映射的内存
char *str = (char*)addr;
sprintf(str, "Hello, mmap!");
printf("内容: %s\n", str);
// 同步到文件
if (msync(addr, 4096, MS_SYNC) == -1) {
perror("msync");
}
// 解除映射
munmap(addr, 4096);
close(fd);
printf("mmap测试完成\n");
}
int main() {
test_malloc();
test_mmap();
return 0;
}
3. 内存泄漏检测
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// 模拟内存泄漏
void memory_leak() {
for (int i = 0; i < 1000; i++) {
char *ptr = malloc(1024);
if (ptr) {
// 故意不释放内存,造成泄漏
sprintf(ptr, "Leaked memory %d", i);
}
}
}
// 正常内存管理
void proper_memory_management() {
for (int i = 0; i < 1000; i++) {
char *ptr = malloc(1024);
if (ptr) {
sprintf(ptr, "Proper memory %d", i);
free(ptr); // 正确释放内存
}
}
}
int main() {
printf("PID: %d\n", getpid());
printf("按回车键开始内存泄漏测试...\n");
getchar();
memory_leak();
printf("内存泄漏测试完成\n");
printf("按回车键开始正常内存管理测试...\n");
getchar();
proper_memory_management();
printf("正常内存管理测试完成\n");
printf("按回车键退出...\n");
getchar();
return 0;
}
动手实验
实验1:观测进程内存布局
目标: 理解进程的虚拟内存空间布局
步骤:
- 编译测试程序:
gcc -o memory_test memory_test.c
- 运行程序并观测:
# 终端1:运行程序
./memory_test
# 终端2:观测内存布局
pmap -x $(pgrep memory_test)
- 分析结果:
- 观察代码段、数据段、堆、栈的地址范围
- 查看内存权限(r-xp, rw-p等)
- 理解虚拟地址空间的组织
实验2:内存分配器行为观测
目标: 理解不同内存分配方式的行为差异
步骤:
- 编译内存分配测试程序:
gcc -o alloc_test alloc_test.c
- 运行程序:
./alloc_test
- 观测内存使用:
# 在另一个终端观测
watch -n 1 'pmap -x $(pgrep alloc_test) | tail -20'
- 分析结果:
- 比较malloc和mmap的地址分布
- 观察内存权限的差异
- 理解不同分配方式的特点
实验3:内存泄漏检测
目标: 学会使用工具检测内存泄漏
步骤:
- 编译内存泄漏测试程序:
gcc -o leak_test leak_test.c
- 使用valgrind检测:
valgrind --leak-check=full --show-leak-kinds=all ./leak_test
- 使用AddressSanitizer:
gcc -fsanitize=address -g -o leak_test_asan leak_test.c
./leak_test_asan
- 分析结果:
- 理解valgrind的输出
- 学习如何修复内存泄漏
- 掌握内存调试工具的使用
实验4:页表大小实验
目标: 理解页表大小对性能的影响
步骤:
- 创建大内存访问程序:
// page_table_test.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <time.h>
int main() {
size_t size = 1024 * 1024 * 1024; // 1GB
char *ptr = malloc(size);
if (!ptr) {
perror("malloc");
return 1;
}
printf("分配了1GB内存,地址: %p\n", ptr);
// 随机访问内存
clock_t start = clock();
for (int i = 0; i < 1000000; i++) {
int index = rand() % (size - 1);
ptr[index] = 'A';
}
clock_t end = clock();
printf("随机访问时间: %f秒\n",
(double)(end - start) / CLOCKS_PER_SEC);
free(ptr);
return 0;
}
- 编译并运行:
gcc -o page_table_test page_table_test.c
./page_table_test
- 观测页表使用:
# 查看页表统计
cat /proc/meminfo | grep -i page
常见问题
Q1: 为什么需要虚拟内存?
A: 虚拟内存提供了以下好处:
- 内存保护:每个进程有独立的地址空间
- 内存共享:多个进程可以共享代码段
- 内存扩展:可以使用交换空间扩展内存
- 简化编程:程序员不需要关心物理内存布局
Q2: 页表大小如何影响性能?
A: 页表大小影响:
- 内存占用:页表本身占用内存
- 访问速度:页表查找需要时间
- TLB命中率:页表大小影响TLB效果
Q3: 如何优化内存使用?
A: 可以通过以下方式:
- 使用内存池减少分配开销
- 合理使用mmap进行大内存分配
- 避免内存碎片
- 使用内存压缩技术
Q4: 如何诊断内存泄漏?
A: 使用以下工具:
- valgrind:检测内存泄漏和错误
- AddressSanitizer:编译时检测
- pmap:观测进程内存使用
- /proc/meminfo:系统内存统计
复习题
选择题
在x86-64系统中,页的大小通常是:
- A. 2KB
- B. 4KB
- C. 8KB
- D. 16KB
TLB的主要作用是:
- A. 缓存页表项
- B. 管理内存分配
- C. 处理缺页中断
- D. 控制内存权限
以下哪个命令可以查看进程的内存映射?
- A. free
- B. top
- C. pmap
- D. ps
缺页中断的主要原因是:
- A. 内存不足
- B. 页表项不存在
- C. 权限不足
- D. 以上都是
在Linux中,malloc()通常使用哪种内存分配器?
- A. SLAB
- B. SLUB
- C. SLOB
- D. 以上都不是
简答题
解释虚拟地址到物理地址的转换过程。
为什么需要多级页表?它有什么优缺点?
解释缺页中断的处理流程。
比较malloc()和mmap()的优缺点。
如何检测和修复内存泄漏?
实战题
性能分析题: 一个程序频繁调用malloc()和free(),导致性能下降。请分析原因并提出优化方案。
内存管理题: 设计一个内存池管理器,要求能够高效地分配和释放固定大小的内存块。
故障排查题: 系统出现内存不足的警告,但free命令显示还有大量可用内存。请分析可能的原因和解决方案。
扩展阅读
推荐资源
深入方向
- 内存压缩技术
- 大页(Huge Pages)优化
- NUMA内存管理
- 内存热插拔
下一章预告: 我们将深入探讨PageCache机制,包括读写缓存、脏页回写策略,以及如何优化文件IO性能。