在正式开始之前我们首先来看这样的一个现象
父子进程(父子进程谁先运行,这个不确定,由调度器决定)更改以前打印的都是同一个地址同一个值这很好理解,子进程在改变了全局变量的值以后,父子进程输出的值不一样了也勉强可以理解,因为进程之间具有独立性。
但是父子进程打印出来的地址居然是一样的,对于一个同一个物理地址来说是不可能有两个值的,就好像南湖广场不可能既有南湖又有北湖。
从上面那个现象可以看出,程序运行时所使用的内存其实并不是物理内存,而是虚拟内存。进程地址空间就是虚拟内存。
一.感性理解进程地址空间
有一个美国富翁家产十个亿,有三个私生子,这个三个私生子都认为自己是这个富翁的唯一继承人。这个富翁和三个儿子说:“我就你这一个儿子,你好好干,以后我这十个亿都是你的。”
这三个儿子不可能独享这十个亿,这只是富翁给儿子们画的饼。
进程不可能独享所有的内存空间,只是操作系统采用进程地址空间的方式给进程画了大饼让每个进程都认为自己独享空间
一个公司中有五百个员工,老板给五百名员工画饼。那么老板必须要记得他给每个员工画的饼是怎么样的,如果今天和昨天画的饼不一致,这个老板可能会失去员工们的信任。所以老板在管理员工的同时,还要管理自己画的饼。
进程要被管理,进程地址空间也要被管理,进程地址空间通过先描述后组织的方法形成一个mm_struct被管理。
我们在C/C++学习阶段的时候,就知道我们定义局部变量就是在栈上开辟空间,malloc/new就是在堆区开辟空间。这些空间也是虚拟的进程地址空间
那么这个虚拟地址空间是如何划分的呢?
在上小学时我有一个同桌,我特别讨厌他,于是我就在桌子上中间画了一条线并告诉这个同桌如果他越线我就揍他。假设这个桌子总长100厘米,也就是说我们各占五十厘米。也就是说他的空间范围是{0,50};我的空间范围是{51,100}
假设每一厘米都是一个地址,那么在他的那五十厘米中,他可以在1-5厘米处放他的铅笔橡皮等,在6-10厘米处可以放他的水杯。
后来有一天我觉得太给他面子了,我就把三八线划到了他桌子边的30厘米处,如果他越线我就揍他。于是他的空间范围变成了{0,30},我的空间范围变成了{31,100}
对于一整块的地址空间,可以通过设定不同的end和start来规定某一个块区域内存放什么数据(类比在桌子某几厘米处放水杯),对于堆栈这种动态生长的空间只要更改strat/end就可以实现区域调整的目的(类比我重新划定38线)。
mm_struct的伪代码如下:
struct mm_struct { uint32_t code_start,code_end; uint32_t data_start,data_end; uint32_t heap_start,heap_end; uint32_t stack_start,stack_end; ······//存储进程地址空间各区域的起始位置 };
二.理性认识进程地址空间
进程之间是共享CPU和主存资源的,但随着对CPU需求的增长,太多的进程需要太多的资源,有些进程就可能会卡死,此外如果某一个进程发生了越界,就会影响到别的进程的存储器。为了更有效的管理和少出错,现代系统提供了虚拟存储器,虚拟存储器为每个进程提供了一个大的,一致的,私有的地址空间,这个地址空间就是进程地址空间。
1.虚拟和物理地址空间
进程是操作系统中最成功的概念之一,每个进程都具有一个私有的虚拟地址空间,它提供一个假象使进程觉得自己独占内存空间。
一个地址空间的大小是由表示最大地址所需要的位数来决定的(现代系统基本为32位或者64位),对于一个32的机器,它有2^32个地址,这些地址都是唯一的,它们能表示的空间范围是4GB。
一个系统还有对应的物理空间,这个不难理解,我们的内存空间就是物理空间,对于一个32位的机器来说,它的物理空间大小就是4GB,也就是说有2^32个物理地址。
但是每个进程都认为自己独享4GB的空间,一个进程的虚拟地址就是4GB的空间范围(我没说大小,因为这个空间是虚拟的),即2^32个地址,但对于32位的机器来说总共的物理地址也就是这么多。而进程往往是多个的,也就是说相同的虚拟地址可能指向不同的物理空间,而不同的虚拟地址也可能指向同一个物理空间。
2.虚拟地址和物理地址联系
a.页表映射
当一个进程被建立,操作系统就会给进程建立对应的进程控制块,这个进程控制块中有一个指针指向mm_struct(进程地址空间)。但进程的数据是被load到内存中的,虚拟内存为了找到物理内存中的数据,就要通过页表的映射。
页表会将虚拟地址中和数据的物理地址建立连接,当需要访问数据时,页表会根据虚拟地址对应的物理地址去物理内存中查找需要的数据。
b.写时拷贝
一块物理空间可以被多个虚拟地址所指向,当这些进程都是对这个物理空间数据只读,那么这个物理空间被这些进程所共享。但是如果一旦有某个进程需要对这个物理空间的数据进行修改,此时就会发生写时拷贝。
所谓写实拷贝就是当某一个进程对共享的物理内存中的数据修改时,操作系统会在物理内存中再开一块空间,并将这个共享的数据拷贝到新开的空间中,提出修改这个共享数据请求的进程的页表映射关系会被改变,最后让进程对数据进行修改。这就是为什么在文章开头的那个程序中会出现同一个虚拟地址中放的却是不同的值。
三.为什么要存在进程地址空间
1.防止越界和恶意程序损害别的进程,保护物理内存。
2.页表不只是建立映射关系,还能在一定程度上拦截不合理的请求,保护物理内存。
3.进程具有独立性,每个进程都有自己独立的进程地址空间,方便进程和进程之间解耦,保证了进程独立性的特征。
4.我们平常所写的代码在加载到内存中就已经有了地址(函数调用就是根据函数的地址找到函数并执行),而编译器使用的就是虚拟地址进行编址的,编译器和进程都遵守这个规则,编址完毕即可使用。