找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

224

积分

0

好友

27

主题
发表于 14 小时前 | 查看: 1| 回复: 0

前言:虚拟内存的必要性

如果每个程序都直接操作物理内存,不同的程序可能会互相覆盖数据,系统崩溃将成为常态。为了解决这个问题,虚拟地址空间应运而生。它为每个进程提供了一个独立的、连续的地址视图,让每个程序都以为自己独占了整个内存空间(例如32位系统上的4GB)。操作系统作为管理者,负责将这些虚拟地址映射到实际的物理内存上,实现了隔离与保护。

一、基础概念:分段与分页机制

1.1 历史演进:从分段到分页

早期的x86架构使用分段机制进行内存管理,但现代Linux主要采用分页机制

机制 优点 缺点 Linux中的使用
分段 逻辑清晰,保护性强 内存碎片化,效率低 仅用于兼容和保护
分页 灵活高效,支持虚拟内存 实现复杂 主要内存管理机制

1.2 分页机制的核心思想

分页机制将物理内存和虚拟地址空间都切分成固定大小的“页”(通常为4KB)。通过页表这一核心数据结构,建立虚拟页到物理页的映射关系,完成地址转换。

// 核心概念:地址转换
虚拟地址 → [页表查询] → 物理地址

二、Linux虚拟地址空间布局

2.1 32位系统的经典布局

0xFFFFFFFF +-----------------+
            |     内核空间     | 1GB (3-4GB)
0xC0000000 +-----------------+
            |                 |
            |     栈(stack)   | 向下增长
            |        |        |
            |        v        |
            |                 |
            |     内存映射区   | 文件映射、共享库
            |                 |
            |        ^        |
            |        |        |
            |       堆(heap)  | 向上增长
            |                 |
            |    BSS段(未初始化数据) |
            |    DATA段(初始化数据) |
            |    TEXT段(代码)   |
0x08048000 +-----------------+
            |  保留区(空指针等) |
0x00000000 +-----------------+

生活比喻:这就像一栋大楼的楼层分配:

  • 地下室(0x00000000):危险区域,禁止进入(空指针访问会段错误)
  • 1-3层(0x08048000开始):公司固定办公区(代码、数据)
  • 中间楼层:可扩展的会议室和临时工位(堆、栈)
  • 顶层(0xC0000000):大楼管理办公室(内核空间)

2.2 64位系统的变化

64位系统拥有巨大的地址空间(通常为用户空间128TB + 内核空间128TB),布局更为灵活。

// x86_64典型布局(简化)
0x7FFFFFFFFFFF +-----------------+
                |     内核空间     | ~128TB
0xFFFF8000000000+-----------------+
                |     栈(stack)   |
                |     共享库       |
                |     堆(heap)    |
                |     数据段      |
                |     代码段      |
0x000000000000  +-----------------+

三、核心数据结构深度剖析

3.1 进程描述符中的内存管理信息

每个进程的task_struct中都包含指向mm_struct的指针,这是进程内存管理的核心描述符。

// include/linux/sched.h 中 task_struct 的部分定义
struct task_struct {
    // ...
    struct mm_struct *mm;      // 进程内存描述符
    struct mm_struct *active_mm;
    // ...
};

// include/linux/mm_types.h
struct mm_struct {
    struct {
        struct vm_area_struct *mmap; // VMA链表
        struct rb_root mm_rb;       // VMA红黑树
        unsigned long mmap_base;    // 内存映射起始地址
        unsigned long task_size;    // 进程虚拟内存大小

        // 各段边界
        unsigned long start_code, end_code;
        unsigned long start_data, end_data;
        unsigned long start_brk, brk, start_stack;
        unsigned long arg_start, arg_end, env_start, env_end;

        pgd_t *pgd;                 // 页全局目录

        atomic_t mm_users;          // 使用该地址空间的用户数
        atomic_t mm_count;          // 对mm_struct的引用计数

        // 统计信息
        unsigned long total_vm;     // 总映射页数
        unsigned long locked_vm;    // 锁定页数
        unsigned long pinned_vm;    // 固定页数
        unsigned long data_vm;      // 数据页数
        unsigned long exec_vm;      // 可执行页数
        unsigned long stack_vm;     // 栈页数
    } __randomize_layout;
};

3.2 虚拟内存区域(VMA)

vm_area_struct(VMA)描述进程地址空间中一段连续的、具有相同访问权限和属性的内存区域,例如一个代码段、一个共享库映射或一块堆内存。

struct vm_area_struct {
    unsigned long vm_start;         // 区域起始地址
    unsigned long vm_end;           // 区域结束地址

    struct mm_struct *vm_mm;        // 所属的内存描述符

    // 权限和标志
    pgprot_t vm_page_prot;
    unsigned long vm_flags;

    // 链接结构
    struct vm_area_struct *vm_next, *vm_prev;
    struct rb_node vm_rb;

    // 文件相关信息(如果是文件映射)
    struct file *vm_file;
    unsigned long vm_pgoff;

    // 操作函数集
    const struct vm_operations_struct *vm_ops;

    // ...
};

3.3 关键数据结构关系图

图片

四、地址转换机制详解

4.1 四级页表结构(x86_64)

现代Linux在x86_64架构上使用四级页表进行地址转换。

虚拟地址(64位):
[63:48] 符号扩展位
[47:39] PGD索引(页全局目录)
[38:30] PUD索引(页上层目录)
[29:21] PMD索引(页中间目录)
[20:12] PTE索引(页表项)
[11:0]  页内偏移

转换过程

物理地址 = walk_page_table(虚拟地址) {
    pgd = mm->pgd + PGD_INDEX(虚拟地址)
    pud = *pgd + PUD_INDEX(虚拟地址)
    pmd = *pud + PMD_INDEX(虚拟地址)
    pte = *pmd + PTE_INDEX(虚拟地址)
    物理页框 = *pte
    物理地址 = 物理页框 * PAGE_SIZE + 页内偏移
}

4.2 页表项(PTE)的构成

页表项(Page Table Entry, PTE)不仅存储物理页框号,还包含丰富的控制位。

典型的x86_64 PTE:
63    62 61 60 59 58 57 56 55 54 53 52 ...... 12 11 10 9 8 7 6 5 4 3 2 1 0
|     |  |  |  |  |  |  |  |  |  |           |   |  | | | | | | | | | | |
+-----+--+--+--+--+--+--+--+--+--+--+----------+---+--+-+-+-+-+-+-+-++-+
  保留   N  G  PAT D  A  PCD PWT U  W         物理页框基址           P  W  U  R  P
                                                   (40位)             C  /  /  /  C  /
                                                                      D  S  S  W  D  /
                                                                                    |
                                                                                  存在位

关键标志位:

  • P(Present):页是否在物理内存中(缺页中断触发点)。
  • R/W(Read/Write):读写权限。
  • U/S(User/Supervisor):用户/内核模式访问权限。
  • A(Accessed):页是否被访问过(用于页面置换算法)。
  • D(Dirty):页是否被修改过。

五、缺页中断处理机制

5.1 缺页中断触发场景

场景 原因 处理方式
首次访问 页表项不存在 分配物理页,建立映射
页面被换出 P位为0 从交换分区读回
写时复制 只读页尝试写入 复制页面,修改权限
权限违规 无访问权限 发送SIGSEGV信号

5.2 缺页中断处理流程

// 简化的缺页处理逻辑
do_page_fault(虚拟地址, 错误码) {
    1. 检查地址是否在有效VMA范围内
    2. 检查访问权限是否匹配
    3. 根据错误类型处理:
        - 页面未分配:调用handle_mm_fault分配
        - 页面被换出:调用do_swap_page换入
        - 写时复制:调用do_wp_page复制页面
    4. 更新页表项,恢复执行
}

六、核心操作原理解析

6.1 内存分配:brk与mmap

用户态进程主要通过brkmmap系统调用来动态获取内存。

brk/sbrk系统调用:通过调整堆顶(program break)指针来扩展或收缩堆内存。传统malloc用于分配小块内存时会使用此机制。

// 传统堆内存分配
void *malloc(size_t size) {
    if (size < 阈值) {
        // 使用brk扩展堆
        // 从堆中分配小块内存
    } else {
        // 使用mmap创建独立映射
    }
}

mmap系统调用:创建新的内存映射,可用于分配大块匿名内存或映射文件。

void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
两种方式的对比: 特性 brk/sbrk mmap
适用场景 小对象频繁分配 大块内存、文件映射
管理方式 连续的堆区域 独立的映射区域
释放效率 只能释放堆顶部分 可独立释放任意映射
碎片问题 容易产生碎片 相对较少碎片
使用示例 malloc小内存 大文件读写、共享内存

6.2 写时复制(Copy-on-Write)

COW的精妙之处在于延迟复制,以此节省系统资源。它在fork()系统调用中扮演关键角色。

// fork()时的COW处理
pid_t fork(void) {
    1. 复制父进程的mm_struct
    2. 复制页表,但将所有页表项设为只读
    3. 当任一进程尝试写页面时,触发缺页中断
    4. 中断处理程序复制物理页,修改映射
    5. 恢复进程执行
}

生活比喻:就像兄弟姐妹共用一本教科书。开始大家都读同一本,但当某个人要做笔记时,才去复印自己的一本,不影响其他人继续使用原书。

七、实战示例:简单内存操作程序

7.1 程序源码:内存映射演示

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

#define MAP_SIZE 4096
#define TEXT "Hello from memory mapping!"

int main() {
    printf("进程PID: %d\n", getpid());
    printf("查看/proc/%d/maps了解虚拟内存布局\n", getpid());

    // 1. 使用mmap创建匿名映射(堆外内存)
    void *anon_mem = mmap(NULL, MAP_SIZE,
                         PROT_READ | PROT_WRITE,
                         MAP_PRIVATE | MAP_ANONYMOUS,
                         -1, 0);

    if (anon_mem == MAP_FAILED) {
        perror("mmap failed");
        exit(1);
    }

    printf("匿名映射地址: %p\n", anon_mem);
    strcpy((char*)anon_mem, TEXT);
    printf("写入内容: %s\n", (char*)anon_mem);

    // 2. 创建文件映射
    int fd = open("testfile.txt", O_RDWR | O_CREAT, 0644);
    if (fd == -1) {
        perror("open failed");
        exit(1);
    }
    // 扩展文件大小
    ftruncate(fd, MAP_SIZE);

    void *file_mem = mmap(NULL, MAP_SIZE,
                         PROT_READ | PROT_WRITE,
                         MAP_SHARED, fd, 0);

    if (file_mem == MAP_FAILED) {
        perror("文件映射失败");
        exit(1);
    }

    printf("文件映射地址: %p\n", file_mem);
    strcpy((char*)file_mem, "This is file mapping test");

    // 3. 演示内存保护
    void *protected_mem = mmap(NULL, MAP_SIZE,
                              PROT_READ,  // 只读权限
                              MAP_PRIVATE | MAP_ANONYMOUS,
                              -1, 0);

    printf("只读内存地址: %p\n", protected_mem);
    // 这里如果写入会触发段错误
    // strcpy((char*)protected_mem, "This will crash");

    // 4. 暂停程序,查看/proc/pid/maps
    printf("\n程序暂停中,请查看/proc/%d/maps...\n", getpid());
    printf("按回车键继续...\n");
    getchar();

    // 清理
    munmap(anon_mem, MAP_SIZE);
    munmap(file_mem, MAP_SIZE);
    munmap(protected_mem, MAP_SIZE);
    close(fd);

    return 0;
}

7.2 编译和运行

# 编译程序
gcc -o mem_demo mem_demo.c

# 运行程序
./mem_demo

# 在另一个终端查看内存布局
cat /proc/<PID>/maps

# 使用pmap查看更详细信息
pmap -X <PID>

八、监控和调试工具

8.1 常用命令行工具

工具 用途 示例
pmap 查看进程内存映射 pmap -X <pid>
procfs 内核提供的进程信息 cat /proc/<pid>/maps
/proc//smaps 详细内存统计 cat /proc/<pid>/smaps
gdb 调试内存访问 gdb -p <pid>
valgrind 内存错误检测 valgrind --tool=memcheck ./prog
strace 跟踪系统调用 strace -e mmap,brk ./prog

8.2 /proc//maps 文件解析

示例输出:

00400000-00401000 r-xp 00000000 08:01 123456    /bin/myapp
00600000-00601000 rw-p 00000000 08:01 123456    /bin/myapp
7f8a40000000-7f8a40200000 rw-p 00000000 00:00 0
7f8a40200000-7f8a40400000 r-xp 00000000 08:01 789012    /lib/libc.so.6
7ffd45ae8000-7ffd45b09000 rw-p 00000000 00:00 0          [stack]

字段解释:

  • 地址范围:虚拟内存起止地址。
  • 权限:r=读, w=写, x=执行, p/s=私有/共享。
  • 文件偏移:映射在文件中的偏移量。
  • 设备:主设备号:次设备号。
  • inode:文件inode号。
  • 路径名:映射的文件或区域类型。

九、高级主题:内存优化技术

9.1 透明大页(Transparent Hugepages)

# 查看THP状态
cat /sys/kernel/mm/transparent_hugepage/enabled

# 控制THP
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

9.2 内存控制组(cgroups)

# 设置内存限制
mkdir /sys/fs/cgroup/memory/mycgroup
echo 100M > /sys/fs/cgroup/memory/mycgroup/memory.limit_in_bytes
echo <pid> > /sys/fs/cgroup/memory/mycgroup/cgroup.procs

十、性能分析和调优

10.1 常见内存问题及诊断

问题现象 可能原因 诊断工具
内存泄漏 分配未释放 valgrind, mtrace
内存碎片 频繁分配释放 /proc/buddyinfo, vmstat
交换抖动 物理内存不足 vmstat 1, sar -B
OOM被杀 内存超限 dmesg, oom_score

10.2 性能监控命令

# 实时监控内存使用
vmstat 1  # 每秒刷新
sar -r 1  # 内存统计
top -o %MEM  # 按内存排序进程

# 详细内存分析
cat /proc/meminfo
cat /proc/vmstat

十一、内核代码深入:缺页处理实现

11.1 缺页中断处理核心代码(简化)

// arch/x86/mm/fault.c 简化版本
static vm_fault_t handle_mm_fault(struct vm_area_struct *vma,
                                 unsigned long address,
                                 unsigned int flags)
{
    // 1. 获取各级页表项
    pgd_t *pgd = vma->vm_mm->pgd;
    p4d_t *p4d;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *pte;

    // 2. 逐级查找页表
    p4d = p4d_alloc(vma->vm_mm, pgd, address);
    pud = pud_alloc(vma->vm_mm, p4d, address);
    pmd = pmd_alloc(vma->vm_mm, pud, address);

    if (!pmd)
        return VM_FAULT_OOM;

    // 3. 处理透明大页
    if (pmd_trans_huge(*pmd) && vma->vm_flags & VM_HUGEPAGE) {
        return do_huge_pmd_wp_page(vma, address, pmd);
    }

    // 4. 普通页处理
    pte = pte_alloc_map(vma->vm_mm, pmd, address);
    if (!pte)
        return VM_FAULT_OOM;

    // 5. 根据PTE状态处理缺页
    if (!pte_present(*pte)) {
        if (pte_none(*pte)) {
            // 全新页面,分配物理内存
            return do_anonymous_page(vma, address, pte, pmd, flags);
        }
        // 页面被换出,需要换入
        return do_swap_page(vma, address, pte, pmd, flags);
    }

    // 6. 写时复制处理
    if (flags & FAULT_FLAG_WRITE) {
        if (!pte_write(*pte))
            return do_wp_page(vma, address, pte, pmd, flags);
    }
    return 0;
}

十二、总结与最佳实践

12.1 核心要点总结

  1. 虚拟化的价值:每个进程拥有独立的、连续的虚拟地址空间,通过页表映射到物理内存,实现隔离、保护与共享。
  2. 关键数据结构mm_struct(进程内存描述符)、vm_area_struct(虚拟内存区域)、四级页表(PGD→PUD→PMD→PTE)。
  3. 核心机制:缺页中断(按需分配)、写时复制(优化fork())、页面置换(管理物理内存)。
  4. 工具链/proc文件系统、pmapgdbvalgrind等提供了强大的观察和调试能力。

12.2 故障排查流程


内存问题排查步骤:
1. 现象观察 → 内存不足?泄漏?碎片?
   ↓
2. 工具确认 → top/pmap/proc分析
   ↓
3. 定位进程 → 哪个进程异常?
   ↓
4. 深入分析 → valgrind/strace/gdb
   ↓
5. 解决方案 → 优化代码/调整参数
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-3 14:19 , Processed in 1.100280 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表