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)实现进程的独立性
在物理内存中,进程中所有的数据和代码都是乱序的,但是因为页表的存在,它可以将乱序的物理内存进行映射,在进程视角中的内存分布(地址空间)就变成有序的。
因为地址空间的存在,每一个进程都认为自己拥有内存的全部空间,并且每个区域都是有序的,进而通过页表映射到不同区域,来实现进程的独立性。每个进程也不知道其他进程的存在,都认为自己独占所有的内存空间。