目录
前言
不知道大家在学习C语言的时候有没有见过这样一幅图,在学习C语言的时候,你也许经常听到有人说:某某变量开辟在堆区,某某变量开辟在栈区,等等。可是进程的地址空间到底是如何进行分布的,为什么要这样分布,进程与进程之间的地址空间又有什么关系这些也许我们还不甚清楚……
![](https://i-blog.csdnimg.cn/direct/0982644c341e4a49953c811100d2d9f6.png)
一、页表
1.回顾一下fork接口
fork是Linux平台提供给我们的一个接口,这个接口可以基于所处的进程信息帮我们创建一个子进程,子进程会继承父进程的数据。fork会返回一个pid_t类型的值(其实就是int类型),这个值在fork被成功调用的情况下返回0,否则返回一个小于0的值。如果你还想了解更多,可以阅读博主的另一篇文章【点我】
2..先看一个有意思的现象
从图2中我们不难发现一些有意思的事情:
我们不难发现父子进程中的count变量的值不同,但是二者的地址居然是相同的。
(注意:这个现象在Windows平台下不会出现,因为Linux平台与Windows平台对该部分的处理不同)
![](https://i-blog.csdnimg.cn/direct/c9f10c1433464f20bcd00bafd2808c0a.png)
3.地址映射与页表
我们根据上述现象其实不难进行推论,上述代码中显示的地址一定不是真实的物理内存地址,因为我们刻骨铭心的知道,一段地址中指向的一定是一份数据。既然现在一段地址中指向的不是同一份数据,那么这个地址就一定不是物理内存地址,这个地址一定是虚拟的,但是它却可以让我们访问到真真切切的数据,所以它一定跟我们的物理内存地址有联系。
![](https://i-blog.csdnimg.cn/direct/3abf044488a44b3f917a13ef2b4b9b37.png)
实际上,这个联系我们就成为地址映射,而维护这个映射关系的结构就是我们的页表,每个进程都有一个属于自己的页表,页表中的地址都是一对一的,所以一个进程可以通过其自身的页表来实现虚拟地址到物理地址的转化。
![](https://i-blog.csdnimg.cn/direct/dad581ce5ab44ffeb84841d6a452476e.png)
那么图2中的现象和页表又有什么关系呢?
当子进程创建的时候,会继承父进程的数据,而有关父进程的数据的映射关系都会被存于一个表中,这个表就叫做页表,如果进程想要访问数据就必须使用页表的映射关系来找到数据,而后才可以对数据进行操作,子进程在继承父进程的数据的时候也会浅拷贝式的继承父进程的页表关系,当子进程对从父进程继承来的数据进程修改的时候会引发写时拷贝,此时子进程的页表映射关系发生改变,页表的键值不变,但是映射对象发生变化。
![](https://i-blog.csdnimg.cn/direct/9a65d6de59684da991cb79ee6bf77ba0.png)
其实到这里,我们就可以解释就可以解释图2中所展示现象的原因了。
二、进程地址空间
1.进程地址空间是什么
在第一部分中的讨论中,其实我们不难发现,我们在Linux平台中使用的地址都是虚拟的地址,这个管理虚拟的地址的结构我们叫做进程的地址空间,进程的地址空间存储着大量的地址相关的信息,进程地址空间将这些地址信息进行区域划分,每一块地址空间都有它的作用。这些不同的小块组成了我们图1所示的进程地址空间分布图,那么,进程是如何对地址进行划分的呢我们可以浏览一下kenel内核的源码
在kenel2.6.32版本下,进程地址空间这一结构存储在mm_struct中,图中红线框选的就是区域划分相关信息:
![](https://i-blog.csdnimg.cn/direct/7f0f8a3a90fe4d76a25912fd2b3312f7.png)
简单叙述一下划分的方式:我们想要对一块区域进程划分的时候只需要指明这段区域的起始地址与结束地址即可,比如在数学上我们如何划定一份区域,假设我们想要指定一个变量属于0-5这个区间我们可以这样表示,x∈[0,5],在计算机中也一样我们只需要指明开始地址与结束地址,并将这两个地址分别存入两个变量即可,这样一段区域就被划分好了。
那么我们如何实现对不同区域的属性进行规定呢,这就又要提到我们的页表了,页表这一结构不仅存储了地址的映射关系,还存储了,当前映射关系所持有的权限。当然,这并不是页表的全部字段,页表还有数据是否加载到内存,数据是否使用等判别字段。由于与本文关系不大,这里就不过多解释了。
![](https://i-blog.csdnimg.cn/direct/6f615458feba4c6f9b4c2aee2b110007.png)
2.进程地址空间与页表起到了什么作用
①可以让内存管理与进程管理进行解耦
②可以让内存由无序变得有序,方便操作系统进行统一管理
3.进程地址空间有什么作用
进程地址空间是面对用户的,一方面为的是更方便操作系统对进程的管理,另一方面,当用户执行程序时,会先对虚拟地址空间进行权限测试,当此处测试成功之后,操作系统才会移入到物理内存中进行对数据的使用,进而保护了物理内存也保护了操作系统。比如,如果对只读数据区进行写等操作,这时,操作系统会对数据进行检测,此时,权限不足,则操作系统不允许执行。
4.缺页中断与写时拷贝
实际上,当我们的子进程继承来自父进程的页表后,此时的页表权限都会被置成只读属性,当我们对数据进行修改发生写时拷贝的时候,由于没有相应的写权限,这样的操作是不合法的,也是页表中未定义的,这样的设计就是想让强制操作系统来判别这段空间中是否可以执行某一过程,操作系统协调页表中没有的权限并判别该操作是否可以进行的过程就是缺页中断的一种,操作系统判别成功后会清除要修改区域的权限的只读属性,而后开放写属性,数据写入,子进程重新建立映射关系。具体操作参考图5。
![](https://i-blog.csdnimg.cn/direct/e53c1e408f7642e5850d147ac1303b56.png)
5.写时拷贝的相关问题
问:为什么操作系统只在子进程想要发生合理性写操作的时候会对合理数据进行拷贝,而不在子进程创建的时候就把数据拷贝给子进程。
答:操作系统要对计算机的效率和资源负责,如果子进程不需要对数据进行修改,那么操作系统的拷贝操作将会是对时间和空间毫无意义的浪费。
6.关于内存空间申请的讨论
当malloc/new申请堆空间的时候,申请出来的空间,我们不一定会立即对资源进行使用,所以操作系统也会如处理何时拷贝数据那样,当你的程序需要使用开辟的空间时,操作系统才会为用户开辟空间,这样做的好处有两个:
①防止资源出现空转
②提升malloc/new的运行速度
第一点比较好理解,如果操作系统在程序申请空间时,立即为程序开辟空间,假如,程序没有立即使用,那么该程序就会保持对资源的占有而不利用,造成资源空转。
第二点,操作系统会将mallo/new的开辟过程分为两个阶段,第一个阶段是在虚拟地址空间上,为malloc/new开辟空间,当进程要对该地址进行写入时,此时由于操作系统还没有为进程开辟物理内存空间,所以页表映射不完全,发生缺页中断,此时操作系统会立即为进程分配物理内存空间,并建立好页表的映射关系。而这一过程是无论如何都需要进行的,操作系统将这整个过程分为两个阶段就可以提升new/mallo开辟空间的效率,就可以提高进程整体的运行效率和资源使用率。