目录
前言:
1.什么是地址空间
区域划分
页表:
2.为什么要有地址空间
2.1 进程与内存解耦合
2.2安全
3.凭什么说进程具有独立性:
4.用地址空间解释一下申请内存
前言:
在C语言中,我们说我们将内存分为,栈区,堆区,静态区。栈区存放形式参数和临时的数据变量,堆区主要是用于开辟内存,静态区是存放全局变量,和static修饰的变量的。也说了内存的使用是从低地址到高地址,今天我们也可以一一来验证一下:
首先对于栈区堆区和全局变量的地址验证:
*int g_unval;//未初始化的变量
48 int g_val = 100;//初始化的全局变量
49 int main(int argc,char*argv[],char*env[])
50 {
51
52 printf("code addr: %p\n ",main);//main函数地址
53 printf("Initdata addr: %p\n ",&g_val);
54 printf("Unitdata addr: %p\n ",&g_unval);
55 //验证堆区地址
56 char*heap1 = (char*)malloc(20);
57 char*heap2 = (char*)malloc(20);
58 char*heap3 = (char*)malloc(20);
59 char*heap4 = (char*)malloc(20);
60
61
62 int c = 0;
63 printf("heap1 addr:%p\n",heap1);
64 printf("heap2 addr:%p\n",heap2);
65 printf("heap3 addr:%p\n",heap3);
66 printf("heap4 addr:%p\n",heap4);
67 //验证栈区地址
68 printf("stack addr:%p\n",&heap1);//变量在栈区上定义
69 printf("stack addr:%p\n",&heap2);//变量在栈区上定义
70 printf("stack addr:%p\n",&heap3);//变量在栈区上定义
71 printf("stack addr:%p\n",&heap4);//变量在栈区上定义
72 printf("c addr:%p\n",&c);
验证命令行参数和环境变量所以堆栈是相对而生的
74 int i = 0;
75 for(i = 0;argv[i];i++)
76 {
77 printf("&argv[%d] = %p\n",i,argv+i);//命令行参数
78
79 }
80 //环境变量
81
82 int j = 0;
83 for(j = 0;env[j];j++)
84 {
85 printf("&env[%d] = %p\n",j,env+j);//命令行参数
86
87 }
88 //环境变量
89
90
91 return 0;
92
两张表,先有命令行参数表才有了环境变量表
换成argv[i]看环境变量字符串和命令参数字符串在那个位置:
int i = 0;
75 for(i = 0;argv[i];i++)
76 {
77 printf("argv[%d] = %p\n",i,argv[i]);//命令行参数
78
79 }
80 //环境变量
81
82 int j = 0;
83 for(j = 0;env[j];j++)
84 {
85 printf("env[%d] = %p\n",j,env[j]);//命令行参数
86
87 }
结论:无论是表还是表指向的项目,都在栈上部
未初始化数据和已初始化数据在进程中一直存在,我们现在创建一个变量
加上static 修饰
62 static int c ;
63 c = 0;
static 定义的变量,不随着函数调用释放,只初始化一次,语言上已经是全局的了,不随着函数调用而释放。即使不初始化也是会初始化为0
以上的测试结果基本和第一张图是保持一致的,但是 这套规则在vs下可能不一样,在linux中是一样的。我们还能发现,在linux中我们每次运行对于同一个量,其地址是一样的,但是在vs中不同
地址为什么一样?vs下不一样
我们现在只能说:一样不一样对于linux来说无所谓,windows有所谓,差别大,windows防止代码或者数据,被固定编制到其他地方,做了很多策略,地址随机化就是一种,也就是说是出于安全考虑,而linux的做法优雅得多。(vc6.0打印出来是一样的。)
现在有这样一个问题:
上面这张图是内存吗?
我们现在来写这样一段代码来看一下一个现象:
int g_val = 100;
17 int main()
18 {
19 pid_t id = fork();
20 if(id == 0)
21 {
22 //子进程
23 int cnt = 0;
24 while(1)
25 {
26 printf("child,pid :%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
27 sleep(1);
28 cnt++;
29 if(cnt == 5)
30 {
31 g_val = 200;
32 printf("child change g_val: 100->200");
33 }
34 }
35
36 }
37 else{
38
39 while(1)
40 {
41 printf("father,pid :%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
42 sleep(1);
43 }
44 }
45 return 0;
46 }
内容不一样,地址一样?
同一个地址打印出不同的值,父子进程分别看值不一样吗?
这个地址,绝对不是物理地址,因为如果是一个物理地址绝对不会拿出两个值。
现在只能说这个地址叫做虚拟地址或者叫做线性地址,更为关键的是,我们都是取语言级别的地址,那么也就是所说我们C语言c++打印出来的都不是物理地址而是虚拟地址或者线性地址。
打印出这么地址都是上述图片结构当中对应的地址,这个地址空间排布的情况不是物理内存,正确的叫法为:进程地址空间
开启正文:
不管是C语言还是c++程序变成进程,每一个进程都存在一个进程地址空间。所以这个和语言没有多少区别。C语言和c++要遵守这样的规则是系统决定的,我们的虚拟机也是进程,也要遵守规则。
1.什么是地址空间
程序运行时,每个进程都要有自己的pcb,都会有自己的进程地址空间,对于这个进程的数据肯定是存放在物理内存中的,但是,我们的进程运行时如何拿到或者访问到这块内存,拿到数据,我们在操作系统内部还存在一种映射关系,大家可以理解为一张表,这个映射表,会将虚拟地址和物理地址建立一个映射。
这是一个进程,子进程也是会拥有同样的配置。同样的子进程将相同的映射关系也继承了下来:
所以子进程指向同一个物理地址,所以虚拟地址打印也会是一样的。 然后我们的子进程对数据进行修改,但是进程具有独立性,两个进程指向同一个内存空间,所以操纵系统会在物理内存中再给子进程开辟一块空间。让后子进程修改数据就不影响父进程,最后再修改一下映射关系。
此时子进程就只想自己的变量内存空间,打印的地址是虚拟地址,所以打印的地址一样,但是由于映射关系不同,父子进程拿到了不同的值。
本质原因就是:虚拟关系对应不一样
当时我们说fork函数的返回值对于父子进程不一样的原因也就是:
fork分流的根本原因,同一个虚地址,不同的对应关系
每个程序运行时都有自己的pcb,每一个进程都会有自己对应的地址空间。然后,进程地址空间在32位操作系统下为0到4gb,那么我们就要来讲一下什么是地址空间:操作系统给每个进程画的饼,每个进程一个饼,饼也需要管理 ,操作系统也要对进程地址空间进行管理,如何管理:新描述再组织
所以什么是进程地址空间:进程地址空间是是数据结构,具体到进程中,就是特定的数据结构对象。
struct 进程地址空间
{
//进程地址空间的属性
struct *next
}
所以我们的进程pcb中肯定还会存在一个这样的指针来指向程序的进程地址空间:
struct tast_struct
{
struct 进程地址空间* p;
}
所以进程和进程地址空间之间的关系就是数据结构之间的关系。
那么进程地址空间中都有什么呢?
区域划分
我们原先说这个空间分为栈区,堆区,静态区等等,那么又如何理解这些区域呢?
同桌之间会划线,三八线本质是区域划分,如何用计算机语言描述区域划分的工作:
struct area
{
int start;
int end;
}
struct destop_area
{
astruct area xiaopaamh ara
struct xiaoaua_area;
}
struct destop_area = {{1,50},{51,100}}
这样就将长度为100的空间进行了划分,为了更好表述,结构体可以这样书写:
struct destop_ara
{
int person1_start;
int persosn1_end;
int person2_start;
int person2-end;
}
可以实现这样的划分:
struct deatop_area ={1,50}{51,100}}
怎么判断越界:有了划分,可以随时判断越界。你放东西放到另外区域内很容易判别,因为区域是一个范围。
怎么扩大区域划分,怎么扩大自己的区域划分
person1_end -=20;
area.person2_star +=20
所以,如果我们设计进程的地址空间,进程地址空间的栈区啦,堆区啦,静态区等等都是一个区域,所以该地址空间中必须有的字段是:整数描述的各个区域
struct xxx
{
int code_strt,code_end;
int init,intit end;
int heao_start,heap_end
}
我们来看一下linux内核的源代码看一下他的进程地址空间的实现方式:
存在大量的sart end,代码从哪里开始,到哪里结束,所以进程的地址空间是 进程的一种数据结构,地址空间里将范围划分好了,进程创建时,进程地址空间就被创建出来,每个字段被初始化,所以进程的视角就有区域的概念了。
页表:
理解一下空间划分:空间划分不仅仅限定一个区域,而是区域划分本质为区域内的各个地址都可以使用,每一个地址都可以被进程使用,但是我们的地址空间不具备对我们的代码和数据的保存能力,是在物理内存中存放的那么现在需要一种机制,将地址空间上的地址(虚拟地址)转换到物理内存,操作系统给我们的进程准备一个映射表:我们称为:页表
当可执行程序是一个文件存放在磁盘中,双击运行变成一个进程,操作系统要为这个进程创建pcb,在创建进程的地址空间,然后要将进程的数据和代码加载到实际的内存中进程怎么找到代码和数据,此时需要页表,页表左侧叫做虚拟地址,右侧叫做物理地址,每行代码都有地址,都会经过页表通过虚拟地址转换映射到实际地址,最后找到相应的代码和数据
这个转化工作是由cpu做的,当cpu读取正文代码时,cpu内有一个寄存器cr3:cr3寄存器保存页表的起始地址,cpu读取指令时,指令里面比如对变量进行++,要读取变量的内容拿到的是虚拟地址,然后通过页表找到实际内容,实际上这个转换工作,是用MMu来做的,叫做内存管理单元,是集成在cpu上的,所以映射,查找等工作是由硬件自动完成的。
MMu
MMU是Memory Management Unit的缩写,中文名是内存管理单元,有时称作分页内存管理单元(英语:paged memory management unit,缩写为PMMU)。它是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件。它的功能包括虚拟地址到物理地址的转换(即虚拟内存管理)、内存保护、中央处理器高速缓存的控制,在较为简单的计算机体系结构中,负责总线的仲裁以及存储体切换(bank switching,尤其是在8位的系统上)
虚拟地址是给用户看的,cr3保存的地址是物理地址,页表也是一种数据结构,我们的操作系统也要对其进行管理,也是需要保存的,虚拟地址是给进程的,进程是用户的,自己用的是物理内存
所以为什么fork过后父子进程的代码一样,是因为页表是继承来的,页表也是一样的。关于页表我们后续还会慢慢补充。
2.为什么要有地址空间
为什么要存在转化,直接访问地址不好吗?
2.1 进程与内存解耦合
因为有了映射的存在,左边叫做进程管理,右边叫做内存管理,将来对应的程序
可不可以将磁盘的数据在内存中任意加载呢,也就是说操作系统给进程申请内存空间不用考虑进程的感受,只用建立好映射。
因为有地址空间和页表存在:可以将物理内存从无序变为有序,也就是说页表左侧有序,右侧随便放,那么进程就可以以统一的视角,看待整个内存。而同时可以做到进程和内存互不干扰。,将内存管理和进程管理解耦合,这两部分代码可以没有直接关联。 方便操作系统设计
2.2安全
.除了正常转化,对非法请求进行拦截
当进程想要访问内存中的某个数据,要是请求合法就正常访问,如果要是访问不正常就拦截,拦截非法请求。所以地址空间+页表是保护内存安全的重要手段。(指针越界崩溃,不影响操作系统和其他进程)转换工作是cpu+MMU共同完成
3.凭什么说进程具有独立性:
进程=内核数据1结果+自己的代码,即使是父子进程,内核数据结构各自独立,代码可以共有,因为是只看,数据通过写实拷贝的方式各自拥有一份。独立性得到保证。我们删除增加一个进程也不会影响其他进程
进程拥有:进程的pcb,进程的地址空间;进程的页表,进程的映射关系,自己的代码和数据
4.用地址空间解释一下申请内存
解释:内存申请
我们喜欢在C语言中使用malloc等申请内存
申请内存立马使用吗?
.申请的内存本质在哪里申请呢?
操作系统内部有很多进程,如果进程申请内存不立马使用,如果内存分配给你,时间在走,这块内存操作系统也拿不走,但是进程短时间不使用,我们的操作系统要为效率和资源使用率负责,操作系统必须确保用户要用,再给用户,所以我们经常使用的malloc,reallloc等函数申请内存并不是直接就在物理内存上去开辟空间,用户拿到的是虚拟地址,用户拿到虚拟地址过后,页表不会建立映射关系,因为操作系统不会立马将内存资源给这个进程,当用户尝试通过虚拟地址进行写入的时候,写入的时候,当前访问地址空间合法,二页表没有建立映射,操作系统此时就会将用户写入暂停,然后在物理内存上开辟空间,建立映射,此时这个内存是立马就给用户,所以地址空间就像一张支票。这样做,我们可以充分保证我们内存的使用率,用户要用再给,这个操作不会慢,时因为,不论是立马给还是用户要用的时候给都设计到扩大我们的那个进程地址区间和内存开辟以及建立映射关系,立马给和用才给无非就是将这几件事一次性做完和分开做完的确保,时间成本是不变的,但是带来的好处new 、malloc速度会变快。操作系统的整体效率整体提升。
我们申请了空间,当我们尝试写入的时候,访问合法,页表没有建立关系,操作系统将用户拦截住,然后到物理内存开辟内存,建立映射关系,这个过程我们叫做缺页中断。
在l不管是什么语言,要在内存中跑起来,都要变成进程,都要能够以进程地址空间的形式出现,任何语言都有,这个图出现最多在c类语言,是编译语言,编译形成什么样子,也就是说我们写的代码就是cpu要执行的代码,但是有些语言它是在语言层给我们做了很多分装,叫做解释型或半解释型语言,python没有编译器只有解释器,java有虚拟机,这些都是c语言写的,也会有自己的虚拟地址空间。只要在系统里面,誰都一样。