程序地址空间

1.验证地址空间排布

为了验证地址的排布,我们将地址空间的各个区域的地址打印出来

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

int g_unval;
int g_val=100;

//函数参数
int main(int argc,char *argv[],char *env[])
{
  const char* str= "hello world";
 // 10;
 // 'v';
  //函数和数组的规则是一样的,函数名也代表了该函数的地址
  printf("code addr:%p\n",main);
  printf("init global addr:%p\n",&g_val);
  printf("uninit global addr:%p\n",&g_unval);
  //malloc 返回的是个指针 指针指向的即创建的在堆上申请空间的地址
  char *heap_mem=(char*)malloc(10);
  char *heap_mem1=(char*)malloc(10);
  char *heap_mem2=(char*)malloc(10);
  char *heap_mem3=(char*)malloc(10);
  printf("heap_addr:%p\n",heap_mem);
  printf("heap_addr:%p\n",heap_mem1);
  printf("heap_addr:%p\n",heap_mem2);
  printf("heap_addr:%p\n",heap_mem3);
  
  printf("stack_addr:%p\n",&heap_mem);
  printf("stack_addr:%p\n",&heap_mem1);
  printf("stack_addr:%p\n",&heap_mem2);
  printf("stack_addr:%p\n",&heap_mem3);

  printf("raed only string:%p\n",str);

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

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

代码注释:

为了打印代码区的地址,因为函数名同数组名一样,代表其首地址,故我们打印代码区的地址时输入main即可

main函数最多可以输入三个参数  分别是命令行参数的个数 命令行参数 环境变量参数,可以通过遍历输出,将命令行的参数地址打印出来

打印堆区地址时,直接输入该指针即可,指针所指即malloc在堆区上申请空间的地址

打印栈区地址时,代码上创建的变量都在栈上,指针也是变量,所以取指针的地址即可

打印结果如下

输出的地址如同地址空间排布图一样,从上至下,并且验证了堆向下增长,栈向上增长。(堆栈相对而生)

在c/c++下只输入字面常量是可以编译通过的!并且正文代码上有一块字符常量区,都是只读的!

2.什么是地址空间

操作系统给这些进程分配地址空间时,要对这些空间进行管理

所以内存中的地址空间,本质上也是一种特定的数据结构,要和特定的进程相关联起来

早期的计算机是直接访问的物理内存,内存随时可以被用户读写,这样十分不安全

为解决以上弊端,现代的计算机提出给每个进程提供一个pcb结构体,给每个进程开创一个虚拟空间,虚拟空间通过页表映射到物理内存,当虚拟地址访问非法地址时,系统会禁止映射,这样就变相的保护了物理地址。只要保证每一个进程的页表映射的是物理内存的不同区域,就可以做到进程之间互不干扰,保证了进程的独立性。

总结:每个进程都要有地址空间,它是一种特殊的数据结构,里面包含了各区域的划分。

在task_struct结构体中可以找到指向struct mm_struct的指针,里面就包括了各区域的划分

3.关于虚拟地址空间
(1)程序在编译完成后形成可执行程序但并未加载到内存时,程序内部有地址么?

有的,地址空间不仅os要遵守,编译器同样遵守!编译器再编译代码时,就已经形成各个区域,采用和linux一样的编址方式,所以程序在编译时每一个字段,就早已形成虚拟地址!

代码内的每一个变量和函数之前都有自己的虚拟地址,并且会互相跳转,通过页表映射到物理内存,在反馈给cpu执行指令,cpu会先找到物理内存中的跳转地址(虚拟地址),再根据页表映射到物理内存。

代码向系统发送指令,指令内部也有地址,该地址是虚拟地址!

(2)为何代码中的一个变量会有不同的值?
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

int g_val=100;

int main()
{
  pid_t id=fork();
  if(id<0)
  {
    perror("fork");
    return 0;
  }
  else if(id==0)
  {
    g_val=200;
    printf("child[%d]:%d:%p\n",getpid(),g_val,&g_val);
  }
  else
  {
    printf("parent[%d]:%d:%p\n",getpid(),g_val,&g_val);
  }
}

同一个地址但是值不同,说明改地址用的是虚拟地址

父子进程的g_val的虚拟地址相同,甚至一开始映射的物理地址也相同,当子进程的g_val值改变时,发生写时拷贝,子进程g_val映射的物理地址也发生改变

4.为什么要有程序地址空间?
(1)保护内存中所有的合法数据,包括各个进程以及内核的相关有效数据

凡是非法的访问或者映射,os都会识别,并且终止进程。因为地址空间和页表都是os创建的,凡是使用地址空间和页表来进行映射,都是在os的监视下来访问

(2)采用延迟分配的策略,来提高整机的效率

因为有地址空间存在,所以当上层申请空间的时候,其实是在地址空间上申请的,物理内存甚至一个字节都可以不分配!而当真正对物理内存访问时,才执行内存的相关管理算法,帮你申请内存,构建映射关系,然后再进行内存的访问(由于是os自动完成的,用户和进程完全0感知)

(3)实现进程的独立性

在物理内存中,进程中所有的数据和代码都是乱序的,但是因为页表的存在,它可以将乱序的物理内存进行映射,在进程视角中的内存分布(地址空间)就变成有序的。

因为地址空间的存在,每一个进程都认为自己拥有内存的全部空间,并且每个区域都是有序的,进而通过页表映射到不同区域,来实现进程的独立性。每个进程也不知道其他进程的存在,都认为自己独占所有的内存空间。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值