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

进程地址空间:操作系统中的虚拟世界与心灵映射,深入解析进程地址空间

文章目录

  • 引言
  • 📘正文
    • 📖问题引入
    • 📖虚拟空间划分
    • 📖真实空间分布
    • 🖋️代码实现
    • 🖋️问题反思
  • 📖进程地址空间
    • 🖋️虚拟地址
    • 🖋️页表+MMU
    • 🖋️写时拷贝
  • 📕各部分结构的详细说明
    • 🖊地址空间布局图
    • 🖊代码段
    • 🖊数据段
    • 🖊 BSS段
    • 🖊堆(Heap)
    • 🖊 栈(Stack)
    • 🖊C代码示例:进程地址空间
  • 结语:进程地址空间的哲学意义

在这里插入图片描述

引言

在操作系统的浩瀚宇宙中,进程地址空间犹如一片神秘的荒原,深藏着无数的秘密与可能性。它是每一个程序运行的根基,是虚拟与现实交汇的地方,是计算机内存的魔法世界。它存在于操作系统的心脏之中,伴随着进程的诞生与死灭,演绎着一场关于资源管理与控制的永恒故事。

今天,我们将一起穿越这片无形的疆域,去探讨进程地址空间的奥秘,去感受它在操作系统中的重要地位与深刻影响。

对于 C/C++ 来说,程序中的内存包括这几部分:栈区堆区静态区 等,其中各个部分功能都不相同,比如函数的栈帧位于 栈区,动态申请的空间位于 堆区,全局变量和常量位于 静态区区域划分的意义是为了更好的使用和管理空间
他们的结构排布如图所示。那么真实物理空间也是如此划分吗?多进程运行 时,又是如何区分空间的呢?写时拷贝 机制原理是什么?本文将对这些问题进行解答.

内存条:真实的物理空间,用来存储各种数据

在这里插入图片描述

📘正文

📖问题引入

地址是唯一的,对地址进程编号的目的是为了不冲突

这是个耳熟能详的概念,在 C语言 学习阶段,我们可以通过对变量 & 取地址的方式,查看当前变量存储空间的首地址信息

#include <stdio.h>

int main()
{
  const char* ps = "这是一个常量字符串";
  printf("字符串地址:%p\n", ps);  //%p 专门用来打印地址信息
  return 0;
}


linux下的执行结果
在这里插入图片描述
利用前面学习的 fork 函数创建子进程,使得子进程和父进程共同使用一个变量
代码示例如下:

  1#include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4 #include<sys/wait.h>                                                                                                                                                                                          
  5 #include<stdlib.h>
  6 int main()
  7 {
  8     int val=10;
  9     pid_t id=fork();
 10     if(id==0)//子进程
 11     {
 12         val*=2;
 13         printf("我是一个子进程,pid: %d ppid : %d 共享值: %d 共享值地址: %p\n",getpid(),getppid(),val,&val);
 14         exit(0);
 15     }
 16     waitpid(id,0,0);
 17     printf("我是父进程,pid: %d ppid: %d 共享值: %d 共享值地址: %p\n",getpid(),getppid(),val,&val);
 18     return 0;
 19 }

在这里插入图片描述

可以看到,针对同一个地址相同的值val,二者打印出来的共享值却不同,难度一个数可以同时拥有两个不同的值吗,这显然是不可能的。

这与之前提到的fork有两个返回值原理类型,都利用了写时拷贝

因为真实地址都是 唯一 的,分析:

  • 不同的空间出现同名的情况
  • 父子进程使用的真实物理空间并非同一块空间!

原因

  • 当子进程尝试修改共享值时,发生 写时拷贝 机制
  • 语言层面的程序空间地址不是真实物理地址
  • 一般将此地址称为虚拟地址线性地址

结论: 语言层面的地址都是虚拟地址,用户无法看到真实的物理地址,由 OS 统一管理

📖虚拟空间划分

一般用户的认知中,C/C++ 程序内存分布如下图所示,直接表示内存中的各个部分

在这里插入图片描述

📖真实空间分布

但实际上的空间分布是这样的:
在这里插入图片描述
如果有多个进程(真实地址空间只有一份),此时情况是这样的:
在这里插入图片描述

🖋️代码实现

在实现虚拟地址空间时,是用结构体mm_struct实现的

task_struct一样,mm_struct 中也包含了很多成员,比如不同区域的边界值

//简单展示其中的成员信息
mm_struct
{
	//代码区域划分
	unsigned long code_start;
	unsigned long code_end;

	//堆区域划分
	unsigned long heap_start;
	unsigned long heap_end;

	//栈区域划分
	unsigned long stack_start;
	unsigned long stack_end;

	//还有很多其他信息
	……
}

因此其程序地址空间的管理,也就是区域划分,只需要修改不同区域的startend即可.

每个进程都会有这样一个 mm_struct,其中的区域划分就是虚拟地址空间

  • 通过对边界值的调整,可以做到不同区域的增长,如堆区、栈区扩大

  • mm_struct 中的信息配合 页表+MMU 在对应的真实空间中使内存(程序寻址)

🖋️问题反思

此时可以理解为什么会发生同一块空间能读取到不同值的现象了

父子进程有着各自的 mm_struct,其成员起始值一致

  • 对于同一个变量,如果未改写,则两者的虚拟地址通过 页表 + MMU 转换后指向同一块空间
  • 发生改写行为,此时会在真实空间中再开辟一块空间,拷贝变量值,让其中一个进程的虚拟地址空间映射改变,这种行为称为 写时拷贝

刚开始,父子进程共同使用同一块空间
在这里插入图片描述

在发生改写行为后,子进程在真实空间内重新开辟一块空间,拷贝变量值改下,虽然此时的虚拟地址仍为初始值,蛋映射关系已经发生变化。
在这里插入图片描述

📖进程地址空间

下面来好好谈谈 进程地址空间 (虚拟地址)

🖋️虚拟地址

在早期程序中,是没有虚拟地址空间的,对于数据的写入和读取,是直接在物理地址上进行的,程序与物理空间直接打交道,存在以下问题:

  • 假设存在野指针问题,此时可能直接对物理内存造成越界读写
  • 程序运行时,每次都需要大小为 4GB 的内存使用,当进程过多时,资源分配就会很紧张,引起进程阻塞,导致执行效率下降
  • 动态申请内存后,需要依次释放,影响整体效率

在这里插入图片描述
为了解决各种问题,大佬们提出了 虚拟地址空间 这个概念,有了 虚拟空间 后,当进程创建时,系统会为其分配属于自己的 虚拟空间,需要使用内存时,通过 寻址 的方式,使用物理地址上的空间即可

  • 多个进程互不影响,动态使用,做到 效率、资源 双赢
  • 发生越界行为时,寻址 机制会检测出是否发生越界行为,如果发生了,能在其对物理地址造成影响前进行拦截
  • 因为每个进程都有属于自己的空间,OS 在管理进程时,能够以统一的视角进行管理,效率很高

光有 虚拟地址空间 是不够的,还需要一套完整的 ‘‘翻译’’ 机制进行程序寻址,如 Linux 中的 页表 + MMU

🖋️页表+MMU

页表本质上是一张表,一侧存储虚拟地址,另一侧存储所映射的真实物理地址。

操作系统 会为每个 进程 分配一个 页表,该 页表 使用 物理地址 存储。当 进程 使用类似 malloc 等需要 映射代码或数据 的操作时,操作系统 会在随后马上 修改页表 以加入新的 物理内存。当 进程 完成退出时,内核会将相关的页表项删除掉,以便分配给新的 进程

系统底层机制的研究是非常生涩的,这里简言之就是 页表 记录信息,通过 MMU 机制进行寻址使用内存.

假设目标空间为只读区域(比如数据段、代码段),在进行空间开辟时,会打上只读权限标签。后续对这块进行写入操作时,会直接拒绝

🖋️写时拷贝

思考:为什么要采取写时拷贝映射开辟的方式进行存储呢?每一个数据直接存储不可行吗?

写时拷贝是一种为了优化空间和时间应运而生的操作,带有一定赌博成分。

  • 操作系统认为你对于数据的修改与存储操作相比,是较为低频的,因此采取偷懒的方式进行映射存储,多个进程中的共享数据均指向同一块存储空间,用来优化存储冗余多次构造的情况
  • 这一点在自定义类型时较为明显,内置类型的优化效率并不算高。

可以通过一个简单的例子来证明此现象

//计算 string 类的大小
#include <iostream>
#include <string>
using namespace std;


int main()
{
	string s;
	cout << sizeof(s) << endl;
	return 0;
}


VS2022中
在这里插入图片描述
linux环境中
在这里插入图片描述
分析:

在代码中,string
s;定义了一个C++的字符串对象。虽然你没有给字符串分配任何内容,但是字节大小却不为0。这是因为不同的编译器和标准库实现可能会有不同的内存布局,通常会包括指向动态内存的指针、当前大小、容量和一些额外的管理信息。

📕各部分结构的详细说明

操作系统将进程地址空间划分为多个区域,每个区域用于存储特定类型的数据。以下是典型的地址空间布局:

在这里插入图片描述
在这里插入图片描述

🖊地址空间布局图

以32位操作系统为例,地址空间布局如下:

+---------------------------+  0xFFFFFFFF
| 内核空间                  |  
+---------------------------+  0xC0000000
| 用户栈                    |
+---------------------------+
| 动态分配的堆(Heap)      |
+---------------------------+
| BSS段                    |
+---------------------------+
| 数据段                    |
+---------------------------+
| 代码段                    |
+---------------------------+  0x00000000

🖊代码段

  • 存储内容:存放程序的可执行代码。
  • 访问权限:只读,防止程序意外修改指令。
  • 特点:多个进程可以共享同一段代码段(如共享库)

🖊数据段

  • 存储内容:存储已初始化全局变量静态变量
  • 访问权限:读写权限。
  • 特点:程序运行时大小固定。

🖊 BSS段

  • 存储内容:存储未初始化全局变量静态变量
  • 特点:初始值默认为0,占用物理内存时才分配。

🖊堆(Heap)

  • 存储内容:动态分配的内存(如malloc、new分配的内存)。
  • 特点:向高地址增长;由程序员手动分配和释放。

🖊 栈(Stack)

  • 存储内容:局部变量函数调用参数返回地址等。
  • 特点:向低地址增长;由操作系统自动管理,超出范围会触发栈溢出。

上面的几种是主要的几种,还有几个小的内存区,比如字符段常量区字符常量区的内容不能修改,只有读权限

🖊C代码示例:进程地址空间

以下代码展示了不同段的地址空间位置。

#include <stdio.h>
#include <stdlib.h>
 
int global_var = 10;  // 全局变量(数据段)
int uninit_var;       // 未初始化变量(BSS段)
 
void print_addresses() {
    int local_var = 20;  // 局部变量(栈)
    void *heap_var = malloc(10);  // 动态内存(堆)
 
    printf("代码段地址: %p\n", (void*)print_addresses);
    printf("全局变量地址: %p\n", (void*)&global_var);
    printf("未初始化全局变量地址: %p\n", (void*)&uninit_var);
    printf("局部变量地址: %p\n", (void*)&local_var);
    printf("堆变量地址: %p\n", heap_var);
 
    free(heap_var);
}
 
int main() {
    print_addresses();
    return 0;
}

输出示例:

代码段地址: 0x401000
全局变量地址: 0x601020
未初始化全局变量地址: 0x601030
局部变量地址: 0x7ffd25d3f8bc
堆变量地址: 0x55d3ecf1b260

在这里插入图片描述

结语:进程地址空间的哲学意义

进程地址空间的概念,超越了技术的范畴,它涉及到操作系统如何管理和控制内存,如何为每个进程提供一个独立而安全的运行环境。在这个过程中,虚拟内存技术则如同魔法师一样,巧妙地将虚拟世界与现实世界结合在一起,为开发者创造出一个更加自由和高效的编程环境。

在这个隐形的、却至关重要的空间中,进程的每一次启动、每一次执行、每一次销毁,都在演绎着一场关于资源管理、内存控制与程序执行的深刻哲学思考。正如人生的每一个阶段,进程地址空间也在默默地展示着操作系统的智慧与魅力,成为计算机科学中不可或缺的一部分。

本篇关于进程地址空间的介绍就暂告段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持斧正!!!

在这里插入图片描述

相关文章:

  • 【Axure元件分享】年份范围选择器
  • 批量删除 txt/html/json/xml/csv 等文本文件空白行
  • Spring MVC 页面跳转方案与区别
  • 第十四届蓝桥杯大赛软件赛省赛Python 大学 C 组:6.棋盘
  • 基于 Fluent-Bit 和 Fluentd 的分布式日志采集与处理方案
  • 【零基础入门unity游戏开发——2D篇】SpriteMask精灵遮罩组件
  • 【蓝桥杯】单片机设计与开发,温度传感器DS18B20
  • TPS入门DAY01 服务器篇
  • US112S-ASEMI智能家居专用US112S
  • 深入理解 IntersectionObserver:让前端滚动监听更高效
  • [AI] 如何将 ComfyUI 的作图能力融合到 OpenWebUI
  • Scala:大数据时代的多面手
  • stm32面试
  • Go+Gin实现安全多文件上传:带MD5校验的完整解决方案
  • 使用Java爬虫按关键字搜索淘宝商品?
  • 用matlab探索卷积神经网络(Convolutional Neural Networks)-3
  • 2025年- G33-Lc107-150. 评估逆波兰表示法--java版
  • 电脑办公之文件(夹)操作
  • CentOS-查询实时报错日志-查询前1天业务报错gz压缩日志
  • 当AI开始“思考“:揭秘大语言模型的文字认知三部曲题
  • 2025上海十大动漫IP评选活动启动
  • 夜读丨喜马拉雅山的背夫
  • 欧洲理事会前主席米歇尔受聘中欧国际工商学院特聘教授,上海市市长龚正会见
  • 康子兴评《文明的追求》|野人脚印:鲁滨逊的恐惧与文明焦虑
  • 西南大学教授、重庆健美运动奠基人之一李启圣逝世
  • 成都公积金新政征求意见:购买保障性住房最高贷款额度上浮50%