【Linux】进程地址空间

进程地址空间是Linux进程概念中非常重要的一个点,也是Linux系统学习的基础

下面进入正文:


一、认识进程地址空间

1.1 进程地址空间简介及内部结构展示

在我们学习C/C++的初始阶段,我们听过C/C++有:栈区、堆区、静态区(全局区)、字符常量区、代码区。C语言内存分配有三种方式:从静态区存储区域分配、从栈上分配、从堆上分配。

我们或许也通过编译器打印地址的方式了解过内存分配。

那么计算机物理内存是否按照这种顺序排序呢?

不一定。我们在编程语言层面上看到的地址,本质都是虚拟地址,也就是进程地址空间。

Linux内核中的进程地址空间不是地址,其本质是一种数据结构mm_struct

内部结构: 

1.2 进程地址空间的创建

当可执行程序(.exe)从磁盘加载到物理内存中时,操作系统创建进程数据结构PCB、进程地址空间mm_struct 以及 页表。

mm_struct结构体规定区域划分,如上图。

我们在语言层面上看到的各种地址,本质都是真正的地址(程序在物理内存中的地址)通过页表映射出的虚拟地址。

我们在语言层面上看到数据在内存中的存储是如上图那样的顺序,而实际上数据在物理内存中存放的顺序不一定是上图中的顺序。语言层面上的地址本质上都是虚拟地址。

每一个进程创建时,都会有一个进程地址空间。该进程认为进程地址空间中的虚拟地址就是真是的地址,因而对数据的存储也会以mm_struct的规则进行。

1.3 static 、const的本质

在C语言中,static关键字的作用如下:

1、在修饰变量的时,static修饰的静态局部变量只执行一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。 

2、static修饰全局变量的时,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是extern外部声明也不可以。 

3、static修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。Static修饰的局部变量存放在全局数据区的静态变量区。

const具有常属性,不可被改变。 

那么static 和 const为什么不能执行上述那些操作呢?

是被他俩修饰的变量不能被写入到物理内存中吗?

当然不是。被static或const修饰的变量的虚拟地址在mm_struct的特定区域内,不同的区域有不同的权限,权限只能缩小不能放大,所以它们可以执行的操作必然受到约束。 


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

既然进程地址空间里的地址都是虚拟地址,那么真实的物理地址是怎么和虚拟地址联系起来的呢?

事实上,操作系统在 进程地址空间(mm_struct)物理内存之间还生成了一个页表,页表内部结构类似于哈希结构,一边存放虚拟地址,另一边存放物理地址。也就是说页表让虚拟地址和物理地址一一映射,这样虚拟地址就和物理地址联系起来了。

下面我们一起来看一个有意思的现象。

  #include <stdio.h>                                                                                                                                                     
  #include <unistd.h>
  #include <stdlib.h>
  
  int g_val = 0;
  
  int main()
  {
      pid_t id = fork();
      if (id < 0)
      {
          perror("fork");
          return 0;
      }
  
      else if (id == 0)
      {
          // child
          printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
      }
      else
      {
          //parent
          printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
      }
      sleep(1);

      return 0;
  }
  

 

我们看到子进程和父进程的g_val和地址是一样的,这很好理解呀,因为g_val的地址只有一份呀!

那么现在看下面这种情况:

 

我们发现,父子进程,输出地址是一致的,但是变量内容不一样! 

这似乎不符合逻辑呀,同一个变量,怎么可能同时有两个值?!!

事实上,该变量发生了写时拷贝。

写时拷贝

创建子及进程时,子进程会对父进程的mm_struct以及页表做一份拷贝。此时子进程和父进程共用用一份物理内存和虚拟地址。只有当子进程对进程数据修改时,操作系统会对其分配一块物理内存,此时子进程页表中所映射的物理地址为拷贝后子进程的地址。但是由于mm_struct是拷贝父进程的,所以子进程的虚拟地址和父进程虚拟地址相同,即便数据修改,虚拟地址也不会改变。所以父子进程虚拟地址一直是相同的。  


三、进程地址空间存在的意义

3.1 维护系统安全

有些进程会进行非法访问,这对操作系统的内核数据和合法数据具有很大的威胁。

有了地址空间之后,凡是非法的访问或者映射,OS都会识别到并终止这个非法的进程,有效的保护了物理内存。

地址空间和页表是OS创建并维护的!这就意味着凡是想使用地址空间和页表进行映射,也一定要在OS的监管之下来进行。

也便保护了物理内存中的所有合法数和各个进程,以及内核的相关有效数据。

3.2 降低进程之间的耦合性 

在一个项目中,模块和模块之间的耦合性越低,那么后期维护的成本越低,所以低耦合十分重要。

有地址空间和页表映射的存在,我们可以对未来数据在物理内存中进行任意位置的加载吗?

当然可以!

因为进程地址空间的虚拟地址要映射到物理地址,然而具体映射到物理内存的哪个位置都是可以的。所以物理内存的分配就可以和进程的管理做到没关系!

如此内存管理模块与进程管理模块就完成了解耦合!

所以,我们在C/C++语言上new、malloc、realloc空间的时候,本质都是在虚拟地址空间上申请的!

本质上,因为有地址空间的存在,所以在上层申请空间,其实是在地址空间上申请的,物理内存甚至可以一个字节都不给分配!

而当真正进行对物理地址空间访问的时候(此过程由操作系统自动完成,用户、进程完全0感知),才执行相关管理算法,然后申请物理内存,构建页表映射关系,最后,才可以进行内存的访问。

3.3 进程独立性 

因为在物理内存中,理论上可以任意位置加载,那么是不是物理内存中几乎所有数据和代码都是乱序的呢?

是的!但是因为页表的存在,可以将虚拟地址和物理地址进行映射,那么在进程的分布视角,所有的内存分布都是有序的!

地址空间+页表的存在,可以将内存分布有序化!

进程要访问的物理内存中的数据和代码,可能目前并没有在物理内存中,同样的,也可以让不同的进程映射到不同的物理内存,如此便很容易做到进程独立性的实现!!

因为由地址空间的存在,每一个进程都认为自己拥有整个内存空间,并且各个区域都是有序的,进而可以通过页表映射到不同的区域,来实现进程的独立性!每一个进程不知道也不需要知道其他进程的存在!!!


四、程序运行与进程地址空间的联系


当程序进行编译形成可执行程序的时候,没有被加载到内存的时候,我们的程序内部就已经有了地址。因为地址空间不仅是OS内部需要遵守,编译器也要遵守!!!,即编译器编译代码的时候,就已经形成了各个区域,并且采用和Linux内核一样的编码方式,给每一个变量,每一行代码都进行了编址,每一个字段早都具有了一个虚拟地址!!!

程序内部的地址,依旧用的是虚拟地址,当程序被加载到内存中的时候,每行代码。每个变量便具有了一个物理地址,外部的。

生成可执行程序后,当可执行程序从磁盘加载到内存中,虚拟地址和物理地址在页表中一一映射,CPU访问mm_struct中的虚拟地址,通过页表找到物理地址,再到程序内部读取指令,结果返回到CPU进行操作。

注意:CPU在物理内存中读取到的程序的指令是虚拟地址!!! 


以上就是本篇文章的全部内容,欢迎大家学习交流。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值