本文引用代码及图片均来自 李治军: 操作系统32讲
在 操作系统 (11) 段页结合 中讲到为了将分段和分页思想结合起来操作系统在逻辑地址和物理地址之间增加了一层虚拟地址。在用户看起来整个虚拟地址空间都是可以使用的,而虚拟地址如何映射到物理内存用户是不知道也不需要知道的
虚拟地址空间往往是大于实际物理内存大小的,例如在64位系统中最大寻址空间为,这是PB级别的内存,实际上计算机不可能有那么大的内存,所以虚拟地址到物理地址的映射会存在重叠
重叠的时候当然不能直接把已使用的内存覆盖,这时我们需要用别的介质把内存数据保存,等这部分数据被用到的时候再导回到内存,也就是内存的换入换出
这里的介质通常是磁盘,所以换入换出其实是指将物理内存的数据放到磁盘以及将磁盘中放置的数据载入回内存的过程,Linux中用于内存换入换出的磁盘分区称为Swap分区
内存换入
内存换入是指进程当前访问的内容不在物理内存中,需要从磁盘导入
进程根据逻辑地址访问内存,逻辑地址会转换成虚拟地址,然后根据虚拟地址得出页号再查页表得出物理页框号
如果此时页表显示该页号没有映射到具体的物理页框号或者页内容不在内存中会产生缺页中断,中断处理程序负责将内容从磁盘中导入并更新页表
代码实现
本节来看看缺页中断是如何处理的,缺页中断号是14,对应的处理程序是page_fault
前面是进入内核态的寄存器压栈和内核数据段选择子的设置等,关键是要将cr2寄存器中的虚拟地址作为参数传入,然后调用_do_no_page
_do_no_page里面计算页面地址,获取空闲内存页,读磁盘到空闲页,最后更新页表
更新页表的操作是定位到页目录项,然后定位到页表项,最后更新相应页表项内容
内存换出
分配给进程的内存页是有限的,不可能每次都能找到空闲的页,因此有时需要将不活跃的内存换出,其中的关键在于选择哪一页
接下来简单介绍几个页面置换算法,评价这些置换算法的指标是缺页次数
FIFO
FIFO(First In First Out) 先进先出是很容易想到的一个算法,思想很朴素,哪一页先进来哪一页就先被换出
假设内存页面访问顺序如图,此时FIFO会导致5次缺页(最开始的三次缺页以及后面DABC四次缺页)
OPTIMAL
最优算法,每次选择最远将被使用的页换出,该算法在理论上是最优的,每次换出最远将被用到的页可以最大限度减少内存换出次数
OPTIMAL算法导致5次缺页
不过,该算法是无法实现的,因为系统无法预知内存的访问情况从而无法确定哪一页会在最远的将来被用到
LRU
LRU(Last Recent Use) 最近最少使用,选择最近最长一段时间没有被使用的页换出,是使用过去预测未来的一种思想 ,过去很久都没被使用的页在将来大概率也不会被使用
LRU导致5次缺页
LRU的实现
LRU是公认很好的一个算法,下面简单介绍它的准确实现和近似实现
时间戳
LRU的准确实现
给每个页面维护一个时间戳,页面被访问时更新时间戳,页面不在内存中则换出时间戳最小(最近最远未被使用)的页,如上图中D第一次出现时替换了时间戳为3的C页面,并且D页面时间戳更新为6
页码栈
LRU的准确实现
用一个固定大小的栈维护最近访问的页面号(页码),每次有新的页码进来(不在内存中的页被访问)则将栈底页码对应的页换出
无论是时间戳还是页码栈,作为LRU的准确实现,理论上可以达到很好的性能。但是,通过分析可以发现它们在每次访问地址时都需要修改时间戳或指针,然后还要维护全局时钟之类的变量,这些操作会带来不小的代价。因此,实际上LRU的准确实现是很少使用的,基本上使用的都是LRU的近似实现
时钟算法
时钟算法类似于时间戳算法,但是它用简单的引用来代替复杂的时间戳,页面被访问时引用位置为1代表最近被访问过,引用位为0时表示最近没被访问过可以被换出
在实现上把进程分配的页组织成循环队列,一个环,然后一个指针指向某一页。当需要换出时,指针开始移动,如果当前指向的页引用为1则将其置零然后指针移动到下一页,否则当前页就是要被换出的页
将引用位由1置为0的操作也称为Second Change Replacement,当指针指向引用位为1的页面时先不换出而是将其引用位置为0,当指针转了一圈再次指向同一个页面时如果引用位还是0,说明该页面最近都没被使用过,此时再将它换出
相比于时间戳和页码栈算法,时钟算法只需更改引用位,这可以由硬件MMU单元查找页表时顺便完成(修改页表项的对应标志位),节省了各种指针的修改和全局变量的维护等操作
当前时钟算法在缺页较少的时候性能会退化,缺页少的时候我们可以假设当前页面基本都为1,当某页想换入时,指针指向的页面置为0,然后由于页面基本都为1,指针转一圈又回到最初指向的页面并将其换出。当某一页又想被换入时重复以上过程,每一页的换入几乎都需要指针转一整圈,此时就退化为FIFO
退化的原因在于如果一个页面引用位被置1了,即使最近未被使用,在指针再次指向它之前将保持为1,这里并没有很好的实践最近未被使用换出的思想。所以,我们可以引入另一个指针,它转的比较快并且被指向的页面都将被置为0。如果某个页面最近被使用了,那么即使它之前被快指针置为0也会被重新置为1而不会被换出,而那些被快指针置为0且最近未被访问的页则会被换出
分配问题
现在置换算法有了,接下来要考虑一次给进程分配多少页的问题了,页面数和CPU利用率之间有如下关系:
物理内存大小是不变的,然而每个进程都需要一定数量的内存页,当进程数量增多对内存的访问就更频繁,相应的内存换入换出也更频繁
CPU利用率降低为什么会导致进程数进一步增大呢?这里的CPU利用率低是因为总是缺页需要访问磁盘,而访问磁盘的时候为了让CPU忙起来就会让它执行其他进程而不是干等着从而导致进程增多
为了避免CPU利用率的颠簸,我们要合理分配进程的内存页数,具体算法有动态分配和求工作集的方法等,这里只引出分配问题不具体介绍算法
总结
最后内存的换入换出可以总结为下图: