【Linux】进程地址空间(带你认清内存的本质)

       🔥🔥 欢迎来到小林的博客!!
      🛰️博客主页:✈️小林爱敲代码
      🛰️博客专栏:✈️Linux之路
      🛰️社区 :✈️ 进步学堂
      🛰️欢迎关注:👍点赞🙌收藏✍️留言

💖进程地址空间

我们在学习C语言的时候,应该都知道这个内存空间图。

在这里插入图片描述

但其实我们对它并不了解,为什么呢?我们用一段代码来感受一下!

#include<stdio.h>
#include<unistd.h>

int g_val = 100;

int main()
{
  int pid = fork(); //创建子进程
  if(pid == 0)
  {
    //child
    int count = 5;
    while(count)
    {
      printf("i am child , g_val = %d, &g_val = %p\n",g_val,&g_val);

      if(count == 3)
      {
        //修改数据
        printf("******开始修改数据*******\n");
        printf("i am child , g_val = %d, &g_val = %p\n",g_val,&g_val);
        g_val = 200;
        printf("******修改数据done*******\n");
      }
      count--;
      sleep(1);
    } 
    
  }
  else if(pid > 0)
  {
    //parent
    while(1)
    {
      printf("i am father , g_val = %d, &g_val = %p\n",g_val,&g_val);
      sleep(1);
    }
  }
  else 
  {
    //erro
    perror("fork:");
  }
  return 0;
}

我们这个代码的主体逻辑是,创建一个全局变量。然后再创建一个子进程,随后打印全局变量的值和地址,在子进程特定的时候修改这个全局变量。

那么我们来看看运行结果吧!

在这里插入图片描述

我们可以发现,g_val的值被修改了! 但是它们的地址还是一样的。这是怎么回事!???

我们都知道,父进程和子进程如果没有发生数据修改,那么会共用同一份数据。如果有一方的数据发生了修改,那么就会写实拷贝一份。所以此时的父进程和子进程各有一份属于自己的数据,既然有2份数据那么就说明有2个g_val。2个独立进程的g_val变量用了同一块内存空间,这合理吗?完全不合理!!!2个进程用同一块空间,这不就起冲突了。这是为什么呢??

再探索这个问题之前,先给大家讲个小故事,方便大家理解。

在美国有一个大富翁,他有三个私生子,这三个私生子互相不认识。而这个大富翁有100亿美金。

在这里插入图片描述

然后这个时候,大富翁对私生子小A说:等我老了,就由你来继承我100亿财产吧。这时小A就以为这100亿是他的了。然后大富翁又对私生子小B说:等我老了,你来继承我的100亿吧。 小B听了高兴坏了,也以为这100亿是他的了。然后大富翁又对私生子小C说了同样的话。 所以,大富翁给他的所有私生子都花了一张大饼。告诉他们,他们未来都会继承这100亿。所以大富翁的私生子,都认为自己有100亿可以花,就可以按照这100亿来为自己分配生活。

在这里插入图片描述

而这里面的大富翁,就是操作系统,私生子就是进程,大饼就是进程地址空间,那么这100亿美金,就是我们的物理内存。而我在之前的篇章里说过,进程的本质其实就是 描述进程的结构体(PCB)+代码数据。那么进程地址空间是否在PCB里呢?答案当然是的。也就是说每个进程都会有一个进程地址空间,每个私生子都认为自己独占了大富翁的100亿美金,所以每个进程都认为自己独占了物理内存。 所以,我们的**进程地址空间,也被我们称之为虚拟内存。**那么为什么会打印相同地址?我们先了解一些东西,再最侯为大家总结结论。

💖进程地址空间是什么?

那么进程地址空间是什么呢?地址空间本质是内核中的一种数据类型,在Linux内核中,它是一个struct mm_struct的结构体。

在这里插入图片描述

也就说,我们程序的内存划分,本质上是一个区域!!

在这里插入图片描述

进程地址空间的划分

那么进程地址空间是怎么划分的?

打个比方:

假如你现在是一名小学生,你的同桌是一名爱干净的小女孩。而你一名爱流鼻涕不讲卫生的小男孩,这时侯,你的同桌嫌弃你。假设你俩的桌子长100cm,此时你的同桌在50cm的地方画了一根三八线,跟你划清界限。那么此时你还能不能把东西放到你同桌所在的区域?当时是不能了!假设你的区域是 0 - 50cm的地方,那么你的东西只能放在0-50区间。假设这时候有一把尺子,你想把你的橡皮擦放在第38cm的地方,于是你就拿尺子量出了38cm,把橡皮放在这个位置上。这里面呢,你和你同桌,充当的是一块区域,而这把尺子,是进程地址空间,橡皮擦,则是你的数据。你要把数据放在指定的地方,那么就需要进程地址空间充当尺子。为什么你知道桌子是100cm?因为有尺子,所以你才知道桌子是100cm。所以你要划分区域,也需要进程地址空间充当尺子来划分区域。

struct mm_struct
{
	unsigned int code_start; //代码段起始地址
	unsigned int code_end;  //代码段结束地址
	
	unsigned int init_data_start;//初始化变量区起始地址.
	unsigned int init_data_end;//初始化变量区结束地址
	
	unsigned int uninit_data_start;//未初始化变量区起始地址.
	unsigned int uninit_data_end;//未初始化变量区结束地址
	
	unsigned int heap_start;//堆区起始地址.
	unsigned int heap_end;//堆区结束地址
	
	.....
	unsigned int stack_start;//堆区起始地址.
	unsigned int stack_end;//堆区结束地址
}

每个进程都认为地址空间的划分是按照4GB的空间划分的,而地址空间上进行区域划分的位置,是虚拟地址!虽然这里只要start和end,但是每个进程都可以认为mm_struct 代表整个内存,且所有的地址为0x00000000 -> 0xFFFFFFFF。

虚拟内存转换成物理内存

既然每个进程都有一块地址空间,而程序里面的数据和代码都是根据进程地址空间存放。那么我们的系统调用它时是如何为它分配地址的呢?如何把对应的数据放到物理内存的呢?

那是因为物理内存和虚拟内存之间,有一张页表。

在这里插入图片描述

而页表的本质就是哈希表。通过虚拟内存来映射物理内存。也就说,每一个进程地址空间,都会有一张对应的页表。意思就是每一个进程都会有一张页表,通过页表的虚拟地址,就可以找到对应的物理内存。从而操作系统对物理内存进行操作。

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

1. 通过添加一层软件层,完成有效的对进程操作内存进行风险管理(权限管理),本质的目的是为了,保护物理内存以及各个进程的数据安全。

比如:

先给大家放一段代码。

int main()
{
	const char* str = "hello world";
    str = "HW";
	return 0;
}

这段代码会报错,为什么呢?因为 str它所处的内存空间是常量区。通过虚拟内存映射到真实的物理地址之后,它的权限是只读权限。当你修改它时,因为你不具备写权限,所以操作系统会直接把你干掉。这也是为什么要有进程地址空间的原因。如果没有进程地址空间,那么就无法进行权限管理,那么即使是常量也可以被修改!这是非常严重的!而有了进程地址空间之后,你能不能修改,全部取决于操作系统让不让你修改!

2. 将内存申请和内存使用的概念在时间上划分清除,通过虚拟地址空间,来屏蔽底层申请的过程,达到进程读写内存和OS进行内存管理操作,进行软件上面的分离!

先抛出一个问题:假如我们申请5000个字节,我们立马能使用这5000字节吗??

答案是:不一定,可能会存在暂时不会全部使用,甚至暂时不使用的情况。

因为,在OS(操作系统)的角度上,如果空间立马就给你的话,是不是就意味着,整个系统会有一部分空间,本来可以先给其他进程立马使用,现在却被你闲置着?说简单点就是这个进程现在正茅坑,但丝毫没有要拉屎的意思。这种做法是人人恨之的,所以操作系统不一定会立马给你使用。

打个比方:比如你要开学了,你和你老爹要8000块钱的学费。但是你还有一星期才开学呢,于是你老爹说:好,我知道了,开学前一天给你。 你像你老爸要了8000块钱,你老爸对应给你了。这就就相当于你申请了8000字节的空间。但是你还有一星期才开学,也就是你这8000块钱暂时用不上,你这8000字节也暂时用不上。所以你爸说等你开学前一天的时候给你,而操作系统也在进程要使用的时候,给进程真实的物理内存。

在这里插入图片描述

而这和我们的写实拷贝非常的像,数据不改变就共用同一份数据,改变就拷贝一份。

3.站在CPU和应用层的角度,进程统一可以看做统一使用4GB空间,而且每个空间区域的相对位置,是比较确定的!OS最终这样设计的目的,达到了一个目标:每个进程都认为自己是独占系统资源的!进程具有独立性的!

这种情况也就是我们开头演示的那样,为什么2个进程的g_val的地址是相同,而值是不同的。这是因为**子进程创建是以父进程为模板创建的,所以子进程也会继承父进程的页表。**在子进程没有对g_val的值进行修改时,父子进程共享一份数据。而一旦子进程对g_val的值进行修改,那么在OS会对g_val的数据进行一份拷贝(写实拷贝)。且让子进程页表映射到g_val的值映射至新拷贝后的物理地址。这样子,即使它们的g_val的地址是相同的,但是在它们在页表 g_val的数据 是映射到不同的物理地址。

在这里插入图片描述

最后,为什么下面str1和str2的地址是相等的?

#include<stdio.h>
int main()
{
	const char* str1 = "hello world";
	const char* str2 = "hello world";
	printf("str1 的地址是: %p",str1);
	printf("str2 的地址是: %p",str2);
}

如上代码,我们会发现str1和str2的地址是一样的,可是它们不是同一个变量啊,为什么?因为str1和str2都在常量区。也就是说操作系统只给了这个区域可读权限,所以操作系统认为,对于只有可读的数据,操作系统只需要维护一份即可。

  • 29
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 30
    评论
评论 30
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

林 子

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

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

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

打赏作者

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

抵扣说明:

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

余额充值