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

Linux系统编程Day13 -- 程序地址空间

 往期内容回顾

        环境变量(初识)

        进程状态的优先级和特性

        进程属性和常见进程

        进程管理

        理解计算机的软硬件管理

前言

在 C/C++ 程序的学习和调试过程中,你可能会遇到这样的问题:

  • 为什么局部变量会“莫名其妙”被修改?

  • 为什么访问一个随机指针会导致段错误(Segmentation Fault)?

  • 为什么同样的程序运行在不同机器上,地址值可能不同?

这些现象都与 程序的地址空间(Program Address Space) 密切相关。

一、程序地址空间是什么?

        所谓“程序地址空间”,指的是。理解它能帮助我们更好地管理内存、优化性能、以及避免 Bug。

┌───────────────────────────┐ 高地址
│        栈 Stack            │  (局部变量、函数参数、返回地址等,向下增长)
├───────────────────────────┤
│        空闲区              │  (堆和栈之间的空闲区域)
├───────────────────────────┤
│        堆 Heap             │  (malloc/new 分配的内存,向上增长)
├───────────────────────────┤
│   BSS 段 (.bss)            │  (未初始化的全局变量、静态变量)
├───────────────────────────┤
│   数据段 (.data)           │  (已初始化的全局变量、静态变量)
├───────────────────────────┤
│   代码段 (.text)           │  (程序机器指令,常量字符串等)
└───────────────────────────┘ 低地址


C / C++ 中的典型程序地址示意图

高地址

│   栈 Stack
│   ├── main 函数的局部变量
│   ├── 函数调用保存的返回地址
│   ├── 临时变量(C++ 里构造的临时对象)
│   └── 函数参数

│  
(栈向低地址方向增长)

│   共享库映射区(动态链接库)

│   空闲区(堆和栈之间的未使用空间)

│   堆 Heap
│   ├── C: malloc / free
│   ├── C++: new / delete
│   └── STL 容器动态分配的内存
│  
(堆向高地址方向增长)

│   BSS 段 (.bss)
│   ├── 未初始化的全局变量: int g1;
│   └── static int s1;

│   数据段 (.data)
│   ├── 已初始化的全局变量: int g2 = 10;
│   └── static int s2 = 20;

│   代码段 (.text)
│   ├── 程序的机器指令
│   ├── 常量字符串("hello")
│   └── C++: 虚函数表、类型信息

低地址


 为什么栈的内存向下、堆向上?

早期的操作系统设计者希望:

  • 堆和栈从两端向中间增长,这样能最大限度利用进程的虚拟地址空间。

  • 如果栈和堆都从同一方向增长,可能很快就会互相撞到,而另一半地址空间浪费掉。

  • 安全性:不同增长方向可在中间加保护页,防止越界直接破坏另一块数据

二、程序地址空间与进程的关系

        这里我们有一段c语言的代码

   
#include <complex.h>
#include <stdatomic.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int globalvalue = 100;int main(){pid_t id = fork();int cnt = 0;if(id < 0){printf("fork error!/n");}else if (id == 0) {while (1) {printf("Child process: pid = %d, ppid = %d, global value = %d, &globalvalue = %p\n",getpid(),getppid(),globalvalue,&globalvalue);sleep(1);cnt++;if(cnt == 10){globalvalue = 300;printf("The Child process has change the globalvalue!\n");}}}else {while(1){printf("Father process: pid = %d,ppid = %d, global value = %d, &globalvalue = %p\n",getpid(),getppid(),globalvalue,&globalvalue);sleep(2);}}
}

这里我们创建了一个全局变量globalvalue,并且创建了父子进程,并且在子进程中修改了全局变量的值,然后分别在父子进程中打印输出:

运行程序:

我们查看进程和输出描述如下:

Child process: pid = 77763, ppid = 77762, global value = 100, &globalvalue = 0x104448000

Father process: pid = 77762,ppid = 25958, global value = 100, &globalvalue = 0x104448000

Child process: pid = 77763, ppid = 77762, global value = 100, &globalvalue = 0x104448000

Child process: pid = 77763, ppid = 77762, global value = 100, &globalvalue = 0x104448000

Father process: pid = 77762,ppid = 25958, global value = 100, &globalvalue = 0x104448000

Child process: pid = 77763, ppid = 77762, global value = 100, &globalvalue = 0x104448000

The Child process has change the globalvalue!

Child process: pid = 77763, ppid = 77762, global value = 300, &globalvalue = 0x104448000

Father process: pid = 77762,ppid = 25958, global value = 100, &globalvalue = 0x104448000

Child process: pid = 77763, ppid = 77762, global value = 300, &globalvalue = 0x104448000

Child process: pid = 77763, ppid = 77762, global value = 300, &globalvalue = 0x104448000

Father process: pid = 77762,ppid = 25958, global value = 100, &globalvalue = 0x104448000

Child process: pid = 77763, ppid = 77762, global value = 300, &globalvalue = 0x104448000


你会惊讶地发现,当子进程修改全局变量时,父进程的全局变量没有发生变化,但是它们全局变量的地址竟然是相同的!!

原因解释:

        我们c语言/c++的程序地址并不是真实的地址-->虚拟地址

1. fork() 做了什么

操作系统会复制当前进程的整个地址空间,包括:

  • 代码段(指令部分)

  • 数据段(全局变量、静态变量)

  • 堆(动态分配)

  • 栈(局部变量、函数调用栈)


复制后的父子进程:

  • 拥有完全相同的数据副本(初始值相同)

  • 虚拟地址相同(所以 &globalvalue 打印出来一样)

  • 物理内存不同(写入时各用自己的副本)

2. 为什么地址一样但值不一样

这里的“地址”是虚拟地址,进程之间的虚拟地址可以相同,因为:

  • 每个进程都有自己的虚拟地址空间

  • 虚拟地址通过页表映射到物理内存。

  • 父子进程的虚拟地址相同,但映射到的物理页不同(在写时可能会分离)。


3. 为什么父进程没有看到子进程的修改

这是因为 Linux 在 fork() 后使用了 写时复制(Copy-On-Write,COW):

  • 在 fork() 之后,父子进程会共享同一份物理内存,直到有一方写入这块内存。

  • 当子进程 globalvalue = 300; 时,内核会:

    1. 分配一个新的物理页给子进程。

    2. 把原来的数据(值 100)复制过去。

    3. 子进程在新物理页上写入 300。

  • 父进程依旧指向原来的物理页(值仍是 100)。

所以最终:

  • 子进程:globalvalue = 300(新物理页)

  • 父进程:globalvalue = 100(旧物理页)


4. 简单说明

fork 前:
父进程:
  globalvalue(100)  --->  [物理页 P]

fork 后(初始):
父进程:globalvalue(100) --> [物理页 P] (共享, 只读)
子进程:globalvalue(100) --> [物理页 P] (共享, 只读)

子进程写 globalvalue = 300 时:
父进程:globalvalue(100) --> [物理页 P]
子进程:globalvalue(300) --> [物理页 Q] (COW 分配的新页)


三、程序地址空间和进程管理的联系

1. 每个进程拥有独立的地址空间

  • 操作系统为每个进程分配一个独立的虚拟地址空间,确保进程间的内存访问隔离。

  • 进程只能访问自己的地址空间,不能直接访问其他进程的内存,保障系统安全与稳定。

  • 这也是操作系统实现多任务的基础。

2. 虚拟内存机制是进程管理的核心

  • 操作系统利用虚拟内存技术,实现程序地址空间到物理内存的映射。

  • 每个进程看到的地址空间是虚拟的,物理内存由操作系统动态分配和管理。

  • 通过页表、TLB(快表)等机制快速转换虚拟地址到物理地址。

3. 进程的创建和地址空间复制

  • 使用 fork() 创建进程时,操作系统会复制父进程的地址空间(写时复制 Copy-On-Write)。

  • 这保证了父子进程拥有独立的数据副本,避免相互干扰。

  • 但因为是虚拟地址空间复制,父子进程看到相同的虚拟地址(例如变量的地址),但物理内存可能不同。

4. 进程切换时切换地址空间

  • CPU 调度时切换进程,需要切换页表,切换虚拟地址空间映射。

  • 这确保切换到的进程访问的地址空间是正确的独立空间。

  • 操作系统通过上下文切换保存和恢复地址空间映射状态。

5. 内存保护和进程隔离

  • 程序地址空间利用硬件保护机制,防止进程访问未授权的内存区域。

  • 如果进程访问越界,操作系统会产生异常(如段错误 SIGSEGV),保护系统安全。

6. 共享内存和映射文件

  • 虽然进程地址空间相互独立,但操作系统允许通过特殊手段(共享内存、映射文件)实现进程间通信。

  • 这些共享区在不同进程的地址空间中映射到相同的物理内存,实现数据共享。


四、总结

程序地址空间

进程管理

联系

是每个进程独立的虚拟内存区域

负责进程的创建、调度、资源管理和销毁

操作系统通过管理程序地址空间实现进程的内存隔离与保护

包含代码段、数据段、堆、栈等

负责进程的内存分配与回收

进程创建时复制地址空间,调度时切换地址空间

利用虚拟内存技术映射到物理内存

通过页表、TLB等实现快速地址转换

地址空间映射是操作系统实现多进程并发运行的基础

进程间地址空间相互独立,互不干扰

进程切换时切换地址空间上下文

保障进程间安全与稳定

支持共享内存映射文件实现进程间通信

进程间通信的实现途径之一

共享内存作为特殊区域映射进多个地址空间,促进数据交换

http://www.lryc.cn/news/619062.html

相关文章:

  • Seata深度剖析:微服务分布式事务解决方案
  • 微服务ETCD服务注册和发现
  • 力扣经典算法篇-50-单词规律(双哈希结构+正反向求解)
  • SQL 合并两个时间段的销售数据:FULL OUTER JOIN + COALESCE
  • 杰里平台7083G 如何支持4M flash
  • 【K8s】K8s控制器——复制集和deployment
  • 【SpringBoot】08 容器功能 - SpringBoot底层注解汇总大全
  • 4.运算符
  • [激光原理与应用-254]:理论 - 几何光学 - 自动对焦在激光器中的应用?
  • vivo Pulsar 万亿级消息处理实践(2)-从0到1建设 Pulsar 指标监控链路
  • 【微服务过度拆分的问题】
  • web服务器tomcat内部工作原理以及样例代码
  • Airtable 入门指南:从创建项目到基础数据分析与可视化
  • C++中类之间的关系详解
  • LangChain 入门学习
  • 【限时分享:Hadoop+Spark+Vue技术栈电信客服数据分析系统完整实现方案
  • Docker概述与安装Dockerfile文件
  • Docker使用----(安装_Windows版)
  • 第二章:核心数据结构与面向对象思想之接口的奥秘
  • 3 Abp 核心框架(Core Framework)
  • Milvus 结合极客天成 NVFile 与 NVMatrix 实现高性能向量存储
  • LDAP 登录配置参数填写指南
  • 【VB.NET快乐数】2022-10-17
  • (树形 dp、数学)AT_dp_v Subtree 题解
  • 5年保留期+4次补考机会,灵活通关的申研机制
  • 【CV 目标检测】②——NMS(非极大值抑制)
  • git+lfs 如何安装
  • 股票智能体系统的设计与开发
  • Vue3 组合式API vs 选项式API:深度对比与最佳实践
  • SQL连接操作全解析:从入门到精通