1. 为啥要有虚拟内存管理
当前的处理器都多用户多任务的,同时运行着很多进程。
- 如果每个进程都直接访问物理内存,这样就要求程序员增加管理物理内存,以避免多个进程访问同一块物理内存,同时程序员直接访问物理内存,这样会造成可以随意修改别人的东西,编码困难,安全完全无法得到保证。
- 多用户的情况,经常会出现一个程序的多个实例,这种情况怎么解决呢? 就完全没有办法了
- l同时注意当前我们的程序都非常之大,占用内存都非常多,一台机器跑的进程又太多
针对上述问题,肯定无法通过直接有限的物理内存来实现。
2. 虚拟内存
2.1 什么是虚拟内存?
系统侧帮我们解决了,系统帮我们动态分配和释放物理内存。程序运行时,给每个进程虚出一整块虚拟内存,而且可能比实际物理内存还大,让我们认为每个进程自己独占所有的物理内存,我们只管写代码,申请和使用内存,而不用管内存是否足够(当然也不能这么任性)。让程序员认为所有内存都是自己的。
今天我们以32位系统为例,系统为每个进程虚拟出4G内存空间,其中3G为用户空间,1G内核工具大家共享。
于是这样就解决了我们之前提出来的前两个问题。
2.2 虚拟内存的结构
每个进程都有如下图一样的虚拟内存空间。
从下往上分别为代码段、bss段、数据段、堆、栈……
注意不是从0地址开始存储的,是因为如果所有的进程都从0地址开始存储,那是不是非常容易攻击。所以系统让起始地址取随机。
2.3 虚拟内存的实现
进程要运行在物理内存中,现在每个进程都有4G内存,实际上不可能有那么多内存。
注意:我们一直说的都是虚拟内存,也就是说骗你的,你根本没有那么多内存,当然也不能让你知道是骗你的。
怎么骗你的:操作系统中每个进程都有一个内存描述符结构体(mm_struct)里面记着你的内存结构情况,实际上就是欠你的内存,——账本。只有你真的要用的时候才会真正给你分配物理内存,不用时就用磁盘空间保存,比如一些文件资源,只给你记账了,实际上还没分配呢。比如malloc比较大块的内存时,当不使用时,很可能是没有分配真正物理内存的,真正对他读写时才会真正分配物理内存。同时会出现这种情况,多个进程使用相同的只读动态库,也操作系统让大家使用同一段物理内存,于是这不就节省了内存空间吗?于是这样就解决了我们之前提出来的第三个问题
3. 虚拟内存的映射
最终运行的时候还是要在物理地址上的,系统通过段机制和页机制来实现虚拟地址到物理地址的映射的。因为linux简化了段机制,本文就不讲段机制了,只简述下页机制。
3.1 页机制
3.1.1 几个概念
页:系统将虚拟内存分成4K大小的块。每块就是一页,作为内存管理的最小单位。每个页都会有一个编号,从小到大。
页框(块):系统将物理内存也和虚拟内存一样分成4K大小的块,每块就是一个页框,与页一一对应。
地址结构:分页存储管理的逻辑地址结构如图前一部分为页号P,后一部分为页内偏移量W。地址长度为32 位,其中0~11位为页内地址,即每页大小为4KB;
页表:为了便于在内存中找到进程的每个页面所对应的物理块,系统为每个进程建立一张页表,记录页在内存中对应的页框号,页表一般存放在内存中。在配置了页表后,进程执行时,通过查找该表,即可找到每页在内存中的物理块号。可见,页表的作用是实现从页号到页框号的地址映射。
3.1.2 虚拟地址到物理地址映射
如下图,虚拟地址的各个页通过页表找到具体的物理页框,也就找到了真正的物理内存。同时可以看到在虚拟地址空间中连续的空间,实际上在物理上不连续的。这样就使碎片化内存得到利用,使内存的利用率变得更高。
3.1.3 二级页表
分页管理,进程在执行时不需要将所有页调入内存页框中,而只要将保存有映射关系的页表调入内存中即可。但是我们仍然需要考虑页表的大小。以32 位逻辑地址空间、页面大小4KB、页表项大小4B为例,若要实现进程对全部逻辑地址空间的映射,则每个进程需要2^20,1M个表项。也就是说,每个进程仅页表这一项就需要4MB主存空间,这显然是不切实际的。而即便不考虑对全部逻辑地址空间进行映射的情况,一个逻辑地址空间稍大的进程,其页表大小也可能是过大的。以一个40MB的进程为例,页表项共40KB,如果将所有页表项内容保存在内存中,那么需要10个内存页框来保存整个页表。整个进程大小约为1万个页面,而实际执行时只需要几十个页面进入内存页框就可以运行,但如果要求10个页面大小的页表必须全部进入内存,这相对实际执行时的几十个进程页面的大小来说,肯定是降低了内存利用率的;从另一方面来说,这10页的页表项也并不需要同时保存在内存中,因为大多数情况下,映射所需要的页表项都在页表的同一个页面中。
将页表映射的思想进一步延伸,就可以得到二级分页:将页表的10页空间也进行地址映射,建立上一级页表,用于存储页表的映射关系。这里对页表的10个页面进行映射只需要10个页表项,所以上一级页表只需要1页就足够(可以存储2^10=1024个页表项)。在进程执行时,只需要将这1页的上一级页表调入内存即可,进程的页表和进程本身的页面,可以在后面的执行中再调入内存。
我们以Intel处理器80x86系列的硬件分页的地址转换过程为例。在32位系统中,全部32位逻辑地址空间可以分为220(4GB/4KB)个页面。这些页面可以再进一步建立顶级页表,需要210个顶级页表项进行索引,这正好是一页的大小4K,所以建立二级页表即可,所以内存实际要保存页表所占的空间就极少了。
例如逻辑地址: 0x20021406 (0010 0000 0000 0010 0001 0100 0000 0110 B)
顶级页表字段:0x80 (00 1000 0000 B)
二级页表字段:0x21 (00 0010 0001B)
页内偏移量字段:0x406 (0100 0000 0110 B)
顶级页表字段的0x80用于选择顶级页表的第0x80表项,此表项指向和该进程的页相关的二级页表;二级页表字段0x21用于选择二级页表的第0x21表项,此表项指向包含所需页的页框;最后的页内偏移量字段0x406用于在目标页框中读取偏移量为0x406中的字节。
3.1.4 页表结构
请求分页系统,请求分页系统在一个作业运行之前不要求全部一次性调入内存,因此在作业的运行过程中,必然会出现要访问的页面不在内存的情况,如何发现和处理这种情况是请求分页系统必须解决的两个基本问题。为此,在请求页表项中增加了四个字段
增加的四个字段说明如下:
状态位P:用于指示该页是否已调入内存,供程序访问时参考。
访问字段A:用于记录本页在一段时间内被访问的次数,或记录本页最近己有多长时间未被访问,供置换算法换出页面时参考。
修改位M:标识该页在调入内存后是否被修改过。
外存地址:用于指出该页在外存上的地址,通常是物理块号,供调入该页时参考。
3.1.5 页检索
快表检索:若找到要访问的页,便修改页表项中的访问位(写指令则还须重置修改位),然后利用页表项中给出的物理块号和页内地址形成物理地址。若未找到该页的页表项,应到内存中去查找页表,再对比页表项中的状态位P,看该页是否已调入内存,未调入则产生缺页中断,请求从外存把该页调入内存。
3.2 磁盘到虚拟内存的映射
前面讲了比较多的虚拟内测到物理内存的映射。下面我们简要描述下磁盘(交换区)到虚拟内存的映射。
每个进程都有一个内存描述符结构体mm_struct,此结构体保存整个进程的用户地址空间情况,包括各个段的开始结束地址等。每个段内存区详细信息却由其成员VMA描述,比如通常用到的代码段、数据段等,同时我们也可手动通过mmap()函数将文件映射到虚拟内存中,使文件操作像直接操作内存一样方便,映射都都会在程序的虚拟地址空间多一个VMA区段,如下图。当进程切换到相应的位置后,就会利用我们前面讲的页机制映射到相应的物理内存中。