Linux中对进程地址空间和写时拷贝的理解

C/C++程序的地址空间

首先,抛出一个问题:

我们平时所说:一个c/c++程序,内存被发划分好多个区域啊,自地址空间,从下往上增长,就有代码区,字符常量区,已初始化全局区,未初始化全局区,堆区,共享区,栈区等区域;

请问这是内存吗?
答案:肯定不是,那到底是什么呢?我们暂且不说;


在这里插入图片描述


我们验证一下:这几个区域是否按上面画图的排序:从代码角度查看一下:

#include<stdio.h>                                                                                                                      
  
#include<stdlib.h>    
  int g_init_val = 100;    
  int g_noinit_val ;    
int main(int argc,char* argv[],char* env[])    
  {    
    const char* s = "hello world\n";    
    printf("字符常量区的地址:%p\n",s);    
    printf("已初始化全局区的地址:g_init_val= %p\n",&g_init_val);    
    printf("未初始化全局区的地址:g_noinit_val= %p\n",&g_noinit_val);    
      
    int* pa =(int*) malloc(100);    
    int* pb =(int*) malloc(100);    
    int* pc =(int*) malloc(100);    
    printf("堆区地址:pa = %p\n",pa);    
    printf("堆区地址:pb = %p\n",pb);    
    printf("堆区地址:pc = %p\n",pc);    
      
    int a = 10;    
    int b = 20;    
    int c = 30;    
    printf("栈区地址:a = %p\n",&a);    
    printf("栈区地址:b = %p\n",&b);    
    printf("栈区地址:c = %p\n",&c);    
    int i = 0;    
    for(;argv[i];i++){    
      printf("命令行参数的区域地址:argv[%d] = %p\n",i,argv[i]); 
         printf("命令行参数的区域地址:argv[%d] = %p\n",i,argv[i]);
    }
    i = 0;
    for(;env[i];i++){
      printf("环境变量参数区域地址:env[%d] = %p\n",i,env[i]);
    }
  return 0;
  } 

在这里插入图片描述


由上验证:观察地址空间的大小,确实是按照我们图的排序的;


虚拟地址的引入

我们可以观察一个程序:
这个程序主要是创建一个子进程,在父进程中修改全局数据;观察父子进程获取该全局数据的变量值和地址;

#include<stdio.h>    
#include<stdlib.h>    
#include<unistd.h>    
    
int g_val = 100;    
int main(){    
  pid_t pid = fork();    
  if(pid == 0){    
    //child process    
    printf("i am a child; g_val = %d,&g_val =%p\n ",g_val,&g_val);    
  }    
  else if(pid > 0){    
    g_val = 10;    
    printf("i am a parent;i modify  g_val =%d,&g_val =%p \n",g_val,&g_val);                                                              
  }    
  sleep(1);    
  return 0;    
}  

在这里插入图片描述


我们观察到一个现象:父子进程访问全局变量的地址明明是一样的,但是值确实不一样的?

我们知道进程之间都是独立的,也就是说这里的父子进程都是独立的;
我们通过现象发现:它们虽然都是访问同一个变量,但是在父进程修改该变量的值,不会影响到子进程,这就证实了进程之间的独立性;
但是:为什么父子进程访问同一个变量,数据都不一样,而地址空间确实一样的呢?
>这就说明一个问题:我们在程序中看到的地址,并不是真正意义上的地址空间;也不是所谓的内存
它只是我们常说的一个概念:虚拟内存,也就是说,我们程序所看到的内存布局空间,都是虚拟内存;
而真实的物理内存,我们是看不到的;


也就是说:所谓的内存:本质指的是物理内存,而我们从语言角度上谈论的内存,只不过是虚拟内存罢了;


在操作系统的角度上:每个进程都有自己一个独立的进程地址空间,也就是虚拟内存;
而操作系统为了管理进程,就必须管理进程的PCB,而进程也有自己的进程地址空间,也必须管理进程地址空间;
对于操作系统,为了管理一项东西,必须干的事是:先描述,再组织
先对进程地址空间描述:在Linux操作系统:进程地址空间就是数据结构的集合,Linux用一个结构体:mm_struct{}描述进程地址空间,也就是我们的虚拟内存,而这个进程地址空间mm_struct只不过也是进程PCB中的一个属性罢了



在这里插入图片描述


那进程地址空间里面mm_struct有什么呢?

由于进程的地址空间,都是被划分为多个逻辑结构区域的,也就是堆区,栈区,字符常量区等这些区域,而
要描述这个区域,我们就必须再mm_struct 有属性描述它们,也就是描述堆区,栈区等区域的范围;
自然而然的,在mm_struct里存放的就是这些区域范围的变量;


在这里插入图片描述


请再次注意:每个进程都认为自己有自己的进程地址空间,也就是每个进程都有自己单独的mm_struct,并且在32位下,进程地址空间大小位4GB,也就是说,每个进程都认为自己对内存是有4GB的使用权的,但是真实情况如何呢?是否真的有这4GB的使用权呢?


虚拟地址到底是什么?

虚拟地址:就是地址空间被划分时候,对应的线性位置就是虚拟地址;
也就是在mm_struct中里面的属性:区域划分开的属性,它就表示虚拟地址;
可以宏观理解:进程地址空间,是由4GB个虚拟地址组成的;


页表 MMU 的作用

而这个虚拟地址是如何访问到物理内存的呢?

在Linux操作系统:我们是通过 页表来完成记录:虚拟地址和物理地址之间的映射,然后通过MMU内存管理单元这个硬件设备来查询页表;从而达到进程访问虚拟内存时候,能通过页表的查询,找到真正的物理内存;


对于每个进程来说:操作系统都会为它维护一张页表,该页表就是存放虚拟地址和物理地址之间映射关系,当然,还会记录对该地址是否有读写操作权限;
在这里插入图片描述


现如今:我们必须有一个清晰的认识:
对于一个进程:我们是有一个进程控制块:task_struct{ },该task_sruct里面有个属性:mm_struct{ }也就是进程地址空间;还有一个页表:存放虚拟内存和物理内存映射;而加上我们的进程代码和数据;


为什么需要用页表MMU来管理物理内存

也就是说,直接让进程访问到物理内存不方便吗?为什么要通过页表和MMU这些设备来建立虚拟地址和物理地址映射关系?

很简单:就是为了操作系统方便合理管理物理内存这个设备;你说,页表是建立虚拟地址和物理地址的映射,这是谁做的?就是操作系统做的事;操作系统仅仅就是做了映射吗?不,它还做了一件很重要的事,就是给该映射地址赋予权限,到底是可读还是可写的数据,都是操作系统为你分配好的;它这么做为了什么?不就是为了安全嘛;
你说假如让你直接访问物理内存,你是否会可能越界访问到其他进程的数据,这就很不安全啊,你修改了其他进程的数据,不是你的东西,你却可以修改该成功;这肯定不可取的;


为什么要有进程地址空间

  1. 我们认为:操作系统为了安全有效的管里物理内存,通过增加一层软件层,也就是进程地址空间,能够完成该进程的风险管理,也就是让该进程访问和修改该自己的数据,不是它的数据不可以访问修改该;

  1. 其实我们在向内存申请空间时候,并不是一申请就会给你在物理内存开辟好空间的;假如操作系统在你已申请内存就给你那么多物理内存的话,你却没有使用该内存呢?那是不是造成了你申请该内存的浪费;对于其他进程来说,它想申请内存时候,发现内存不够用,实际上,有内存是被你的进程申请却没有使用;这就导致管理不够合理;操作系统也不允许这样的事情发生;
    所以操作系统要引入一个进程地址空间,通过申请内存,先给进程地址空间划分好区域给你先,但是实际上,物理内存并没有你的空间信息,对于用户来说:物理内存和虚拟进程地址空间是透明的,你是无法察觉到的;
    当你申请空间,再考试使用该空间时候,操作系统才会帮你去查询页表,发现页表只有虚拟地址,没有对应物理地址,就会发生一个缺页中断机制:它完成的工作就是,OS通过该机制,发现你要使用的空间没有物理地址映射,给你完成虚拟地址和物理地址的映射,并且允许你访问物理内存,使用该空间;

总结来说:有了进程地址空间的软件层,将申请空间和使用空间进行在时间上进行分离,通过进程地址空间,达到对用户透明的效果,对用户屏蔽了OS在底层申请空间的过程;进而达到了进程读写内存和操作系统管理内存的分离操作互不干扰;


其实还是举例子一下:我们所说:假如我们物理内存没有你申请空间多的大小的空间时候,是否会申请空间成功呢?

答案有可能滴,从虚拟的进程地址空间的角度上来说:你申请空间,在没有使用的情况,是不会帮你映射到物理空间上的,也就是说,对于用户来说,你申请了空间,你就认为你有了那么多空间可以使用;
但是对于OS来说,实际上并没有给你进程分配内存; 这就是进程读写和操作系统管理的分离的理解;


  1. 站在CPU的角度和应用层的角度,进程都可以统一被看成有4GB 的独立的地址空间,而且每个区域的划分都是相对确定的,这样我们CPU在读取进程的代码和数据时候,就可以快速定位到位置;对于物理内存来说,实际数据代码却可以存放任意位置;

再次理解写时拷贝

我们知道:操作系统有一个机制是写时拷贝机制,对于fork出来的子进程,默认情况下,fork是以父进程为模板,几乎完完全全的复制了父进程的所有数据和代码;当然有些进程pid,或者其他没有复制过来;
当我们没有修改该父子进程直接的数据时候,它们的数据都是共享的,当我们修改父子进程任意一方的数据时候,就会发生写时拷贝,也就是修改一方的进程,会把共享的数据复制出来一份到物理内存,再区修改;


在这里插入图片描述


对于上图:
没有发生写时拷贝,也就是fork之后,父子进程都时共享同一块物理内存,fork之后,子进程的虚拟地址,也就是子进程的进程地址空间,都是基本完全和父进程一样的,页表也是一模一样滴;
但是一旦发生了写时拷贝,此时指向物理内存的进程就会被复制一份出来,在复制的那一份修改数据;
这也可以解释,为什么父子进程的数据地址是一样的,因为fork是复制出来的虚拟地址,和父进程是一样的;
为什么地址一样,而数据不一样?因为地址都是虚拟地址,而真正的数据是存放在物理地址,物理地址不一样,只不过虚拟地址一样,但是父进程和子进程的虚拟地址却指向不同的物理地址罢了;


  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呋喃吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值