HiHuo
首页
博客
手册
工具
首页
博客
手册
工具
  • 系统底层修炼

    • 操作系统核心知识学习指南
    • CPU调度与上下文切换
    • CFS调度器原理与源码
    • 内存管理与虚拟内存
    • PageCache与内存回收
    • 文件系统与IO优化
    • 零拷贝与Direct I/O
    • 网络子系统架构
    • TCP协议深度解析
    • TCP问题排查实战
    • 网络性能优化
    • epoll与IO多路复用
    • 进程与线程管理
    • Go Runtime调度器GMP模型
    • 系统性能分析方法论
    • DPDK与用户态网络栈
    • eBPF与内核可观测性
    • 综合实战案例

内存管理与虚拟内存

章节概述

内存管理是操作系统的核心功能之一,它负责为进程分配和管理内存资源。本章将深入探讨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处理:

  1. 硬件触发TLB Miss异常
  2. 软件查找页表
  3. 更新TLB
  4. 重新执行指令

源码解析

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:观测进程内存布局

目标: 理解进程的虚拟内存空间布局

步骤:

  1. 编译测试程序:
gcc -o memory_test memory_test.c
  1. 运行程序并观测:
# 终端1:运行程序
./memory_test

# 终端2:观测内存布局
pmap -x $(pgrep memory_test)
  1. 分析结果:
  • 观察代码段、数据段、堆、栈的地址范围
  • 查看内存权限(r-xp, rw-p等)
  • 理解虚拟地址空间的组织

实验2:内存分配器行为观测

目标: 理解不同内存分配方式的行为差异

步骤:

  1. 编译内存分配测试程序:
gcc -o alloc_test alloc_test.c
  1. 运行程序:
./alloc_test
  1. 观测内存使用:
# 在另一个终端观测
watch -n 1 'pmap -x $(pgrep alloc_test) | tail -20'
  1. 分析结果:
  • 比较malloc和mmap的地址分布
  • 观察内存权限的差异
  • 理解不同分配方式的特点

实验3:内存泄漏检测

目标: 学会使用工具检测内存泄漏

步骤:

  1. 编译内存泄漏测试程序:
gcc -o leak_test leak_test.c
  1. 使用valgrind检测:
valgrind --leak-check=full --show-leak-kinds=all ./leak_test
  1. 使用AddressSanitizer:
gcc -fsanitize=address -g -o leak_test_asan leak_test.c
./leak_test_asan
  1. 分析结果:
  • 理解valgrind的输出
  • 学习如何修复内存泄漏
  • 掌握内存调试工具的使用

实验4:页表大小实验

目标: 理解页表大小对性能的影响

步骤:

  1. 创建大内存访问程序:
// 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;
}
  1. 编译并运行:
gcc -o page_table_test page_table_test.c
./page_table_test
  1. 观测页表使用:
# 查看页表统计
cat /proc/meminfo | grep -i page

常见问题

Q1: 为什么需要虚拟内存?

A: 虚拟内存提供了以下好处:

  • 内存保护:每个进程有独立的地址空间
  • 内存共享:多个进程可以共享代码段
  • 内存扩展:可以使用交换空间扩展内存
  • 简化编程:程序员不需要关心物理内存布局

Q2: 页表大小如何影响性能?

A: 页表大小影响:

  • 内存占用:页表本身占用内存
  • 访问速度:页表查找需要时间
  • TLB命中率:页表大小影响TLB效果

Q3: 如何优化内存使用?

A: 可以通过以下方式:

  • 使用内存池减少分配开销
  • 合理使用mmap进行大内存分配
  • 避免内存碎片
  • 使用内存压缩技术

Q4: 如何诊断内存泄漏?

A: 使用以下工具:

  • valgrind:检测内存泄漏和错误
  • AddressSanitizer:编译时检测
  • pmap:观测进程内存使用
  • /proc/meminfo:系统内存统计

复习题

选择题

  1. 在x86-64系统中,页的大小通常是:

    • A. 2KB
    • B. 4KB
    • C. 8KB
    • D. 16KB
  2. TLB的主要作用是:

    • A. 缓存页表项
    • B. 管理内存分配
    • C. 处理缺页中断
    • D. 控制内存权限
  3. 以下哪个命令可以查看进程的内存映射?

    • A. free
    • B. top
    • C. pmap
    • D. ps
  4. 缺页中断的主要原因是:

    • A. 内存不足
    • B. 页表项不存在
    • C. 权限不足
    • D. 以上都是
  5. 在Linux中,malloc()通常使用哪种内存分配器?

    • A. SLAB
    • B. SLUB
    • C. SLOB
    • D. 以上都不是

简答题

  1. 解释虚拟地址到物理地址的转换过程。

  2. 为什么需要多级页表?它有什么优缺点?

  3. 解释缺页中断的处理流程。

  4. 比较malloc()和mmap()的优缺点。

  5. 如何检测和修复内存泄漏?

实战题

  1. 性能分析题: 一个程序频繁调用malloc()和free(),导致性能下降。请分析原因并提出优化方案。

  2. 内存管理题: 设计一个内存池管理器,要求能够高效地分配和释放固定大小的内存块。

  3. 故障排查题: 系统出现内存不足的警告,但free命令显示还有大量可用内存。请分析可能的原因和解决方案。

扩展阅读

推荐资源

  • Linux内存管理源码
  • Understanding the Linux Virtual Memory Manager
  • Memory Management Reference

深入方向

  • 内存压缩技术
  • 大页(Huge Pages)优化
  • NUMA内存管理
  • 内存热插拔

下一章预告: 我们将深入探讨PageCache机制,包括读写缓存、脏页回写策略,以及如何优化文件IO性能。

Prev
CFS调度器原理与源码
Next
PageCache与内存回收