【Linux】虚拟地址空间解析

1. 虚拟地址空间的引入与必要性

1.1 问题现象:相同地址不同值

  • 关键观察 :父子进程打印同一个全局变量 的地址,显示地址相同但值不同
bash 复制代码
父进程: gval=100, &gval=0x60104c
子进程: gval=200, &gval=0x60104c  # 地址相同,值不同!
  • 结论 :C/C++程序中使用的地址不是物理内存地址 ,而是虚拟地址

1.2 基本概念

  1. 虚拟地址空间 :操作系统为每个进程提供的抽象内存视图

  2. 物理内存:实际存在的RAM硬件。

  3. 虚拟地址 :程序代码中使用的地址,通过页表映射到物理地址。

  4. 页表(Page Table):存储虚拟地址到物理地址映射关系的数据结构。

  5. MMU(Memory Management Unit):CPU中的硬件单元,负责地址转换。

1.3 核心原则

  1. 一个进程,一个虚拟地址空间:每个进程拥有自己独立的4GB(32位系统)地址空间。

  2. 一个进程,一套页表:每个进程有自己的页表,实现虚拟到物理地址的映射。

  3. 进程独立性

    • 内核数据结构独立

    • 加载到内存的代码和数据独立

2. 虚拟地址空间的本质与结构

2.1 虚拟地址空间布局(32位Linux)

2.2 虚拟地址空间的本质

  • 不是物理内存 :虚拟地址空间是一套地址编排规则内存管理机制

  • 是操作系统提供的抽象:让每个进程认为自己独占4GB内存。

  • 通过数据结构实现管理:虚拟地址空间本身是一个数据结构对象。

3. 虚拟地址空间的管理:"先描述,再组织"

3.1 地址空间数据结构:mm_struct

c 复制代码
// Linux内核中管理地址空间的核心数据结构(简化)
struct mm_struct {
    // 区域边界定义
    unsigned long start_code, end_code;    // 代码段
    unsigned long start_data, end_data;    // 数据段
    unsigned long start_brk, brk;          // 堆区
    unsigned long start_stack;             // 栈区
    unsigned long arg_start, arg_end;      // 命令行参数
    unsigned long env_start, env_end;      // 环境变量
    
    // 页表相关
    pgd_t *pgd;                           // 页全局目录
    
    // 虚拟内存区域链表
    struct vm_area_struct *mmap;          // 虚拟内存区域链表头
    
    // 统计信息
    unsigned long total_vm;                // 总虚拟内存大小
    unsigned long locked_vm;               // 锁定的内存大小
    // ... 其他字段
};

3.2 区域划分的实现原理

  • 区域定义 :通过起始地址结束地址定义内存区域。

  • 示例:桌子划分区域

c 复制代码
struct Desktop {
    int size;          // 桌子总长度
    int zone1_start;   // 区域1起始
    int zone1_end;     // 区域1结束
    int zone2_start;   // 区域2起始
    int zone2_end;     // 区域2结束
};

struct Desktop area = {100, 0, 49, 50, 99};  // 各占一半

调整区域:只需修改边界值

c 复制代码
area.zone2_start = 20;  // 调整区域2起始位置
area.zone1_end = 20;    // 调整区域1结束位置

3.3 虚拟内存区域:vm_area_struct

  • 更精细的管理 :每个独立的内存区域(代码段、数据段、堆、栈、共享库等)用vm_area_struct描述。

  • 数据结构

c 复制代码
struct vm_area_struct {
    struct mm_struct *vm_mm;      // 所属地址空间
    unsigned long vm_start;       // 区域起始地址(包含)
    unsigned long vm_end;         // 区域结束地址(不包含)
    
    // 权限和标志
    pgprot_t vm_page_prot;        // 访问权限
    unsigned long vm_flags;       // 区域标志(读、写、执行等)
    
    // 链表和树结构
    struct vm_area_struct *vm_next;  // 单链表下一个
    struct vm_area_struct *vm_prev;  // 单链表上一个
    struct rb_node vm_rb;           // 红黑树节点
    
    // 文件映射相关
    struct file *vm_file;          // 映射的文件(如有)
    unsigned long vm_pgoff;        // 文件内的偏移
    // ... 其他字段
};

3.4 完整数据结构关系

复制代码
task_struct
    ├── pid_t pid                     # 进程ID
    ├── struct files_struct *files    # 打开文件表
    └── struct mm_struct *mm          # 指向地址空间描述符
            ├── pgd_t *pgd            # 页表基址
            └── struct vm_area_struct *mmap  # 虚拟内存区域链表头
                    ├── vm_area_struct (代码段)
                    │   ├── vm_start, vm_end
                    │   ├── vm_flags (只读、可执行)
                    │   └── vm_next
                    ├── vm_area_struct (数据段)
                    │   ├── vm_start, vm_end
                    │   ├── vm_flags (读写)
                    │   └── vm_next
                    ├── vm_area_struct (堆区)
                    │   ├── vm_start, vm_end
                    │   ├── vm_flags (读写)
                    │   └── vm_next
                    └── vm_area_struct (栈区)
                        ├── vm_start, vm_end
                        ├── vm_flags (读写)
                        └── vm_next

4. 虚拟地址空间的工作机制

4.1 地址转换流程

复制代码
程序使用虚拟地址 (0x11111111)
        ↓
CPU的MMU单元接收虚拟地址
        ↓
查询进程的页表(通过mm_struct->pgd)
        ↓
找到对应的物理地址 (0x22334455)
        ↓
访问物理内存获取数据

4.2 写时拷贝的底层实现

复制代码
// 父子进程共享数据时的内存管理
// fork() 时:
// 1. 复制父进程的页表给子进程
// 2. 将共享的页面标记为只读

// 当任一进程尝试写入时:
// 1. 触发页错误(Page Fault,访问违例)
// 2. 操作系统检测到是COW页面
// 3. 分配新的物理页面,复制原内容
// 4. 更新该进程的页表项,指向新页面
// 5. 将页面标记为可写
// 6. 恢复进程执行,完成写入操作

5. 虚拟地址空间的好处

5.1 内存保护与安全性

  1. 野指针检测:访问未分配或越界的虚拟地址会触发页错误。

  2. 权限保护:尝试向只读区域(如代码段、字符串常量)写入会触发保护错误。

c 复制代码
char *str = "hello";  // 字符串常量在只读区域
*str = 'H';           // 触发段错误(Segmentation Fault)
  1. 隔离性:进程无法直接访问其他进程的内存。

5.2 内存管理的解耦

  • 进程管理内存管理分离:

    • 进程管理:关注task_struct、调度等

    • 内存管理:关注mm_struct、页表、物理内存分配

  • 灵活的内存分配:可以延迟分配物理内存(按需分页)。

5.3 统一简洁的编程模型

  • 每个进程从相同虚拟地址开始:简化链接器和加载器设计。

  • 连续虚拟地址,不连续物理地址:程序员看到连续内存,实际物理内存可能碎片化。

5.4 支持高级特性

  1. 内存映射文件:将文件映射到虚拟地址空间。

  2. 共享内存:多个进程映射到相同的物理页面。

  3. 交换(Swap):将不常用页面换出到磁盘。

6. 重要概念澄清

6.1 进程创建的步骤

  1. 先创建内核数据结构task_structmm_struct、页表等。

  2. 后加载代码和数据:将程序文件加载到内存,建立映射。

  3. 可以只有结构没有内容 :进程可以只有task_structmm_struct,没有实际代码(如内核线程)。

6.2 进程挂起的理解

  • 挂起状态:进程被换出到磁盘交换区。

  • 内存管理 :进程的代码和数据不在物理内存,但task_structmm_struct、页表仍在内存。

  • 恢复执行:需要时将代码和数据换入内存。

6.3 写时拷贝的好处

  1. 减少创建时间fork()时只复制页表,不复制实际数据,速度快。

  2. 减少内存浪费:共享的页面只有一份物理拷贝,直到需要修改。

  3. 支持进程快速创建:如Web服务器创建子进程处理请求。

7. 虚拟地址空间的扩展理解

7.1 64位系统的地址空间

  • 64位系统理论上支持2642^{64}264字节(16EB)的地址空间。

  • 实际实现中,通常只使用48位或57位地址线。

  • Linux x86_64使用48位虚拟地址,提供256TB的地址空间。

7.2 内存映射的类型

  1. 文件映射:将文件内容映射到虚拟地址空间。

  2. 匿名映射:没有对应文件的映射(如堆、栈、共享内存)。

  3. 私有映射:写时拷贝,修改不影响原文件。

  4. 共享映射:修改对所有映射进程可见。

7.3 缺页中断的类型

概念:缺页中断是程序访问的虚拟页不在物理内存时,由MMU触发、操作系统将对应页面从外存调入内存并更新页表后让程序继续执行的异常处理过程。

  1. 硬缺页:页面不在物理内存,需要从磁盘加载。

  2. 软缺页:页面在物理内存但不在页表中(如共享库)。

  3. 写时拷贝缺页:尝试写入只读的COW页面。

8. 总结

  1. 虚拟地址空间是操作系统为每个进程提供的抽象内存视图,不是物理内存。

  2. 通过页表 实现虚拟地址到物理地址的映射,由MMU硬件加速。

  3. 采用 "先描述,再组织" 思想,用mm_struct管理整个地址空间,用vm_area_struct管理各个内存区域。

  4. 提供内存保护隔离性统一编程模型

  5. 写时拷贝 技术显著提升了fork()性能和内存利用率。

  6. 虚拟地址空间实现了进程管理与内存管理的解耦,支持高级特性如内存映射文件和共享内存。

  7. 理解虚拟地址空间是掌握操作系统内存管理、进程隔离和现代计算机体系结构的基础。

相关推荐
(Charon)1 小时前
【C++】网络缓冲区设计(二):Ring Buffer环形缓冲区、head/tail与跨界读写
开发语言·c++
码匠许师傅1 小时前
【设计模式精讲】24.观察者模式(Observer)
c++·观察者模式·设计模式·uml
imgsq1 小时前
S-57 数据解剖:把一个 ENC 文件拆给你看
c++·数据可视化
可视化运维管理爱好者1 小时前
机房工勘记录器分享
运维·网络·nvisual
i网路游侠1 小时前
免费堡垒机 | 福瑞智能运维审计系统(堡垒机)?
运维·堡垒机·等级保护·运维审计·运维安全审计
天外天-亮1 小时前
docker + window 安装
运维·docker·容器
迷茫、Peanut2 小时前
中断的时钟
c++
User_芊芊君子2 小时前
让异构增量同步飞起来-KFS全链路并行同步方案
运维·自动化·ansible
2402_882893862 小时前
用哈希表封装 unordered_map 和 unordered_set —— 手撕 C++ 哈希容器
c++·哈希桶·unordered_map·unordered_set