欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 高考 > 进程地址空间

进程地址空间

2025/2/26 14:52:19 来源:https://blog.csdn.net/addicted_to/article/details/141728476  浏览:    关键词:进程地址空间

文章目录

  • 一、地址空间了解
    • 地址空间图
    • 虚拟地址
  • 二、进程、地址空间、物理内存之间的关系
    • 进程地址空间和物理内存
    • 地址空间的本质
    • 地址空间的区域划分
  • 三、页表
  • 四、为什么要有进程地址空间?

一、地址空间了解

地址空间图

很早我们就了解到空间分布的图是这样的:
在这里插入图片描述

  • 首先我们写段代码来看一下各部分的地址,检验一下上图所显示的地址趋势是否是正确的:
#include<stdio.h>int g_unval ;           //未初始化数据
int g_val = 20;        //初始化数据
static int sval = 50;int main()
{int a = 0;      //栈区数据printf("代码区地址:%p\n",main);printf("初始化数据区地址:%p\n",&g_val);printf("未初始化数据区地址:%p\n",&g_unval);printf("静态数据区地址:%p\n",&sval);char* heap = (char*)malloc(20);printf("堆区地址:,%p\n",heap);printf("栈区地址:,%p\n",&heap);return 0;
}

运行一下,看看结果:
在这里插入图片描述
我们可以看到,各部分地址确实是按上图的趋势存在的。

注意: heap 里存的是 malloc 出来的空间的地址(堆),而 heap 本身是在栈中存放着的。
在这里插入图片描述

  • 再来看一下命令行参数和环境变量:
int main(int argc ,char* argv[], char* env[])
{for(int i =0; i< argc;i++){printf("argv[%d]:%p\n",i,argv + i);//打印命令行各参数地址}printf("\n");for(int i = 0; env[i];i++){printf("env[%d]:%p\n",i,env + i);//打印各环境变量的地址}

结果如下:
在这里插入图片描述
环境变量和命令行参数的地址相近,可以猜测大概是位于同一区域的。

虚拟地址

首先我们来看这样一份代码,父子进程共享数据,但子进程要对某个数据进行改动,我们来看一看这个数据的地址会不会变化:

 int g_val = 20;       int main(int argc ,char* argv[], char* env[])
{pid_t id = fork();if(id == 0) //子进程{int count = 0;while(1){printf("pid of child:%d, g_val:%d,&g_val:%p\n",getpid(),g_val,&g_val);sleep(2);if(count++ == 5){g_val = 100;printf("child change g_val!!!\n");}}}else  //父进程{while(1){printf("pid of father:%d, g_val:%d,&g_val:%p\n",getpid(),g_val,&g_val);sleep(2);}}return 0;
}

执行结果:

在这里插入图片描述

  • 我们可以看到在子进程更改 g_val 变量的值之前,父子进程共享数据,是一个 g_val = 20,地址也是一样的。但当子进程将该变量的值改变之后,可以看到一个 g_val20,一个是 100,也是合理的,但是两个的地址竟然也是一样的,同一个地址空间放的数据又是 20 ,又是 100 的,这显然是不可能的。所以,这里打印出来的地址绝对不是物理地址,该地址我们称为虚拟地址/线性地址。
  • 因为我们就是使用的 & 正常取的地址,所以表明我们语言用到的地址,全都不是物理地址,而是虚拟地址。
    所以下图表示的不是物理内存,而是进程地址空间。
    在这里插入图片描述
    该图属于操作系统,而不属于语言。不管什么语言写的程序,要运行都要变为进程,拥有进程地址空间。

二、进程、地址空间、物理内存之间的关系

每一个进程都有一个自己的进程地址空间,因为进程信息由 PCB 管理,所以进程地址空间的信息也在 PCB 中。

进程地址空间和物理内存

我们可以接着我们上面所看到的不同的两个 g_val 值对应的一个地址,来看看到底内部是怎样的 ?

  1. 物理内存和虚拟地址之间有映射关系:
    在这里插入图片描述首先,g_val 属于初始化数据,会在地址空间的初始化数据区有地址,这是一个虚拟地址,既然是虚拟的那就不是实际空间,无法存放数据,数据是存放在内存中的(程序被加载到了内存中),既然这样,那虚拟地址和物理内存的地址就存在对应关系(映射),一张表中存放的就是两个地址间的映射关系,这张表叫页表。

  2. 当创建一个子进程时,子进程继承父进程,两个进程指向同一块内存:
    在这里插入图片描述

父进程创建子进程,子进程会继承父进程的大部分数据,包括 g_val 的虚拟地址、映射关系,所以呈现出来的就是父子进程的 g_val 的虚拟地址一样(所以打印出来的地址是相同的),同时两个变量也会映射到同一块实际存放 g_val 数据的物理内存空间。

  1. 子进程修改数据,发生写时拷贝:
    在这里插入图片描述
    当子进程尝试修改变量时,会影响到父进程的数据,而进程之间是相互独立的,不能相互影响。所以,在物理内存上重新开辟一块空间,将父进程的数据拷贝一份下来,让子进程使用新开空间的数据。
    这样父子进程是可以保持独立的,内核数据结构是父子都各有一份的,代码是只读的,所以两者共用一份没问题,而数据也是各有一份的,所以可以保证独立性。

总结:
从上面几点,我们可以解释之前提出的问题了:【不同的两个 g_val 值对应的一个地址?】

  • 因为打印出来的是虚拟地址,而两个进程是父子进程,子进程继承于父进程,继承了 g_val 变量的虚拟地址,所以打印出来的地址相同。
  • 因为两个虚拟地址映射的物理内存不是一块,而是两个不同的物理内存空间,所以存放不同的数据,一个存 20 ,一个存 100 是完全可以的。

地址空间的本质

在系统中,同时存在很多进程,那就会有很多进程地址空间,对于这些地址空间,操作系统也需要管理,所以会定义一个结构体/类来描述地址空间,这样每个地址空间就可以成为一个对象了,然后再通过某种数据结构连接起这些对象,从而通过增删查改来实现管理(在Linux系统中,描述地址空间的结构体叫 mm_struct)。
在这里插入图片描述
综上,那么地址空间的本质是什么呢?—— 特定数据结构的对象。

地址空间的区域划分

进程地址空间通过结构体来描述了,那地址空间中的不同区域的划分会通过成员变量来划分。
在这里插入图片描述

三、页表

地址空间(结构体对象)不具备保存代码和数据的能力,代码和数据是在物理内存中存放的。所以需要为进程提供一张虚拟地址和物理内存的映射表,该表就是页表。
在这里插入图片描述

  • 页表也是先描述,再组织进行管理,它本质也是一个结构体对象。
    在这里插入图片描述

  • 进程地址空间(结构体)中有指针指向页表地址。
    在这里插入图片描述

  • 虚拟地址到物理地址的转换工作,由 CPU 完成。
    在这里插入图片描述

    • CPU 中有一个硬件,MMU(memory manage unit)内存管理单元,该硬件集成在 CPU上,整个管理,映射,查找工作都由硬件自动完成。
    • CPU 中存在一个寄存器 CR3,里面存放当前进程页表的起始地址(CR3 中存放的就直接是物理地址,因为如果是虚拟地址,会无法映射找到物理地址)。
    • CPU 通过 CR3 找到页表,通过页表查找到某个变量虚拟地址对应的物理地址,然后进行访问,修改。

四、为什么要有进程地址空间?

  1. 将物理内存从无序变为有序,让进程以统一的视角,看待内存
    将不同位置不连续的物理内存可以映射到连续的一块空间。
    每个进程看到的都是同一个地址空间,同样的区域划分。

  2. 内存管理和进程管理分开/解耦
    两个方面只需要通过页表映射连接即可,即使内存管理出了问题,某个进程重新选择物理内存空间,也只需要改动页表即可,不会影响到进程管理。

  3. 地址空间+页表是保护内存安全的重要手段
    当进程对内存的访问不合法时,操作系统可以拦截,阻止进程转换虚拟地址,必要时操作系统可能会杀死进程。
    在这里插入图片描述

  4. 提高空间资源的利用效率

  • 当我们通过 new/molloc 申请内存时,如果一申请就直接给你一段物理内存空间,但你不立即使用,那这段时间这段空间就浪费了(已经给你了,那操作系统也控制不了这块空间了),而操作系统,一定要为效率和资源使用率负责,这种做法显然不合适。
  • 那么地址空间的存在就可以解决这个问题:① 先在地址空间申请空间。申请内存时,将地址空间堆上某一区域地址给进程,该地址当前在页表上没有映射内容;② 2. 等用户使用时,再申请物理内存空间。当用户尝试通过虚拟地址进行写入时,操作系统会暂停写入操作(缺页中断),然后在物理内存上开辟空间,并在页表上建立映射关系,再恢复写入操作。
  • 关于分成两个阶段后的效率问题: 就算直接给内存,那也需要【申请地址空间】【申请物理内存】两步操作,只不过是一起做的。现在操作系统将两个部分拆开,在总的时间成本上没有任何差别,但拆开后,会使 new/malloc 的效率变高。

本文到这里就结束了,如果对您有帮助,希望得到您的一个赞!🌷
如有错漏,欢迎指正!😄

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词