Linux系统编程Day13 -- 程序地址空间(进阶)

往期内容回顾

程序地址空间

环境变量(初识)

进程状态的优先级和特性

进程属性和常见进程

进程管理

理解计算机的软硬件管理


前言:程序地址空间回顾

在现代操作系统(尤其是采用虚拟内存的系统)中,程序地址空间 是进程能看到的、可访问的虚拟地址布局,通常包括几个典型区域:

┌───────────────────────────────┐ 高地址
│ 栈区 Stack │ 向下增长
├───────────────────────────────┤
│ 共享库区 │ 动态库、运行时链接
├───────────────────────────────┤
│ 堆区 Heap │ 向上增长,malloc/new 分配
├───────────────────────────────┤
│ 数据段 Data │ 已初始化的全局/静态变量
├───────────────────────────────┤
│ BSS 段 │ 未初始化的全局/静态变量
├───────────────────────────────┤
│ 代码段 Text │ 程序指令(只读)
└───────────────────────────────┘ 低地址

这个地址空间是虚拟的,由操作系统通过 内存管理单元(MMU) 映射到物理内存或磁盘

区域 存放内容 大小确定方式 生命周期 增长方向
代码段 可执行指令 编译/链接阶段固定 全局 固定
数据段 已初始化的全局/静态变量 编译/链接阶段固定 全局 固定
BSS 段 未初始化的全局/静态变量 编译/链接阶段固定(加载时清零) 全局 固定
动态分配的内存 运行时动态扩展,受系统限制 程序员控制释放 向上增长
局部变量、调用记录 线程创建时固定大小(可修改) 自动回收 向下增长

关键点:

  • 栈的大小 是创建线程时一次性分配的,超了就溢出。

  • 堆的大小 运行时动态申请,只要系统有内存就能扩展。

  • 代码段、数据段、BSS 段大小 都在编译/链接时已经确定,不会变。

  • 堆和栈方向相反,是为了让它们从虚拟地址空间两端往中间长,最大化利用内存


1. 地址空间的意义

  • 定义:地址空间就是进程能够访问的所有内存地址的范围,它是逻辑上的概念。

  • 为什么要有

    1. 隔离性 → 每个进程都认为自己独占整个内存,互不干扰,避免一个程序越界破坏另一个程序数据。

    2. 统一性 → 不管真实物理内存大小多少,程序看到的地址从 0x0000... 开始,写代码时不必关心内存的物理分布。

    3. 方便管理 → 操作系统通过地址映射控制进程访问权限、分配内存区域、回收资源。

2. 虚拟内存的作用

虚拟内存是地址空间的具体实现方式,它通过**内存管理单元(MMU)**和页表,把进程的虚拟地址映射到物理内存或磁盘。

  • 主要优势

    1. 内存保护:一个进程访问越界地址时,MMU 能立刻触发异常(段错误),保证安全。

    2. 扩展性:即使物理内存不足,也能用磁盘空间(Swap)虚拟成"更多的内存"。

    3. 共享与私有并存:允许不同进程共享一段物理内存(例如共享库),同时其他区域仍保持独立。

    4. 简化编程:程序员不必关心内存碎片和物理布局,虚拟地址看起来是连续的。

3. 物理内存的角色

  • 物理内存是真正存放数据的硬件资源(RAM 芯片)。

  • 系统通过**分页(Page)分段(Segment)**把虚拟地址映射到物理地址。

  • 物理内存有限,虚拟内存机制让多个进程看起来拥有足够大的可用空间


一、进程管理是如何分配地址空间

操作系统管理进程时,进程控制块(PCB,Linux 里是 task_struct)中会保存 内存管理相关的信息,比如:

  • 页表基地址(虚拟地址到物理地址的映射)

  • 程序代码段位置

  • 堆和栈的起始地址和大小

  • 内存映射区(mmap 动态库、文件映射)

进程调度时,操作系统会切换页表(即虚拟地址映射),从而让 CPU 看到的是该进程的地址空间

也就是说,进程管理和地址空间是绑死在一起的

  • PCB 记录地址空间信息

  • 切换进程 = 切换虚拟地址空间

如果用伪 C 代码表示,简化的 PCB 结构可能是这样:

cpp 复制代码
struct PCB {
    pid_t pid;                // 进程 ID
    pid_t ppid;               // 父进程 ID
    enum state;               // 进程状态
    struct CPU_context ctx;   // CPU寄存器等上下文信息

    struct mm_struct *mm;     // 进程的内存描述符(指向地址空间结构)
    
    struct file *files[MAX_FILES]; // 打开文件表
    struct sched_info sched;       // 调度信息
};

重点 :PCB 本身不直接保存整个地址空间,而是保存一个指针 mm ,指向 内存描述符(Memory Descriptor),这个描述符才负责具体的虚拟地址空间布局。

地址空间是怎么被描述的(mm_struct)

在 Linux 内核中,进程的虚拟内存布局是用 struct mm_struct 表示的。

cpp 复制代码
struct mm_struct {
    unsigned long start_code, end_code; // 代码段范围
    unsigned long start_data, end_data; // 数据段范围
    unsigned long start_brk, brk;       // 堆的起始和当前结尾
    unsigned long start_stack;          // 栈顶地址

    struct vm_area_struct *mmap;        // 链表/红黑树管理的 VMA 区域
};
  • VMA(虚拟内存区域,Virtual Memory Area)

    这是描述进程内连续虚拟地址区间的结构体。

    比如代码段、数据段、堆、栈、mmap 映射区,每一块都是一个 VMA。

cpp 复制代码
struct vm_area_struct {
    unsigned long vm_start;
    unsigned long vm_end;
    unsigned long vm_flags;  // 读写执行权限
    struct vm_area_struct *vm_next; // 链表连接
};

所以从数据结构上看,PCB → mm_struct → vm_area_struct 是一条链路:

PCB
└── mm_struct(描述整个虚拟地址空间)
└── 链表/红黑树(每个节点是一个虚拟内存区域 VMA)


二、 PCB 如何"分配"进程地址空间

PCB 本身不直接"存放"进程的内存,而是通过 内存管理信息 (比如页表地址、段表信息)来指向进程的 虚拟地址空间

参考示意图

(1) 创建进程时(fork())

  • PCB 本身不直接"存放"进程的内存,而是通过 内存管理信息 (比如页表地址、段表信息)来指向进程的 虚拟地址空间

    当新建一个进程时,内核会:

  • 创建 PCB (分配内核内存保存它),分配虚拟地址空间 (或者复制父进程的映射关系),初始化页表 (映射虚拟地址 → 物理地址),设置代码段、数据段、堆、栈的起始地址和大小 ,将这些信息写入 PCB 的内存管理部分。


(2) 父子进程的地址空间分配

在 Linux 中,创建进程通常通过 fork()

  • fork() 之后,父子进程会有各自独立的 PCB

  • 父子进程的 虚拟地址空间布局相同(代码段、数据段、堆、栈内容初始相同)

  • 但是物理内存并不一定复制一份,而是采用 写时复制(Copy-on-Write, COW) 技术

📌 写时复制的过程

  1. fork 之后,父子进程的页表都指向同一块物理内存,并标记为 只读

  2. 当任意一个进程试图修改某一页时,内核才会:

    • 分配新的物理页

    • 复制旧页内容

    • 更新该进程的页表,解除只读标记

    • 这样保证了修改不会影响另一个进程


三、 为什么说是"修改 PCB 分配地址空间"

  • PCB 自身不存放具体的内存,而是通过 mm_struct 维护虚拟地址空间的元信息。

  • 所谓"修改 PCB"就是:

    • 在创建进程、加载新程序或内存分配时,更新 PCB 里的 mm 指针指向的新内存布局

    • 这个布局用 VMA 链表/红黑树来表示。

  • 这样 OS 就能根据这些结构,找到进程每个虚拟地址对应的物理页

[PCB]
├─ PID, 状态, 寄存器...
└─ mm → [mm_struct]
├─ start_code, end_code
├─ start_data, end_data
├─ start_brk, brk (堆)
├─ start_stack (栈)
└─ mmap → [VMA 链表 / 红黑树]
├─ VMA1: 代码段
├─ VMA2: 数据段
├─ VMA3: 堆
├─ VMA4: 栈
└─ ...

  • PCB 里保存了进程的内存映射信息,但本身不存放数据。

  • fork 会复制 PCB 和页表,地址空间初始相同,但物理内存通过 写时复制 节省资源

  • 父子进程不一定共享地址空间,除非用线程或显式共享内存。

  • 共享内存 是多进程通信的重要手段。


总结

进程管理在分配地址空间时,不会一次性分配所有物理内存,而是:

  1. 创建进程 → 操作系统先为其建立虚拟地址空间(代码段、数据段、堆、栈等)。

  2. 建立页表 → 通过页表记录虚拟地址与物理地址的映射关系。

  3. 按需分配 → 当进程访问某个虚拟地址时才分配对应的物理内存(缺页中断机制)。

  4. 保护与隔离 → 每个进程的地址空间互不干扰,访问非法地址会被操作系统拦截。

相关推荐
FirstFrost --sy4 分钟前
C++ stack and queue
开发语言·c++·queue·stack·priority_queue
现实与幻想~14 分钟前
Linux:企业级WEB应用服务器TOMCAT
linux·前端·tomcat
网硕互联的小客服33 分钟前
服务器如何应对SYN Flood攻击?
运维·服务器·网络
FPGA44 分钟前
曼彻斯特编解码:数字世界的“摩斯密码”与FPGA高效实现
数据结构
数据智能老司机1 小时前
图算法趣味学——图遍历
数据结构·算法·云计算
ZERO的秃头之路1 小时前
WMware的安装以及Ubuntu22的安装
linux·服务器·ubuntu
厦门辰迈智慧科技有限公司1 小时前
现代化水库运行管理矩阵建设的要点
运维·网络·物联网·线性代数·安全·矩阵·监测
daiyanyun1 小时前
Ubuntu 20.04 虚拟机安装完整教程:从 VMware 到 VMware Tools
linux·c语言·c++·ubuntu
苦逼IT运维1 小时前
Jenkins + SonarQube 从原理到实战三:SonarQube 打通 Windows AD(LDAP)认证与踩坑记录
运维·服务器·windows·docker·云计算·jenkins·devops
GalaxyPokemon1 小时前
Linux的pthread怎么实现的?(包括到汇编层的实现)
运维·开发语言·c++