内存工作原理:分段、分页和虚拟内存
1、分段
段的定义:把程序与其相关的数据划分到几个段中,比如数据段、代码段和堆栈段。段的长度有限制,但是不要求所有程序的所有段长度都一样。
分段:一个程序的所有段被装入内存中时,同样不要求这些段是连续的。分段类似于动态分区,当进入放入内存中时,系统会为这些段分配容量相等的内存空间,随着大小不同的段被放入内存,渐渐会产生外部碎片,即段外部的内存小碎片。如图,放了进程1、2、3之后内存剩余4MB,很难再放入其他段了,这就是外部碎片。
段表:段表中的每一项表示一个段,由于段的长度可以不同,所以段表项不仅要给出这个段在内存中的起始位置,还要给出该段的长度。
地址转换:采用分段技术时的逻辑地址也是由段号和偏移量组成。以段号为索引,查找段表中的对应项,找到这个段的起始内存地址,再判断偏移量是否超出了该段的长度,超出则地址无效。物理地址就是该段的起始地址与偏移量之和。
2、分页
页的定义:内存被划分成大小固定、相等的块,且块比较小,每个进程也被划分成一个个这样的块,进程中的块称为页(page),内存中的可用块称为页框(frame),进程中的页可以被装载到内存的页框中(类似把照片放入相框)。
分页:操作系统以页的方式,将进程的一部分换入内存或者换出到外存。分页不存在外部碎片,进程在被划分成页时,仅进程的最后一页可能会出现未使用的空间,即内部碎片。
页表:一个进程的所有页在内存中可以不是连续存放的,操作系统为每个进程维护一个页表,页表中每一项代表一页,每个页表项指明该页对应的页框地址(物理地址)。
地址转换:逻辑地址是程序中的某个字相对于程序开始处的位置,采用分页技术时的逻辑地址是由页号和偏移量组成的,偏移量指的是这个字距离该页起始位置的偏移距离。通过地址翻译硬件和页表,可以将逻辑地址(页号,偏移量)转换为物理地址(页框号,偏移量),第4部分结合虚拟内存说明了逻辑地址的翻译方式。
上图中,内存被划分成一个个页框,进程被划分成一个个页,图(b)将进程A装入内存,接下来装入进程B和C,换出进程B,图(f)装入进程D的时候,该进程的页没有被连续存放。
进程A的页表中每一项表示了该页对应的页框号,也就是该页在内存中的起始位置。
比较分段和分页:
- 原来的程序都是占用很长一段连续的内存,直接操作这个物理内存,对于编程比较麻烦。分段从程序执行的角度,把程序拆分成一个个模块,比如代码段、数据段、堆栈段等,这样进程在内存中的布局更加清晰。
- 分段提供程序的保护功能,段表中指示了段的长度,如果逻辑地址的偏移量超过了该段的长度,则会被限制访问。
- 分页建立在分段的基础上,继续把段内的内存进行划分,方便操作系统的管理,页对于用户是透明的。分页没有外部碎片,分段外部碎片比较多。
3、虚拟内存
由于进程只能在内存中执行,因此这个存储器称为实存。但用户或程序员可以感觉到的是一个更大的内存,分配在磁盘上,这就是虚拟内存。内存可以说是磁盘的高速缓存,任何时刻,任何一个进程只有一部分位于内存中,基于分段、分页技术,在需要的时候可以将进程的块(页或段)换入内存或者换出到磁盘,这样可以在内存中保留更多的进程。
局部性原理表明,在很短的时间内仅需使用进程的一部分块,因此虚拟内存方案是可行的。通过对将来要访问的块进行良好的预测,还可以减少块的频繁换入换出(“抖动”)。
4、地址翻译
采用虚拟内存技术时,所有的地址访问都是逻辑访问,在运行时转换为真实的物理地址。使用MMU(内存管理单元)中的地址翻译硬件和该进程的页表,可以将进程中某个字的逻辑地址转换为物理地址。
翻译过程:
CPU生成一个虚拟地址,0 ~ p-1位为页偏移,页的大小即 2 p 2^p 2p,p ~ n-1位为虚拟页号,与页表索引相对应。
在页表中找到对应的行(页表条目)后,如果有效位为1,表示页面的内存中,可以使用页表条目的物理页号,物理页的偏移量与虚拟页的偏移量相对应。物理页号和物理页偏移量拼接成物理地址。
如果有效位为0,则表示页面不在内存中,也就是缺页了。此时会调用内核的缺页异常程序,该程序需要先淘汰内存中的一个页。页表项中另一个控制位是修改位,它表示这个页被装入内存后是否被修改过,如果被修改过则属于脏页,那么内核会将其写回磁盘,然后再淘汰它。接下来,内核从磁盘复制所需的页面到内存中,更新页表中的对应条目,随后返回。
当异常处理程序返回时,会重新启动导致缺页的指令,然后将原先那个导致缺页的虚拟地址重新进行翻译。此时,将会按照有效位为1进行后续处理。
翻译后备缓冲区(Translation Lookaside Buffer, TLB):很多系统,在MMU中加入了一个缓存页表条目的翻译后备缓冲区(TLB),这样可以快速地获取页表条目,整个地址翻译步骤都是在硬件上完成,非常快。
虚拟内存与高速缓存交互:使用虚拟内存技术,获得某个字的物理地址后,首先查看高速缓存中是否存在包含这个字的块(页或段),如果有,则从高速缓存中取得,返回给CPU;如果没有,则老老实实从内存中取。
5、虚拟内存的内存管理
1)简化链接。操作系统为每个进程提供一个独立的页表,因此,每个进程都有一个独立的虚拟地址空间。独立的地址空间允许每个进程的内存映像都使用相同的基本格式,不用管代码和数据实际存放在哪个物理位置。这样的操作极大地简化链接器的设计和实现,允许链接器生成完全链接的可执行文件,这些可执行文件独立于最终的物理位置。
比如,linux系统上每个进程的内存格式基本类似,64位地址空间中,代码段都被放置在0x400000开始的位置,然后是数据段,高地址是栈空间,向下生长。
2)简化加载。要把可执行目标文件的代码和数据加载到一个新创建的进程中时,加载器无需从磁盘复制任何数据到内存中。实际上,linux加载器只为代码和数据段分配虚拟页,将页表条目的有效位设置为0(即未缓存到内存),再指向磁盘目标文件中对应的.text节(存储代码)和.data节(存储数据)。虚拟操作系统在CPU取指令时会自动调入代码页,在某一指令要引用某个内存位置时自动调入数据页。
下图是一个可重定位的目标文件的基本格式。其中,.text包含编译完成的机器代码,.rodata包含只读数据,.data包含已初始化的全局和静态C变量(局部变量运行时保存在栈中),.bss包含未初始化的全局和静态C变量以及所有仅被初始化为0的全局或静态变量(在目标文件中未初始化变量不占用任何磁盘空间,运行时刻,这些变量的初始值都为0),.symtab包含一张符号表(存放了程序中定义和引用的函数和全局变量的信息),.rel.text包含一个.text节中位置的列表(在链接时,如果本模块调用了外部函数,需要修改这些位置),.rel.data包含了本模块引用或定义的所有全局变量的重定位信息(如果已初始化的全局变量,它的初始化值是一个全局变量地址或者外部定义函数的地址,都需要修改)。
3)简化共享。虽然进程之间大部分地址空间都是私有的,但还是有一些代码和数据是可以共享的,比如操作系统的内核代码,标准库中的函数等。操作系统让不同进程的虚拟地址都映射到这些需要被共享的相同的物理页上,这样可以使得这部分代码和数据在内存中只保留一份。
4)简化动态内存分配。当程序需要在堆上动态分配内存时,操作系统只需要分配所需的k个连续的虚拟内存页面,并将其映射到内存中任意位置的k个任意的物理页面,这k个页面可以分散在物理内存中。
6、小结
虚拟内存提供了三个重要能力:
1)将内存作为磁盘的缓存,内存中只保存程序活动区域。根据需要,在磁盘和内存之间来回传送数据。这种方式高效地使用了内存。
2)为每个进程提供了一致性的地址空间,简化了内存管理。
3)保护每个进程的地址空间私有化,不被其他进程破坏。