进程地址空间

我们一起来看一看C/C++程序地址空间布局:
在这里插入图片描述
问题:这个C/C++程序地址空间是内存吗?

答案:这个不是内存!

那么它究竟是什么呢?

进程虚拟地址空间!

我们来一段代码感受一下布局:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
int g_unval;//为初始化全局数据
int g_val=100;//已初始化全局数据
int main()
{
  const char *s="hello world";
  printf("code addr:%p\n",main);//栈区
  printf("string rdonly addr:%p\n",s);//字符常量区
  printf("init addr:%p\n",&g_val);//已初始化化全局区
  printf("uninit addr:%p\n",&g_unval);//未初始化全局区
  
  char *heap=(char*)malloc(10);
  char *heap1=(char*)malloc(10);
  char *heap2=(char*)malloc(10);
  char *heap3=(char*)malloc(10);
  printf("heap addr:%p\n",heap);//验证堆向上生长
  printf("heap1 addr:%p\n",heap1);
  printf("heap2 addr:%p\n",heap2);
  printf("heap3 addr:%p\n",heap3);
  
  printf("stack addr:%p\n",&s);//验证栈向下生长
  printf("stack addr:%p\n",&heap);

  int a=10;
  int b=20;
  printf("a addr:%p\n",&a);
  printf("b addr:%p\n",&b);
  return 0;
}

结果展示:

[sjj@VM-20-15-centos 2022_4_1]$ ./myproc
code addr:0x40057d
string rdonly addr:0x400770
init addr:0x60103c
uninit addr:0x601044
heap addr:0xaf9010
heap1 addr:0xaf9030
heap2 addr:0xaf9050
heap3 addr:0xaf9070
stack addr:0x7ffce118bc00
stack addr:0x7ffce118bbf8
a addr:0x7ffce118bbf4
b addr:0x7ffce118bbf0

可以看到确实是按照我们的规则排布的!
我们再来一段代码感受一下:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
int g_val=100;//全局变量,被父子进程共享
int main()
{
  pid_t id=fork();
  if(id==0)
  {
    //chlid
    int cnt=5;
    while(cnt)
    {
      printf("I am child! times:%d,g_val=%d,&g_val=%p\n",cnt,g_val,&g_val);
      cnt--;
      sleep(1);
      if(cnt==3)//三秒后修改数据
      {
        printf("#############child更改数据####################\n");
        g_val=200;
        printf("#############child更改数据完成################\n");
      }
    }
  }
  else
  {
      while(1)
      {
      printf("I am father! g_val=%d,&g_val=%p\n",g_val,&g_val);
      sleep(1);
      }
   }
  return 0;
}

演示效果:
在这里插入图片描述
父子进程打印出来的地址怎么能也没有变化呢?

 如果C/C++打印出来的是物理地址,那么这种现象是不可能存在的!所以我们这里,所使用的地址绝对不是物理地址!而被称为虚拟地址

 一般而言我们在语言层面上用到的地址,都是虚拟地址!
 我们系统中存在多个进程,每个进程都有一个地址空间,都认为自己独占整个物理内存!!!我们要管理地址空间,之前就已经提到过了,就得用先描述再组织的方式!描述:进程地址空间再内核中是一个数据结构类型,含有描述进程的地址空间变量,Linux下进程地址空间用一个结构体来描述——mm_struct
在这里插入图片描述
 说明:虽然这里只有start和end,但是每个进程都可以认为mm_ struct 代表整个内存且所有的地址为0x00000000到0xFFFFFFFF,我们就把这串地址,叫做虚拟地址,我们可以将进程地址空间想象成一把有刻度的直尺,从0开始到全F,每一个划分的区间可以对应不同的区域,每一个刻度对应一个虚拟地址,也叫作线性地址。
在这里插入图片描述
可是数据总归是要存储在物理内存上的啊,那么虚拟地址与物理内存地址是怎么建立起联系的呢?那就是通过页表+MMU(MMU内存管理单元,用来查页表的)映射实现的!
在这里插入图片描述
 页表:操作系统通过页表将虚拟地址转换为物理地址,进而再去访问代码和数据。
 为什么我们要有页表映射这一中间层?通过PCB直接访问物理内存不是更加方便吗?

原因:那是为了我们的所有操作都在操作系统的约束下进行,加了中间的虚拟地址和页表映射能够更好的管理约束进程!

在这里插入图片描述
为什么要有进程虚拟地址空间?
先来看几个例子,得出我们的结论!
 例1:如果没有虚拟地址空间,PCB就能直接访问物理内存的数据和代码,甚至能够修改里面的信息,这是很不安全的所以我们要加一个中间软件层——虚拟地址空间和页表
 例2:如果要申请1000字节,我们能马上用它吗?答案是不一定,我们可能会用,也可能不用。站在OS操作系统的角度看,空间如果马上给你,是不是意味着有一部分空间要给别的进程使用,但是现在却给了你闲置着。所以我们可以通过虚拟地址空间,先把内存同意申请给你,到你真正要用它的时候,OS会立刻通过页表建议映射关系,马上到物理内存上给你分配空间,但是反馈给上层(如PCB它是并不清楚底层的情况的)的确实分配到了相应的内存空间!
 例3:我们的CPU如果没有虚拟地址空间和页表映射,那么内存中本来就有着多个进程,那么每次CPU都要到物理内存中去找相应的代码和数据,每次寻找的地址都不一样,这对于CPU来说,效率就很降低,那么我们通过虚拟地址空间和页表映射,我们CPU统一将每个进程都看做一样的进程地址划分,main函数每次开始都是一样的虚拟地址,return也是同样的虚拟地址,即使每个进程在内存中的布局很混乱,通过页表的映射关系,总是能够找到对应的代码和数据。

结论:
 1.通过添加一 层软件层,完成有效的对进程操作内存进行风险管理(权限管理),本质目的是为了,保护物理内存以及各个进程的数据安全!
 2.将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存的过程,达到进程读写内存和OS进行内存管理操作,进行软件的解耦和分离!
 3.站在CPU和应用层的角度, 进程统一可以看做统一使用4GB空间(32位), 而且每个空间区域的相对位置是比较确定的!

操作系统这样设计的目的:让每个进程认为自己独占系统资源!!!

知道上面这些结论我们就能解释开头的那个现象了!
 本质上我们的父子进程的&g_val的值是一样的,这个地址就是虚拟地址。
每个进程都有自己的虚拟地址空间和页表, fork创建子进程后,子进程也是用于自己的虚拟地址空间和页表,同一个变量,地址相同,其实就是虚拟地址相同,内容不同其实是被映射到了不同的物理地址中!3秒时修改g_val值的时候,发生了写时拷贝,将子进程在物理空间上重新拷贝一份到新的物理地址上面,然后再重新在页表上面建立新的映射关系!
  ps:进程的特点:子进程以父进程为模板,进程间是具有独立性的!父子进程的代码一般是共享的!所以一般的只读代码,一般可以只有一份(也是从代码区到物理空间,通过页表建立映射关系),因为这样操作系统的代价才是最低的!
在这里插入图片描述
我们是不是也可以打印一下命令行参数呢?

int main(int argc,char* argv[],char* env[])
{
	for (int i = 0; argv[i]; i++)
	{
		printf("argv[%d]:%p\n",%d,argv[i]);
	}
	for (int i = 0; env[i]; i++)
	{
		printf("argv[%d]:%p\n", %d, env[i]);
	}
	return 0;
}

结果展示:
在这里插入图片描述
 对比之前的进程地址空间,我们能发现,这些地址全部都是在栈的上面,这也完美印证了命令行参数和环境变量在我们栈上面!
总结
 现在对于进程的理解我们又提升了一个台阶:一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建!

评论 42
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值