谈程序的地址空间
目录
回顾
虚拟地址
理解区域划分和页表权限
再扩展虚拟内存
回顾
在初开始学习c语言的时候,肯定看到这么个图:
那时候说这是变量存放着的空间分布图。
但其实这只是粗浅的认识,这并不是物理内存,而是虚拟内存,同样的,变量的地址也不是真实的物理地址,是虚拟的地址。
虚拟地址
我们先看现象,之前提到过的父子进程之间,同一个变量,地址一样,但值不一样。
我们可以得出以下结论:
- 变量内容不⼀样,所以⽗⼦进程输出的变量绝对不是同⼀个变量
- 但地址值是⼀样的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做 虚拟地址
- 我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址!物理地址,⽤⼾⼀概看不到,由OS统⼀ 管理
既然有虚拟地址和物理地址,那么OS必须负责将虚拟地址转化成物理地址!
现在,我们来解释解释,直接说概念。
一个进程,有一个pcb,一个虚拟空间(也是一个结构体对象),一套页表。
代码层面理解:
进程task_struct对象里面有指针指向虚拟空间(mm_struct),而页表就是用来映射虚拟地址和物理内存地址的一张表。
我们知道地址占一个字节,在32位机器下,有2^32个地址,也就是说在32机器下,内存有2^32字节,差不多4GB。64位机器下,内存有2^64字节。
现象理解:
父进程myprocess加载到内存之前,就已经有了task_struct,mm_struct,页表,在内存上加载多少字节的代码和数据就要在虚拟内存上开辟多少字节,一个字节就有一个地址,然后页表进行一个个字节地址的映射,创子进程,子进程会将父进程的task_struct,mm_struct,页表全部拷贝一份给自己,此时,父子进程中的g_val的虚拟内存和物理内存是同一份的,当某个进程修改了g_val的值,就会在内存中重新开辟拷贝一份g_val的物理空间,然后这个进程重新在页表中构建映射关系。
同⼀个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
那物理内存中的变量空间是否要是有序的呢?还是无序的?
不管是有序还是无序的都不重要了,因为虚拟内存空间可以将物理内存中的无序经过映射,将无序变成有序!
理解区域划分和页表权限
我们看图可以知道虚拟内存空间上有很多区域,比如栈,堆,代码区等等。
那这是怎么划分的?
进行区域划分,只需要确定区域的开始和结束即可!
在mm_struct中有着多个变量,确定着每个区域的开始和结束。
但是不同的进程,它的代码和数据大小肯定是不同的,那怎么去调整它呢?不可能每个区域都设置成固定大小吧?
在进程去申请各个区域的大小空间时,进行虚拟内存空间初始化,只需要修改各个区域开始和结束即可!(比如++或--,甚至直接修改值)
页表除了有映射关系,其实还有权限。
怎么理解页表权限?
页表权限作用在于可以将用户的地址和操作进行合法性判定,从而保护物理内存!
比如:
野指针,我们知道产生野指针是因为内存没有被释放从而导致内存泄漏,那么我们在访问这个指针指向的空间时,会发现没有这个指针的映射关系。
再看,一个常量字符串,我们进行修改时,程序编译报错:
因为是常量字符串,不能修改,在去查这个指针映射关系准备修改时,页表权限中只有读权限,不能修改,权限被拦截,所以报错!
再扩展虚拟内存
虚拟空间中有很多不同的区域(栈,堆等),每个不同的虚拟内存区域功能和内部机制都不同,Linux内核中采用一个个vm_area_struct结构体管理,因此⼀个进程使⽤多个vm_area_struct结构来分别表⽰不同类型 的虚拟内存区域。
虚拟空间的组织⽅式有两种:
1.当虚拟区较少时采取单链表,由mmap指针指向这个链表; 2.当虚拟区间多时采取红⿊树进⾏管理,由mm_rb指向这棵树。
为什么要有虚拟内存空间?
这个问题其实可以转化为:如果程序直接可以操作物理内存会造成什么问题?
在早期的计算机中,要运⾏⼀个程序,会把这些程序全都装⼊内存,程序都是直接运⾏在内存上的, 也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运⾏多个程序时,必须保证 这些程序⽤到的内存总量要⼩于计算机实际物理内存的⼤⼩。
那当程序同时运⾏多个程序时,操作系统是如何为这些程序分配内存的呢?例如某台计算机总的内存 ⼤⼩是128M,现在同时运⾏两个程序A和B,A需占⽤内存10M,B需占⽤内存110。计算机在给程序分 配内存时会采取这样的⽅法:先将内存中的前10M分配给程序A,接着再从内存中剩余的118M中划分 出110M分配给程序B。
这种分配⽅法可以保证程序A和程序B都能运⾏,但是这种简单的内存分配策略问题很多。
- 安全⻛险。每个进程都可以访问任意的内存空间,这也就意味着任意⼀个进程都能够去读写系统相关内 存区域,如果是⼀个⽊⻢病毒,那么他就能随意的修改内存空间,让设备直接瘫痪。
- 地址不确定。众所周知,编译完成后的程序是存放在硬盘上的,当运⾏的时候,需要将程序搬到内存当中 去运⾏,如果直接使⽤物理地址的话,我们⽆法确定内存现在使⽤到哪⾥了,也就是说拷⻉ 的实际内存地址每⼀次运⾏都是不确定的,⽐如:第⼀次执⾏a.out时候,内存当中⼀个进程 都没有运⾏,所以搬移到内存地址是0x00000000,但是第⼆次的时候,内存已经有10个进程 在运⾏了,那执⾏a.out的时候,内存地址就不⼀定了。
- 效率低下。如果直接使⽤物理内存的话,⼀个进程就是作为⼀个整体(内存块)操作的,如果出现物理 内存不够⽤的时候,我们⼀般的办法是将不常⽤的进程拷⻉到磁盘的交换分区中,好腾出内 存,但是如果是物理地址的话,就需要将整个进程⼀起拷⾛,这样,在内存和磁盘之间拷⻉ 时间太⻓,效率较低。
存在这么多问题,有了虚拟地址空间和分⻚机制就能解决了吗?当然!
- 地址空间和⻚表是OS创建并维护的!是不是也就意味着,凡是想使⽤地址空间和⻚表进⾏映射, 也⼀定要在OS的监管之下来进⾏访问!!也顺便 保护了物理内存中的所有的合法数据 ,包括各个 进程以及内核的相关有效数据!
- 因为有地址空间的存在和⻚表的映射的存在,我们的物理内存中可以对未来的数据进⾏任意位置 的加载!物理内存的分配和进程的管理就可以做到没有关系,进程管理模块和内存管理模块就完成了解耦合。
- 因为有地址空间的存在,所以我们在C、C++语⾔上new,malloc空间的时候,其实是在地址空间上申请的,物理内存可以甚⾄⼀个字节都不给你。⽽当你真正进⾏对物理地址空间访问 的时候,才执⾏内存的相关管理算法,帮你申请内存,构建⻚表映射关系(延迟分配),这 是由操作系统⾃动完成,⽤⼾包括进程完全0感知!!
- 因为⻚表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的 虚拟地址和物理地址进⾏映射,在 进程视⻆所有的内存分布都可以是有序 的。
好了,我们下期见!