当前位置: 首页 > 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.dtcms.com/a/328496.html

相关文章:

  • Vue3 整合高德地图完成搜索、定位、选址功能,已封装为组件开箱即用(最新)
  • 前端对接豆包AI(vue3+TS版本)
  • 力扣-739.每日温度
  • Leetcode-138. 复制带随机指针的链表
  • AI智能体的“四大支柱”:CAP框架核心层、执行层、约束层、操作层详解​
  • 手机蓝牙无感开锁在智能柜锁与智能箱包中的整体解决方案
  • Iptables 详细使用指南
  • 10-docker基于dockerfile自动制作镜像
  • 计算机网络摘星题库800题笔记 第5章 传输层
  • Ansible 详细笔记
  • _init__.py的作用
  • 电路板的GND与外壳地EARTH通过电容电阻相连
  • 操作系统1.6:虚拟机
  • 图形设计器-Qt Designer (一)包含 LinuxCNC 小部件
  • 基于LLVM的memcpy静态分析工具:设计思路与原理解析(C/C++代码实现)
  • 浏览器面试题及详细答案 88道(12-22)
  • word——选项自动对齐(针对试卷中选项对齐)
  • 2025牛客暑期多校训练营3(FDJAEHB)
  • SuperMap GIS基础产品FAQ集锦(20250811)
  • 多级库存预警:浪智WMS智慧化系统的实时监控体系
  • 启保停-----------单相照明灯的接法
  • LaTex论文审稿修改
  • Day 10-2: Mini-GPT完整手写实战 - 从组件组装到文本生成的端到端实现
  • Jmeter性能测试过程中遇到connection reset的解决方案
  • 深入解析 React 中的 useRef Hook
  • 【c++】反向赋值:颠覆传统的数据交互范式
  • day49 力扣42. 接雨水 力扣84.柱状图中最大的矩形
  • 《疯狂Java讲义(第3版)》学习笔记ch1
  • 【C#补全计划】StringBuilder
  • dify是什么?