当前位置: 首页 > news >正文

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 结构可能是这样:

struct PCB {pid_t pid;                // 进程 IDpid_t ppid;               // 父进程 IDenum 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 表示的。

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。

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. 保护与隔离 → 每个进程的地址空间互不干扰,访问非法地址会被操作系统拦截。

http://www.dtcms.com/a/328958.html

相关文章:

  • uniapp组件的开关选择器可以这个携带多参数
  • CVPR2025敲门砖丨机器人结合多模态+时空Transformer直冲高分,让你的论文不再灌水
  • docker network 与host的区别
  • Uni-app + Vue3+editor富文本编辑器完整实现指南
  • 学习STM32 脉冲计数实验
  • MySQL相关概念和易错知识点(6)(视图、用户管理)
  • Java 大视界 -- 基于 Java 的大数据可视化在能源互联网全景展示与能源调度决策支持中的应用
  • 深度学习与遥感入门(七)|CNN vs CNN+形态学属性(MP):特征工程到底值不值?
  • 一键自动化:Kickstart无人值守安装指南
  • 【unitrix数间混合计算】2.20 比较计算(cmp.rs)
  • Spring Boot (v3.2.12) + application.yml + jasypt 数据源加密连接设置实例
  • 25个自动化办公脚本合集(覆盖人工智能、数据处理、文档管理、图片处理、文件操作等)
  • 【电气】NPN与PNP
  • [C语言]第二章-从Hello World到头文件
  • 四分位数与箱线图
  • Redis持久化机制详解:RDB与AOF的全面对比与实践指南
  • 动静态库
  • FPGA的PS基础1
  • 【FPGA】初始Verilog HDL
  • c++编程题-笔记
  • kali linux 2025.2安装Matlab的详细教程
  • 通过限制网络访问来降低服务器被攻击风险的方法
  • 服务器如何应对SYN Flood攻击?
  • FluxApi - 使用Spring进行调用Flux接口
  • Gradle(三)创建一个 SpringBoot 项目
  • 深度学习(3):全连接神经网络构建
  • mysql的快照读与当前读的区别
  • 11G RAC数据文件创建到本地如何处理
  • 【C语言强化训练16天】--从基础到进阶的蜕变之旅:Day3
  • 《算法导论》第 22 章 - 基本的图算法