存储系列之 虚拟内存:分页技术

引言:前面连续几章讲述的文件系统是存储系统的外存管理的一种抽象,而虚拟内存则是存储系统的内存管理的一种抽象。其实这两种原理有类似地地方,固然也就有不一样的地方。同时这二者也属于操做系统内核的范畴。 html

 

一、虚拟内存的概念

虚拟内存又叫虚拟存储器(Virtual Memory),虚拟内存是计算机系统内存管理的一种技术。mysql

咱们都知道,进程运行前必须将程序加载到内存中,而根据Parkinson定律“存储有多大,程序就会有多长”,因此如何有效的管理内存一直是计算机须要解决的问题,也所以提出了不少简单高效的方案,例如虚拟内存是其中之一。因此虚拟内存是由于内存不足而提出来的,而目前这种技术也已经广泛应用在大多数操做系统中,具体实现起来可能稍有不一样。程序员

虚拟内存简单的定义就是把进程在内存和磁盘之间换进换出。如何换?页面置换。固然这只是比较常见的方法,也还有其余方法或者几种方法的组合。算法

二、交换技术

虚拟内存技术提出以前,其实已有另外一种更简单更直接的技术:交换技术。sql

交换技术,就是把各个进程完整地调入内存,运行一段时间后,而后再放回到磁盘上。缓存

而虚拟内存,只需把进程的一部份内容存放在内存中,并且也能保证进程的正常运行。ide

这两种方式虽然加载的对象大小不相同,可是都须要进程的换进换出。同时,进程的堆栈是实时变化的,那么该如何管理内存空间呢?函数

 其中,操做系统的内核进程是常驻在内存,通常固定在内存空间地址最低端,如上图。32位系统中,通常状况下固定大小1GB为系统区,而其余3GB为用户区。post

2.1 空闲块的管理

内存是动态分配的,那么如何记录当前内存的使用状况?有如下两种方式。性能

(1)位图法

在位图法中,内存被划分为不少个单元,每一个单元对应于位图中的某个数据位,0表示空闲,1表示占用。以下图显示了部分的内存和相应的位图。

分配单元的大小是一个很重要的设计问题,分配单元越小,位图越大。位图法,简单;可是查找一串K个连续的0,比较复杂和缓慢。

 

(2)链表法

 对内存的管理创建一个链表来管理已分配和空闲的内存空间。如上图的(c),P 表明进程Process,H表示空闲Hole,接下来两个字段依次表示起始地址和长度,最后一个字段是指向下一个节点的指针。在这个例子中,链表是按照地址从低到高排序的。这样作的好处是当一个进程运行结束或被置换出去时,能够很方便地来更新链表。

2.2 空闲块的查找

当一个新的程序加载进来的时候又该如何找到空闲的地址空间?一般的分配算法有最早匹配法、下次匹配法、最佳匹配法、最坏匹配法和快速匹配法。这些算法特别是前面几个都有些相似,咱们简单介绍下。

最早匹配法的基本思路是:假如进程的大小M,从链表的首节点开始查找,每一个空闲块的长和M比较,是否大于或等于它,直到找到第一个符合要求的节点。

下次匹配法是在上次查找的结果基础下继续查找直到匹配成功。

最佳匹配法须要遍历全部节点,找到能装得下的最小空闲块。而最坏匹配法则相反,找能装得下的最大的空闲块。事实上,这两种都有比较并且遍历全部链表,效果不佳。

快速匹配法和其余不太同样,基本思路是:对于一些经常使用的请求大小例如2K、4K、8K等,为它们分别设置链表。这样查找匹配很是快,可是若是程序结束时或者被置换后,可能须要合并左邻右舍等操做操做复杂,若是不能合并则可能造成小空洞碎片。

对于这些碎片或者说一些不连续的小黑洞,能够采用内存紧缩(memory compation)技术:把全部的进程都尽量地往内存地址的低端移动,相应地,那些空闲的小分区就会往高端移动,从而在地址高端造成一个较大的空闲区。

三、虚拟内存

上面的交换技术是把程序做为一个总体放入内存。但若是程序太大了,超出了空闲内存的容量,没办法装入,该怎么办?事实上大部分多是这种状况。

当时人们一般采用的解决方案是覆盖技术,即:把程序划分红若干个部分,每一个部分叫作覆盖块(overlay),而后把那些当前须要用到的指令和数据的覆盖块保存在内存中,其余放外存,运行一段时候后,在内外存之间再交换所需的覆盖块。

虽然覆盖块的交换是由操做系统完成,可是覆盖块的划分最开始由程序员来手工完成,这是一项很是复杂的工做,费时费力。后不久人们又想到了一种办法,能够把工做交给计算机完成。

这种方法叫作虚拟存储器(Virtual Memory)(Fotheringham,1961),咱们通常称做虚拟内存。它的基本思路是:程序的代码、数据和栈的总大小能够超过实际可用的物理内存的大小。操做系统把当前须要的那些部分保留在内存中,而把其他部分保持在磁盘上。而后在再须要的时候,再把各个程序片断在内存和磁盘之间来回交换。 

3.1 分页

 大部分虚拟存储系统采用的是一种称为分页(paging)的技术。这种方式叫作虚拟页式存储管理。

由程序产生的地址称为虚拟地址(virtual address),它们构成了一个虚拟地址空间(virtual address space)。虚拟地址也叫作线性地址(linear address)。

若是计算机没有使用虚拟存储机制,那么虚拟地址就是最终的物理地址,它被直接放在地址总线上,从而能够对相应地址的内存单元进行读写操做。若是计算机使用了虚拟存储机制,那么虚拟地址不是直接放在地址总线上,而是被送到存储管理单元(Memory Management Unit,MMU),由它负责把虚拟地址映射为物理地址。MMU通常集成在CPU芯片内部,从逻辑上讲,它能够是单独的一个芯片。     

物理内存空间划分为固定大小的内存块,称为物理页面,或者是页框(page frame)。

虚拟地址空间也划分红大小相同的块,称为虚拟页面,或者简称页面(page)。

页面和页框的大小一般是同样的,要求是2的整数次幂,通常在512字节到1G字节之间。程序在换入换出的时候是以页面为单位

MMU能够完成虚拟地址到物理地址的映射,可是咱们知道,虚拟地址空间是远远大于物理地址空间即内存空间的,因此也就不能保证全部虚拟地址能找到对应的物理地址,即没法完成映射。此时,MMU会引起一个缺页中断(page fault),把这个问题交给操做系统处理。操做系统从内存中挑选一个使用很少的物理页面,把它的内容写回到磁盘,从而腾出了一个空闲页面,而后把引起缺页中断的那个虚拟页面装入该空闲页面中,并对地址映射进行更新。最后回到被中断的指令从新开始。

下面咱们来看看MMU的内部结构,了解一下它的工做原理。举例:页面大小4KB、虚拟地址空间是64KB、物理内存是32KB,所以可获得16个虚拟页面和8个物理页面。以下图所示,虚拟地址8196(二进制是0010 0000 0000 0100),输入的16位虚拟地址被划分为两部分:4位的页号和12位的偏移量。4位的页号能够表示16个页面,12位的偏移量能够寻址4096个字节。

在进行地址映射时,使用虚拟页面号做为索引去访问页表(page table),从而获得相应的物理地址。若是有效位(页表最低位)为0,则产生缺页中断,陷入操做系统中;不然将页表查到的物理页面号加上偏移量,就获得了15位的物理地址。

3.2 页表

如上所述,虚拟地址被分为虚拟页面号(高位)和偏移量(低位)两部分。高4位指定虚拟页面号,也能够是3位、5位或其余,不一样的划分表示不一样的页面大小。

页表的用途是将虚拟页面映射为相应的物理页面

上述例子中只是16位的虚拟地址空间,那么32位的虚拟地址空间为4GB,64位的地址空间可达16EB(虽然咱们广泛不用这么多位),若是将页面映射放在一个页表中,那么页表项将很是庞大。此外因为每一个进程都有本身的虚拟地址空间,所以每一个进程也有本身的页表。这样,页表的数量和规模将更加庞大。有没有一种大而快速的页面映射解决方案呢?分级。

(1)多级页表

多级页表的基本思路是:虽然进程的虚拟地址空间很大,可是当进程在运行时,并不会用到全部的虚拟地址,因此不必把全部的页表项都保存在内存中。

以下图,一个典型的二级页表的例子,虚拟地址为32位,页面大小4KB。虚拟页面号为20位,分红两级,最高10位表示页目录,中间10位为页表,从而造成10+10+12的二级页表。 二级页表也能够扩展为三级、四级或更多级。64位处理器典型地划分为9+9+9+9+12的四级页表。更多的级别带来了更多的灵活性,但算法的复杂性也会更高。 

 

(2)页表项

页目录项和页表项具备相同的结构,但不一样的CPU对页表项的具体安排会有所不一样,咱们讨论一些共性。以下图,给出了一个页表项的示例。页表项的长度因机器而异,通常使用的32位即4字节。

 

 物理页面号------最重要的就是物理页面号,页映射的目的就是找到这个值。

有效位------1表示该表项是有效的,可使用;0则表示该表项对应的虚拟页面如今不在内存中,访问该页面会引发一个缺页中断。

保护位------指出一个页容许什么样的方式访问,最简单的形式是只有一位,0表示读/写,1表示只读;更先进的方式是使用三位,各位分别表示是否启用读、写、执行该页面。

修改位------记录页面的使用状况,在写入一个页时自动设置修改位。若是一个页面已经被修改过(称为“脏页面”),则必须把它写会磁盘。若是没有被修改过(称为“干净页面”),能够直接被覆盖,由于它在磁盘上有备份。修改位也称为脏位,反映了页面的当前状态。

访问位------不管是读仍是写,系统都会在该页面被访问时设置访问位。用于页面置换算法中,未被访问的一般认为是不常用的而被置换出去。页面置换咱们稍后介绍。

禁止缓存位------禁止该页面被高速缓存,对于映射到设备寄存器而不是常规内存的页面很重要。具备独立的I/O空间而不使用内存映射I/O的机器不须要这一位。高速缓存在下一章介绍。

3.3 关联存储器TLB

 从上面咱们能够看出,每一次内存访问,都须要两次访问页表,而随着页表的增多,总体性能是会受很大影响的。后面人们发现绝大多数程序运行时,在任意一个阶段都只会访问一小部分的页面,而非全部页面。这就是访问的局部性原理,或者说程序局部性原理。

人们利用这个特性为计算机设计和增长了一种快速查找的硬件,即TLB(Translation lookaside Buffer)或者称为关联存储器(associative memory),用来存放最经常使用的页表项。这种硬件设备能够直接把虚拟地址映射到物理地址,而没必要访问内存,因此简称为快表。TLB一般位于MMU中,只包含了少许的表项,书上说不超过64,网上有的说不超过256,总之很是少。

工做过程:当一个虚拟地址到来时,MMU首先会到TLB中查找,这个查找很是快,由于它是并行的方式,即同时与全部的页表项进行比较。若是找到了,直接取出物理页面号。不然若是权限够的话将再去内存中查找所需的物理页面号;而后,再将找到的物理页面号所在的页表项添加到TLB中,同时驱逐TLB中某一个页表项;最后将被驱逐的页表项的修改位复制到对应的内存中的页表项。 

软件TLB管理

上面咱们描述的硬件TLB,TLB的管理和TLB未命中时的处理都交由MMU硬件完成,只有当页面不在内存中时才会陷入到操做系统

而在现代有些机器中,几乎全部的页面管理工做都是有软件来完成,TLB表项也由操做系统负责载入。若是发生TLB未命中,MMU会产生一个TLB中断,把问题交给操做系统。操做系统来对TLB进行页面置换,可是这项工做必须用不多的命令完成,由于TLB未命中的频率远远高于缺页中断的频率。为此,人们也设计出了一些方法来提升未命中的几率,例如在内存固定位置设置一个较大的缓冲区,存放最近经常使用的TLB表项;或者预测经常使用的页面预先装入TLB中。

3.4 反置页表

前面咱们讲述的页表方案是经过进程的虚拟页面号来组织的,用虚拟页表号来做为访问页表的索引。若是页面大小4KB,32位寻址时,一个进程的页表项个数是100万。若是再按每一个页表项长度是4字节,一个进程的页表须要4MB的内存空间。这是32位,若是变成64位寻址,须要的内存显然是个天文数字。

一种解决方案就是反置页表(inverted page table),也称做倒排页表,根据内存的物理页面号来组织页表,用物理页面号做为访问页表的索引。有多少个物理页面,就在页表中设置多少个页表项。而通常状况下物理页面远远小于虚拟页面,因此这种方法节省了大量的内存空间。但同时也带来一个问题,即从虚拟页面号到物理页面号的转换变得复杂。必须搜索整个页表。

摆脱这种困境就是使用TLB。由于TLB存放了咱们常常访问的页面。不过若是TLB未命中,则仍然须要对整个反置页表进行搜索。为了加快加快这个过程,人们又想到了一个办法,使用虚拟地址创建一个哈希表。若是两个虚拟页面具备相同的哈希值,那么它们就用链表连起来。(这种方法是否是似曾相识呢:Redis+MySQL,后面提到LRU方法亦是。)

四、其余

以上对虚拟内存的页式存储管理的基本原理已经介绍完成,咱们再深刻了解几个知识点。最后补充一下其余的存储管理方式。

4.1 页面置换的算法和策略

虚拟内存的核心就是进程的换入换出,也就是缺页中断进行页面置换。问题来了,如何选择被置换的页面?页面的换进换出是须要开销的,因此一个好的页面算法就是尽可能减小页面换进换出的次数。

最优算法------易于描述但没法实现,通常用做目标或者说算法性能评价的依据。思路是:对于每个虚拟页面,都计算出下一次访问的时间,用指令数来计算,而后选择等待时间最长的那个页面。明显的比较理想,虚拟页面多并且下一次访问也是不肯定的。

最近未使用算法------Not Recently Used,NRU。按页表项中的访问位和修改位的值对页面进行分红四类,对应四个值,值越小,越没被使用过。而后在值最小的那类再随机抽取一个页面。

先进先出算法------First In First Out,FIFO。把最早访问的页面放在链表的首部,后面访问的再依次排队在链表的首部,最早的变成了尾部,选择的就是链尾页面。该算法可能会淘汰一些不经常使用的页面,可是也存在淘汰一些经常使用只是暂时没用的页面。

第二次机会算法------针对FIFO进行了改进,根据FIFO获得一个页面时不是直接淘汰,而是再给一次机会,把它放在链表的首部。

时钟算法------第二次机会须要移动链表节点,时钟算法将链表变成环形,首尾相连。

最近最久未使用------Least Recently Used,LRU。选择最久未被使用的页面。最优算法的一个近似,它的理论依据是程序的局部性原理。若是某个页面被访问了,它颇有可能立刻被访问;一样若是它好久没被访问,那么未来可能很长时间也不会被访问。在LRU基础上,利用页表项中的访问位和修改位这两个值和二进制移位,获得改进的算法,叫老化算法。减小了LRU链表的操做。

上面讨论的算法是在一个进程内部,若是在相互竞争的进程之间如何分配呢?有两种策略。

局部分配策略------为每一个进程分配固定大小的内存空间。

全局分配策略------全部进程能够动态分配内存空间。一般状况下比局部策略更好,置换页面的时候能够考虑整个内存空间,减小缺页发送的次数。

4.2 工做集模型

页式存储管理中,进程启动之初,全部页面都在外存,因此CPU去取第一个页面时,会引起缺页中断。随着是一系列的缺页中断,一段时间后,中断次数会减小。这种策略就叫作请求调页(demand paging),根据须要随要随调。

而前面咱们介绍过局部性原理,绝大多数进程实际访问的页面只是很小的一部分,咱们把一个进程当前正在使用的页面集合叫作它的工做集(working set)。若是咱们在程序运行前就预先装入它的页面,这种技术叫作预先调页(prepaging)。而装入的页面就是进程运行所需的工做集,这种方法叫作工做集模型。为了实现工做集模型,操做系统必须知道哪些页面属于工做集,一种方法就是前面讨论的老化算法。固然,随着时间的变化,进程的工做集也会发生变化。通常工做集具备渐进式、缓慢变化的特色。

可是进程的工做集有时会发送剧烈的变更,它的运行可能进入一个调整期。若是分配给一个进程的物理页面数太少,不能包含整个工做集,这是进程会形成不少的缺页中断,须要频繁的在内存和外存之间置换页面,从而使得进程的运行速度变慢,咱们把这种状态称做抖动(Denning)。同时咱们也应该要优化咱们的代码尽可能减小和避免这种状况的发生!

4.3 页面大小

页面大小在页式存储管理系统中是一个很是重要的参数,也是一个能够自定义的参数。查看系统中页的大小:

[root@localhost mysql]# getconf PAGESIZE
4096

内核是以页面做为内存管理的单位,内存分配时,每次分配都是页面大小的整数倍。页面越小,内碎片就会越少。分配的内存通常不会是页面的整数倍,最后一个页面剩下的空间叫作内碎片。页面越小,同时页表就越庞大,进程运行时系统开销也会越大。

另外,在内存和磁盘之间传送数据也是以页面为单位的。因此文件系统的逻辑块大小最好和页面大小保持一致。

大多数计算机使用的页面大小在512字节到1M之间,典型的值是1KB、4KB和8KB。如今随着内存容量愈来愈大,页面大小也愈来愈大。

4.4 段式和段页式存储管理

前面介绍都是跟分页方式相关的虚拟内存。事实上,虚拟内存的调度方式有分页式、段式、段页式3种。只是如今大部分的操做系统使用了分页式,并且理解了分页式,再来看其余两种就很是容易了。

(1)段式存储管理

分页式存储管理是一维的,而段式则是二维的。即分页式存储的虚拟地址从0到某一个最大地址,一个接一个。而段式存储提供多个相互独立的地址空间,称为段(segment);每一个段的内部都是从0到某一个最大值这样一个线性地址,段的长度能够动态变化。

分段有助于在几个进程之间共享函数和数据,一个典型的例子就是共享库(shared library)。页式存储管理也能够实现共享库,可是要复杂得多,实际上它们都是经过模拟分段来实现的

在具体实现上,段式和页式存储系统是彻底不一样的:页面是定长的而段不是。因此,随着程序的运行,段式存储很容易造成外碎片(external fragmentation)或者叫作跳棋盘(checkerboarding)。外碎片一般比较小,没法再装入新的段,容易形成浪费。固然这个问题也能够经过前面2.2节讲过的紧缩技术来解决。

(2)段页式存储管理

Intel Pentinum支持16K个段,每一个段最多能够容纳4GB的虚拟地址空间。操做系统能够对其进行设置,使他支持纯页式、纯段式或者段页式存储管理。大多数操做系统如Linux和Windows都采用纯页式存储管理。

Pentinum虚拟存储器的核心是两张表,局部描述符(Local Descriptor Table,LDT)和全局描述符(Global Descriptor Table,GDT)。每一个进程都有本身的LDT,可是GDT只有一个,为计算机上全部进程共享。LDT描述的是每一个程序本身的段,包括代码段、数据段、栈段等,而GDT描述的是系统的段,包括操做系统自己。

 

上图是Pentinum代码段描述符的结构,数据段略有不一样。本文再也不铺开描述了,有兴趣能够参考:分段机制与GDT|LDT

 

 

参考资料:

《操做系统设计与实现》第三版 上册。

《深刻理解LINUX内核》第三版。

相关文章
相关标签/搜索