当前位置: 首页 > 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.lryc.cn/news/619483.html

相关文章:

  • 边缘节点 DDoS 防护:CDN 节点的流量清洗与就近拦截方案
  • IPA1299至为芯替代TI ADS1299的脑机接口芯片
  • 机器翻译:学习率调度详解
  • 云蝠智能 VoiceAgent 在不良资产处理中的技术应用与实践
  • 2020/12 JLPT听力原文 问题一 5番
  • 磁悬浮轴承转子动平衡:零接触旋转下的“隐形杀手”深度解析与精准猎杀指南
  • Video_AVI_Packet(1)
  • 部署 Docker 应用详解(MySQL + Tomcat + Nginx + Redis)
  • 1688商品数据抓取:Python爬虫+动态页面解析
  • Visual Studio Code 跨平台快捷键指南:Windows 与 macOS 全面对比
  • VS2022+QT5.15.2+OCCT7.9.1的开发环境搭建流程
  • vscode远程服务器出现一直卡在正在打开远程和连接超时解决办法
  • LaTeX(排版系统)Texlive(环境)Vscode(编辑器)环境配置与安装
  • IV模型(工具变量模型)
  • windows10装Ubuntu22.04系统(双系统)
  • C++中的`if`语句多操作条件执行及顺序保证技术指南
  • 《汇编语言:基于X86处理器》第13章 复习题和编程练习
  • 当GitHub宕机时,我们如何保持高效协作?分布式策略与应急方案详解
  • 内存可见性和伪共享问题
  • 元数据与反射:揭开程序的“自我认知”能力
  • 5.语句几个分类
  • AXIOS 入门
  • 6 ABP 框架中的事件总线与分布式事件
  • 超越相似名称:Elasticsearch semantic text 如何在简洁、高效、集成方面超越 OpenSearch semantic 字段
  • 深度学习-卷积神经网络-GoogLeNet
  • Perl——qw()函数
  • 【类与对象(下)】探秘C++构造函数初始化列表
  • [idekCTF 2025] diamond ticket
  • AAAI论文速递 | NEST:超图小世界网络让自动驾驶轨迹预测更精准
  • Java面试宝典:G1垃圾收集器下