Linux 进程地址空间

一,引出虚拟地址。

首先,我们来做一个实验。

 #include<stdio.h>
  2 #include<unistd.h>
  3 int g_val=200;
  4 int main()
  5 {
  6   pid_t id=fork();
  7   if(id==0)
  8   {
  9     int cnt=0;
 10     while(1)
 11     {
 12       printf("I am child,pid: %d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
 13       sleep(1);
 14       cnt++;
 15       if(cnt==5)
 16       {
 17         g_val=400;
 18         printf("child vhange g_val 200->400\n");
 19       }
 20     }
 21   }
 22   else{
 23     while(1)
 24     {
 25 
 26       printf("I am father,pid: %d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
 27       sleep(1);                                                                                                                                                                                              
 28     }
 29   }
 30 }


在这里插入图片描述

我们发现,同一个地址,同时读取的时候,竟然出现了不同的值!!!
**所以我们得出结论,这里的地址绝对不是物理内存的地址! 因为同一块物理地址父子进程在读取的时候应该是同样的内容。所以不是物理地址,而是虚拟地址(线性地址)。所以,几乎所有的语言,如果他有地址的概念,这个地址一定不是物理地址,而是虚拟地址。

二,虚拟地址。

1, 地址空间排布图

在这里插入图片描述

2,验证地址空间排布

    1 #include<stdio.h>                                                                                                                                                                                          
    2 #include<stdlib.h>
   
    4 int ung_val;
    5 int g_val=100;
    6 int main(int argc,char *argv[],char *env[])
    7 {
    8 printf("code addr: %p\n",main);
    9 printf("init global addr: %p\n",&g_val);
   10 printf("uninit global addr: %p\n",&ung_val);
   11 static int test=10;
   12 char *str="hello";
   13 char *heap_p=(char *)malloc(10);
   14 char *heap_p1=(char *)malloc(10);
   15 char *heap_p2=(char *)malloc(10);
   16 char *heap_p3=(char *)malloc(10);
   17 
   18 printf(" addr: %p\n",&test);
   19 printf("heap addr: %p\n",heap_p);
   20 printf("heap addr: %p\n",heap_p1);
   21 printf("heap addr: %p\n",heap_p2);
   22 printf("heap addr: %p\n",heap_p3);
   23 printf("stack addr: %p\n",&heap_p);
   24 printf("stack addr: %p\n",&heap_p1);
   25 printf("stack addr: %p\n",&heap_p2);
   26 printf("stack addr: %p\n",&heap_p3);
   27 printf("read only addr: %p\n",str);
   28 int i=0;
   29 for(;i<argc;i++)
   30 {
   31   printf("argv[%d]:%p\n",i,argv[i]);
   32 
   33 }
   34 int j=0;
   35 for(;env[j];j++)
   36 {
   37 
   38   printf("env[%d]:%p\n",j,env[j]);
   39 }                                                                                                                                                                                                          
   40 return 0;
   41 }

上面的验证代码,在windows下面会跑出不一样的结果,上面的结论,默认只在Linux下有效。

三,初步理解地址空间。

在这里插入图片描述

所以我们不能直接使用物理地址。那么该怎么办呢?现代计算机提出了下面的方式。
在这里插入图片描述
cpu读到了指令,在虚拟地址中根据映射找到了物理地址,然后访问了数据,可是最终还是会访问物理地址,那万一虚拟地址是一个非法地址呢?万一映射到一个非法的物理地址,还是会影响到其他的进程,难道是为了复杂而复杂吗?
事实是如果访问了一个非法的虚拟地址,那么我们可以禁止映射,所以会变相的保护物理内存。

1,什么是地址空间

每个进程都要有地址空间,操作系统要对每一个地址空间做管理(先描述,再组织)内核中的地址空间,本质上是一种数据结构,将来一定要和一个特定的进程关联起来。我们的地址空间是一大的区域,这个区域范围是全0~全F,有范围,还是一种数据结构,所以我们得出结论:
地址空间是一种内核数据结构,他里面至少要有各个区域的划分。

在这里插入图片描述

在Linux内核中,进程虚拟地址空间叫做struct mm_struct{}。我们一定会有一个结构task_struct进程控制块用指针指向mm_struct,虚拟地址到物理地址的映射关系,操作系统为我们维护了一个表结构,叫做页表,那么我们就能根据映射关系,找到每个区域各自的数据。
在这里插入图片描述
地址空间和页表(用户级)是每一个进程都私有一份,只要保证每一个进程的页表,映射的是物理内存的不同区域,就能做到进程之间不会互相干扰,保证进程的独立性。

那么我们可以回答最开始的现象,为什么同一个地址读取时会出现不同的值?

** 刚开始创建时,只有父进程,接下来创建了子进程,子进程会继承父进程的很多属性,大多数都是一样的,地址空间和页表都是一样的,所以在地址空间中,g_val所处的位置是一样的,页表映射的物理内存也是一样的,但是当操作系统检测到子进程尝试要修改g_val时,要保证进程的独立性,操作系统会给子进程重新开辟一个空间,有必要的话,将值拷贝一份,然后修改映射关系到新开辟的内存。所以原本的值是200,子进程修改为400,但是父子进程的虚拟地址是一样的,页表映射被映射到不同的物理内存,所以我们看到的值便不一样了(写时拷贝)(我们看到的一样的地址,实质上是虚拟地址一样,虚拟地址可以是一样的)。**

现在就可以解释fork()的返回值id怎么可能同时保存不同的值?
fork内部,return会被执行两次,return的本质,就是对id进行写入,当父子进程对id进行写入时,发生了写时拷贝,所以父子进程各自其实在物理内存中,有属于自己的变量空间,只不过在用户层用同一个变量(虚拟地址)标识了。

四,深入理解虚拟地址

扩展内容
(1) 当我们的程序,在编译的时候,形成可执行程序的时候,没有被加载到内存的时候,请问我们程序内部,有地址吗?
可执行程序其实编译的时候,内部其实已经有地址了,其实地址空间不要仅仅理解成为是os内部要遵守的,其实编译器也要遵守,即编译器编译代码的时候,就已经给我们形成了各个区域,代码区,数据区。。。,并且采用和Linux内核中一样的编址方式,给每一个变量,每一行代码都进行了编址,故,程序在编译的时候,每一个字段早已经具有了一个虚拟地址。

(2) 那么当我们的CPU在读取的时候,先根据页表读到某一行指令,此时读取的指令内部的地址是什么地址?
答案是虚拟地址。
在这里插入图片描述
上面这个过程解释:编译器在编译时,已经给程序编了地址,当我们的程序加载到物理内存时,不仅仅将我们的代码加载进去,还有虚拟地址。程序内部的地址,依旧用的是编译器编译好的虚拟地址,当程序加载到内存时,每行代码,每个变量都具有一个外部的物理地址,接下来,操作系统构建PCB,地址空间,页表,因为编译的时候和Linux内核中一样的编址方式,所以地址空间可以按照加载进来的程序内部的地址来构建,然后将虚拟地址填到页表的左侧,对应的物理地址填到页表的右侧。接下来CPU读取指令,假设读取的地址为0x300,然后根据虚拟地址根据页表找到映射的物理地址,此时找到了0x300,CPU读到内部指令地址0x200,然后继续根据虚拟地址借助页表找到映射的物理地址,CPU读到内部指令地址0x100,然后继续。。。在CPU视觉中,是没有物理地址的概念。

1,为什么要有地址空间

原因一:凡事非法的访问或者映射,OS都会识别,并终止这个进程。有效的保护了物理内存中的合法数据,也包括各个进程,内核中相关的有效数据,因为地址空间和页表都是OS创建并维护的,凡是想使用地址空间和页表进行映射,也一定是在OS的监管下进行访问的

(1)字符常量不可被修改,那么是为什么呢?
页表给每一个映射关系都维护了读和写权限,字符常量最终被映射到物理内存中,物理内存是可以被写入的,所以这里的不可被写入不是硬件上,而是从软件上。
(2)物理内存,是不是可以对未来数据进行任意位置的加载?
是的,因为页表的存在,只要能通过页表映射到物理内存,那么加载到物理内存的任意位置,我们并不关心,物理内存的分配就可以和进程的管理毫无关系。

原因二:内存管理模块和进程管理模块就完成了解耦合,并且在分配内存的时候采用延迟分配的策略,来提高整机的效率 。解耦合的本质是减少了模块和模块之间的关联性。
(1)那么我们c++ c语言new malloc空间的时候,本质是在物理内存还是在虚拟地址空间申请的?
答案是在虚拟地址空间。
(2)那么我在物理内存申请空间,但不立即使用,是不是一种空间的浪费?
答案是 是的。
所以我们总结:因为有地址空间的存在,上层在申请空间时,其实是在地址空间申请的,物理内存可以一个字节都不给你,而当你真正的访问物理内存时,才执行内存的相关算法,申请内存,构建页表映射关系,然后进行地址的访问。上面这个过程,是操作系统自动完成的,用户和进程完全0感知。 (那么操作系统怎么知道在虚拟地址空间分配了空间,而物理内存没有,这里有一个技术叫做缺页中断)

原因三:
因为有地址空间的存在,每一个进程都认为自己拥有4GB的空间,并且各个区域都是有序的,进而可以通过页表映射到不同的区域,来实现进程的独立性,每个进程不知道,也不需要知道其他进程的存在。
(1)物理内存在理论上可以在任意位置进行加载,是不是在内存中几乎所有的代码和数据都是乱序的?
是的,但是有地址空间和页表的存在,可以将虚拟地址和物理地址进行映射,所以在进程的视角,所有的内存分布都是有序的。

五,重新理解挂起

加载的本质就是创建进程,那么是不是必须要立即将程序的数据和代码立马加载到内存,并创建内核数据结构和映射关系?
不是的,在最极端的情况下,甚至只有内核数据结构被创建出来了,此时这个状态被称为新建态,当真正调度的时候,才将程序和代码从外设中加载进来,所以理论上可以分批加载(换入),也可以分批换出,当一个进程短期内不会在执行,出现阻塞,那么就占用内存,这时候就可以操作系统就可以将进程的数据和代码换出,此时这个进程就叫做挂起。

页表映射的时候,不仅仅映射的是内存,磁盘也可以映射,所以我们不用将进程的代码和数据刷新到磁盘中,可以将进程在内存中直接释放,在页表中右侧填入代码和数据在磁盘中的位置。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值