1 物理内存
1.1 物理内存概述
物理内存由多个连续的存储单元组成,每个单元称为一个字节
每个字节有一个唯一的物理地址(Physical Address,PA),地址编码从0开始
在早期的体系结构中(e.g. X86实模式),程序直接使用物理地址。也就是说,程序中每个数据存储在内存中的位置,都由程序员负责
1.2 直接使用物理内存的问题
1.2.1 多进程地址布局困难
由于系统中存在多个进程,每个进程分配多少内存、如何保证指令中访问内存地址的正确性并且与其他进程不冲突,都需要程序员完成。相当于将linker和loader的工作全部交给程序员手动完成
1.2.2 进程地址空间小
由于是多个进程共享物理内存,所以需要对每个进程可使用的物理内存区域进行分配,因此每个进程的地址空间都很小(最大也不会超过当前可使用的物理内存)
1.2.3 程序链接不统一
对于程序而言,链接地址和运行地址需要一致,才能确保程序中的地址相关操作正确执行
由于进程被分配在不同的物理地址运行,所以在链接时需要指定相应的链接地址。或者说,程序需要加载到链接地址处运行
说明:上述情况是针对没有分段机制的处理器而言的,对于有分段机制的处理器(e.g. X86),程序可以使用统一的链接方式(e.g. 程序中的每个段都从0地址开始链接),此时程序中使用相对于段起始位置的偏移地址而不是使用绝对地址
在加载程序时,将程序中不同的段加载到当前空闲的物理内存中,之后用段寄存器记录段的物理起始地址,即可以实现程序的重定位
相关内存可参考X86汇编语言从实模式到保护模式01:处理器、内存和指令麦小兜的博客-CSDN博客 chapter 2,相关加载器的实现可参考X86汇编语言从实模式到保护模式07:硬盘和显卡的访问控制麦小兜的博客-CSDN博客
2 虚拟内存
2.1 引入虚拟内存的目的
虚拟内存的出现,就是为了解决直接使用物理内存的问题
使得每个进程有不依赖物理内存大小的虚拟地址空间,这个空间可以比可用的物理内存大得多
使得每个进程的虚拟地址空间是私有的、独立的,与其他进程的虚拟地址空间相互隔离,这就解决了多进程之间地址冲突的问题
由于虚拟地址空间是进程独占的,可以任意使用,因此可以将变量和函数分配地址的任务交给链接器自动安排
也就是说,每个进程的虚拟地址空间布局是相同的,因此链接器可以按统一的方式链接不同程序
2.2 局部性原理与虚拟内存
处理器和操作系统是基于局部性原理(Principle of locality)为程序员虚拟化了一层内存,也就是虚拟内存。局部性原理有如下2方面,
时间局部性:被访问过一次的内存地址很可能在不远的将来会被再次访问
空间局部性:如果一个内存地址被访问过,那么与他临近的地址在不远的将来也很可能会被访问
从局部性原理就可以得出结论:无论一个进程占用的内存资源有多大,在任一时刻,他需要的物理内存都是很少的
在这个推论的基础上,处理器只要为每个进程保留很少的物理内存就可以保证进程的正常执行
2.3 虚拟内存到物理内存的映射
2.3.1 概述
任何虚拟内存中的的数据,最终还是要保存在真实的物理内存中。也就是说,虚拟内存需要映射到物理内存
虚拟内存的大小远大于物理内存的大小
基于局部性原理,处理器和操作系统只将当前使用的虚拟内存映射到物理内存
2.3.2 页面分配与映射
首先,虚拟内存与物理内存的映射以页为单位,常见的页大小为4KB
虽然虚拟内存提供了很大的地址空间,但是在进程启动后,这些空间并不是全部被使用,而是处于未分配状态
当程序中通过malloc等内存分配接口获取内存时,相应的虚拟内存页面将从未分配转变为已分配但未映射状态
当对这段分配到的虚拟内存进行读写时,操作系统才会在缺页异常中为他们分配物理内存,当映射关系建立后,该页面转变为正常页面
可见虚拟内存实现一个假象,让程序员觉得整个虚拟内存空间可以随时访问,但真实的数据可能不在物理内存中,而是在需要用到时才被加载到内存中,并建立虚拟内存到物理内存的映射
说明1:虚拟地址中的"虚拟",是指不存在但能看见
① 本质上,虚拟地址是一套虚拟内存分配与映射机制,真正操作的还是物理内存。所以说虚拟内存本身是不存在的
② 但是对程序员而言,能直接操作的就是虚拟地址,因此是能看见的
说明2:引入虚拟内存后,分配内存分为2级
① 首先是在进程的虚拟地址空间中分配虚拟内存
② 之后是分配物理内存,并建立虚拟内存与物理内存的映射关系
分配物理内存被推迟到对虚拟内存的访问触发缺页异常时才进行,实现了按需加载
说明3:当虚拟内存已分配但未映射时,他所对应的数据在哪里?
① 可能在磁盘上,比如文件
② 也可能是申请但未访问的内存,比如malloc分配一个数组
说明4:在虚拟内存中连续的页面,在物理内存中不必是连续的。只要维护好从虚拟内存到物理内存的映射关系,就能正确使用内存
说明5:虚拟地址空间的大小
虚拟地址空间的大小一般由处理器字宽决定,
① 对于32位处理器,寄存器是32位的,可以存储32位指针,因此能表示的地址范围为0 ~ 4GB
② 对于64位处理器,寄存器是64位的,可以存储64位指针。但是一般并不实际使用全部位数,比如只使用低48位,此时的虚拟地址空间为256TB
说明6:有不遵循局部性原理的程序吗?
① 这种局部性不好的程序是存在的,他会导致处理器频繁进入缺页异常,为其分配物理内存,从而影响性能
② 对于这种程序,在物理内存足够的情况下,应该让其使用的数据尽可能驻留在内存中
③ 可以在该程序启动时,就分配虚拟内存,并且对分配的虚拟内存空间进行一次访问,强制将未映射的页面转变为正常页面,从而降低缺页异常发生的概率
上述过程通常称为内存的commit
2.4 页表结构
2.4.1 概述
上述虚拟内存和物理内存的映射关系由操作系统管理,而管理这种映射关系的数据结构就是页表
映射的过程由处理器的内存管理单元(Memory Management Unit,MMU)自动完成,但是他依赖操作系统设置的页表。因此,虚拟内存是软硬件一体化设计的典型代表
2.4.2 页表
页表本质上是页表项(Page Table Entry,PTE)的数组,虚拟地址空间中的每个虚拟页在页表中都有一个PTE与之对应
以X86-32体系结构为例,页大小为4KB,每个页表项4B,因此将1024个页表项组成一张页表。这样一张页表的大小正好是4KB,占据一个内存页
而一张页表的1024个页表项能够映射1024 * 4KB = 4MB内存
2.4.3 页目录表
如上文所述,一张页表可以映射4MB内存。为了编码更多地址,就需要更多页表。因此引入了页目录表
页目录表中的每一项叫做页目录项(Page Directory Entry,PDE),每个PDE都对应一张页表
每个页目录项也是4B,因此将1024个页目录项组成一张页目录表,可以映射1024 * 4MB = 4GB内存,可以覆盖32位处理器的虚拟地址空间
说明:使用多级页表结构,而不是使用单级页表的原因,可参考X86汇编语言从实模式到保护模式19:分页和动态页面分配麦小兜的博客-CSDN博客汇编page chapter 2.4.3
2.4.4 虚拟地址翻译过程
以X86-32体系结构为例,使用两级页表,并将虚拟地址划分为3段,分别作为页目录表索引、页表索引和页内偏移量
虚拟地址的翻译过程如下,
- 确定页目录基址
每个CPU都有一个页目录基址寄存器,记录最高级页表的物理基地址。在X86-32体系结构中,就是CR3寄存器
- 定位页目录项(PDE)
页目录物理基址 + 虚拟地址高10位 * 4 = PDE物理地址
页目录项(PDE)中记录了页表的物理地址
- 定位页表项(PTE)
页表物理基址 + 虚拟地址中间10位 * 4 = PTE物理地址
页表项(PTE)中记录了物理页的物理地址
- 确定物理地址
以虚拟地址低12位作为物理页中的索引
说明1:页表项(PTE)中处理记录物理页的物理地址外,还记录了一些属性(e.g. 页的读写权限、标记页是否存在),操作系统可以基于这些属性实现内存保护
说明2:每个进程都有自己的页目录表,当进程切换时,会将目标进程的页目录表物理地址加载到CR3寄存器。所以在任意时刻,只有一个进程页表是活跃的
说明3:在64位处理器上,由于虚拟地址空间更大,需要更多级的页表
以X86-64体系结构为例,PDE和PTE都是8B,所以一页之中只能存放512项,需要9位进行编码。所以虚拟地址会被分割为9 + 9 + 9 + 9 + 12共4段,而页表也是4级
由此可见,页表的级数与虚拟地址的分段是匹配的
3 页面换入换出
由于程序运行符合局部性原理,对于那些没有被经常使用的内存,可以将他们换出到内存之外,比如磁盘的swap分区
当物理内存足够时,操作系统会让尽可能多的页驻留在物理内存中,因为将内存中的数据写入磁盘是非常耗时的操作
说明1:极端情况下,给一个进程4KB物理内存就可以。操作系统通过对一页不断的换入换出,使得进程可以运行
说明2:虚拟内存会耗尽吗?
① 虚拟内存也是会耗尽的,也就是out of memory错误。以Linux 32位操作系统为例,用户空间只有3GB,如果程序中一次性要申请4GB内存,则虚拟内存不足
② 虽然有了虚拟内存,但是物理内存还是会耗尽的。当物理内存不足时,可以将不活跃的物理页换出到磁盘的swap分区。当swap分区耗尽时,物理内存就耗尽了
所以任何时候,及时释放申请的内存是一个好习惯
引用(本文章只供本人学习以及学习的记录,如有侵权,请联系我删除)
编程高手必学的内存知识01:深入理解虚拟内存