add global page replacement algorithm to chp9.md.
This commit is contained in:
109
thu_os/chp9.md
109
thu_os/chp9.md
@@ -18,7 +18,7 @@
|
||||
|
||||
一个简明的思路是在一个用户进程的运行过程中,对发生的缺页异常数进行统计,通过比较不同置换算法下发生的缺页异常数量,来评估置换算法之间的好坏。
|
||||
|
||||
不难看出,这个思路存在很大的缺陷。因为即使是同一个的程序,在先后的两次运行中,其访问的内存页面序列也会是不一样的,从而难以对页面置换算法的性能进行评价。
|
||||
不难看出,这个思路存在很大的缺陷。因为即使是同一个程序,在先后的两次运行中,其访问的内存页面序列也会是不一样的,从而难以对页面置换算法的性能进行评价。
|
||||
|
||||
为了获得相同的内存页面访问序列,我们可以记录进程访问内存的页面轨迹,随后再离线地模拟页面置换的行为,记录产生缺页的次数。从而缺页数更少的页面置换算法对应了更好的性能。
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
|
||||
### 最近最久未使用算法(LRU)
|
||||
|
||||
这个算法是思路就是选择最长时间没有被引用的页面进行置换。它是基于局部性策略的一种思路,因为`LRU`认为,如果一个页面长时间没有被访问,那么它在将来的很长时间内也很可能不会被访问。可以看到,`LRU`其实是最优置换算法的一种近似啊,所以它的性能也是最接近最优置换算法的。
|
||||
这个算法是思路就是选择最长时间没有被引用的页面进行置换。它是基于局部性原理的一种思路,因为`LRU`认为,如果一个页面长时间没有被访问,那么它在将来的很长时间内也很可能不会被访问。可以看到,`LRU`其实是最优置换算法的一种近似啊,所以它的性能也是最接近最优置换算法的。
|
||||
|
||||
为了实现`LRU`,需要记录内存中的每个逻辑页面上一次的访问时间。在缺页时,选择上一次使用到当前时间最长的页面。
|
||||
|
||||
@@ -78,13 +78,13 @@
|
||||
|
||||
因此,我们希望设计一种算法,它会利用到过去页面的访问信息,但是不会像`LRU`那样将全部信息都维护起来,这就是我们这里的时钟置换算法的思路。
|
||||
|
||||
时钟置换算法会利用到页表项中的访问位,当访问某一页面时,就将该页面的访问位置一。发生缺页时,依次检查驻留在内存中的页面的访问位,如果该位为一,将清零;直到找到一个访问位为零的页面,将该页面置换出去。由于这样一个逐个扫描内存中的页面的行为,类似于时钟指针的运行,所以这个算法才叫时钟置换算法。下图是`clock`算法的图示:
|
||||
时钟置换算法会利用到页表项中的访问位,当访问某一页面时,就将该页面的访问位置一。发生缺页时,依次检查驻留在内存中的页面的访问位,如果该位为一,则清零;直到找到一个访问位为零的页面,将该页面置换出去。由于这样一个逐个扫描内存中的页面的行为,类似于时钟指针的运行,所以这个算法才叫时钟置换算法。下图是`clock`算法的图示:
|
||||
|
||||

|
||||
|
||||
可以看到,这里的访问位,其实就对应了上次缺页到此次缺页,页面的访问情况--访问位为一表示在此期间页面曾被访问,访问位为零则表示没有访问。将访问位为零的页面置换,其实就类似于`LRU`中将最长时间没有被访问的页面置换。相对于`FIFO`,`clock`利用了过去页面的访问信息;相对于`LRU`,又没有保存那么全面的信息,因此算法运行的开销较小。
|
||||
|
||||
根据上面的讨论,如果根本就没有过去页面访问的信息,那么`FIFO`, `LRU`, `clock`算法应该是一致的。这种情况对应于,一个页面在第一次进入内存当中后,就再也没有被访问过,此时`LRU`和`clock`算法都将退化为`FIFO`算法。
|
||||
根据上面的讨论,如果根本就没有过去页面访问的信息,那么`FIFO`, `LRU`, `clock`算法应该是一致的。这种情况对应于,所有页面在第一次进入内存当中后,就再也没有被访问过,此时`LRU`和`clock`算法都将退化为`FIFO`算法。
|
||||
|
||||
### 改进的时钟置换算法
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
|
||||
将这样修改的思路应用到时钟置换算法上,就是这里的改进的时钟置换算法。具体的思路就是上面那样,总是优先选择未被访问(访问位为零),且未被修改的页面被置换。
|
||||
|
||||
具体的实现可以参考访问位的思路,在页表项中再添加一个修改位,来表示该页面被修改的情况。在发生缺页异常时,同样地依次检查驻留在内存中的页面的页表项,若访问位为一,则将访问位清零;若访问位为零且修改为为一,则将该页面写回到外存,且将修改位清零;直到发现一个访问位和修改位都是零的页面,选择将其置换。改进的时钟置换算法如下图所示:
|
||||
具体的实现可以参考访问位的思路,在页表项中再添加一个修改位,来表示该页面被修改的情况。在发生缺页异常时,同样地依次检查驻留在内存中的页面的页表项,若访问位为一,则将访问位清零;若访问位为零且修改位为一,则将该页面写回到外存,且将修改位清零;直到发现一个访问位和修改位都是零的页面,选择将其置换。改进的时钟置换算法如下图所示:
|
||||
|
||||

|
||||
|
||||
@@ -104,5 +104,102 @@
|
||||
|
||||
它的实现也比较简单,只需要维护一个计数器,来对每个页面被访问的次数进行计数,在缺页时选出计数值最小的页面进行换出就可以了。
|
||||
|
||||
这个算法还是具有一些问题的,例如考虑一种情况。我们说内存的访问是有局部性的,考虑某一页面,在进程运行的前一半时间内经常被该进程访问;后来该页面的功能已经完成了,在该进程运行的后半时间里面,对这个页面将不再访问。根据`LFU`算法,此时这个页面的计数值已经足够的大,以至于后续根本不能将该页面进行换出了。
|
||||
这个算法还是具有一些问题的,例如考虑一种情况。我们说内存的访问是有局部性的,考虑某一页面,在进程运行的前一半时间内经常被该进程访问;后来该页面的功能已经完成了,在该进程运行的后半时间里面,对这个页面将不再访问。根据`LFU`算法,此时这个页面的计数值已经足够得大,以至于后续根本不能将该页面进行换出了。
|
||||
|
||||
## 全局页面置换算法
|
||||
|
||||
> 为什么还需要全局页面置换算法?它产生的思路是什么?
|
||||
|
||||
我们说,进程在运行的整个生命周期内,对内存的需求是变化的,例如一个进程在刚开始运行的时候需要比较多的内存空间,在运行的后期进行收尾工作了,需要的内存空间也因此变少了。局部页面置换算法无法考虑到这种情况,从而各个时期分配给该进程的空间都是一样的,无法把一个进程多余的页面分配给其他更需要的进程使用。此外,不同的进程对内存的需求也不一样,一些进程需要更多的空间,另一些进程则较小的空间就可以满足,对于这种情况,局部页面置换算法也难以适应。
|
||||
|
||||
全局页面置换算法就是基于这样的考虑产生的,它希望根据进程对内存需求量的变化,动态的调整分配给不同进程的内存页面,从而使得内存的利用率最高。
|
||||
|
||||
全局页面置换算法包括工作集置换算法和缺页率置换算法。
|
||||
|
||||
### 工作集置换算法
|
||||
|
||||
> 什么是工作集?
|
||||
|
||||
工作集是指一个进程当前正在使用的逻辑页面的集合,可以表示成二元函数$W(t, \Delta)$。其中,t是当前的执行时刻,$\Delta$称为工作集窗口(working-set window ),即一个定长的页面访问时间窗口。这样,$W(t, \Delta)$是指在当前时刻t前的 $\Delta$时间窗口中的所有访问页面所组成的集合。
|
||||
|
||||
工作集的大小,就可以表示当前时刻,一个进程对内存的需求量。在进程的执行周期内,工作集的变化如图所示:
|
||||
|
||||

|
||||
|
||||
可以看到,
|
||||
|
||||
+ 进程开始执行后,随着访问新页面逐步建立较稳定的工作集。
|
||||
+ 当内存访问的局部性区域的位置大致稳定时,工作集大小也大致稳定。
|
||||
+ 局部性区域的位置改变时,工作集快速扩张和收缩过渡到下一个稳定值。
|
||||
|
||||
> 工作集置换算法
|
||||
|
||||
工作集置换算法的思路就是换出不在工作集中的页面。可见,工作集置换算法也是基于局部性原理的,它认为过去访问过的页面(工作集中的页面),在以后有很大可能再次被访问。
|
||||
|
||||
这里还需要引入一个常驻集的概念,常驻集是指在当前时刻,进程实际驻留在内存中的页面的集合。工作集和常驻集具有以下的关系,即
|
||||
|
||||
+ 当常驻集大于工作集时,缺页较少。
|
||||
+ 常驻集增大到一定的规模以后,缺页率也不会明显下降。
|
||||
+ 在工作集发生剧烈变动(过渡阶段)时,缺页较多。
|
||||
|
||||
可以看到,工作集置换算法,其实就是维护进程的常驻集与工作集时刻保持相等。
|
||||
|
||||
为了实现工作集置换算法,只需要维护一个工作集链表。在访存时,将不再工作集中的页面换出内存;在发生缺页时,直接为换入的页面分配新的内存空间,并且更新工作集链表。这样,在每一次访问后,都可以保证工作集和常驻集的大小相等。
|
||||
|
||||
容易看出,工作集置换算法的开销很大。尽管在发生缺页时直接将页面换入就可以了,省去了其他算法寻找被换出页面的开销,但是这是以正常访问时的高成本维护为代价的。因为在正常访问时,需要遍历工作集链表,并且将不在其中的页面换出,遍历的时间复杂度为`O(n)`,此外还有换出页面的I/O开销。
|
||||
|
||||
### 缺页率置换算法
|
||||
|
||||
缺页率可以定义为一段时间内发生的缺页次数/内存访问次数。即
|
||||
|
||||
```
|
||||
缺页率 = 缺页次数 / 内存访问次数
|
||||
```
|
||||
|
||||
或者也可以用缺页平均时间间隔的倒数来指示。
|
||||
|
||||
缺页率置换算法的思路是,如果一个进程的缺页率显著地高于某一个阈值,则认为分配给该进程的物理页面数过少,从而增加分配给该进程的物理页面;而如果一个进程的缺页率明显太低,则认为该进程所占用的物理内存空间太多,从而回收该进程的部分空间。这样,通过调整常驻集的大小,使每个进程的缺页率保持在一个合理的范围内。缺页率算法的思路如图所示:
|
||||
|
||||

|
||||
|
||||
为了实现缺页率置换算法,可以给页表项增加引用位标志。在每次发生缺页时,计算从上次缺页到此次缺页的时间间隔$t_{current} - t_{last}$,
|
||||
|
||||
+ 如果$t_{current} - t_{last} > T$(T为阈值),则认为缺页率太低,置换该进程在$[t_{last}, t_{current}]$时间内没有被引用的页。
|
||||
+ 如果$t_{current} - t_{last} > T$,则认为缺页率太高,增加缺失页到工作集中。
|
||||
|
||||
可以看到,缺页率算法的开销要比工作集置换算法小很多,因为在正常访存时不需要做过多的操作,只需要将被访问的页面的引用位置一即可。
|
||||
|
||||
> 两个全局页面置换算法的比较
|
||||
|
||||
实际上,工作集置换算法和缺页率置换算法,在本质上,都是希望尽可能地跟踪进程运行周期内,对内存需求量的动态变化。工作集置换无疑具有更好的性能,因为每次访存后,工作集置换算法都会保证常驻集的大小等于工作集,但是也因为此,它执行起来需要更大的开销。缺页率置换算法则是开销与性能的一种折中。
|
||||
|
||||
### 抖动与负载控制
|
||||
|
||||
设想一种特殊情况,当运行缺页率置换算法的时候,几乎所有进程的缺页率都大于预先设定的阈值。按照缺页率置换算法,此时应该给这些进程分配更多的内存空间,但是此时已经没有剩余的内存空间可以分配了,并且由于所有进程的缺页率都高于阈值,也并不可以将某一个进程的页面换入到内存中来获得空闲的内存空间。
|
||||
|
||||
这种情况有可能会发生吗?答案是会的,当内存中同时存在的进程过多,以至于每个进程都不能得到足够的内存空间时,就会产生这种现象。但是,如果内存中只有很少的进程在运行,例如只有一个,这就又回到了以前的单进程操作系统了,此时CPU的利用率又会非常低下。可见,CPU的利用率与并发进程存在相互促进与制约的关系。如图所示:
|
||||
|
||||

|
||||
|
||||
+ 进程数少时,提高并发进程数,可提高CPU利用率
|
||||
+ 并发进程导致内存访问增加
|
||||
+ 并发进程的内存访问会降低了访存的局部性特征
|
||||
+ 局部性特征的下降会导致缺页率上升和CPU利用率下降
|
||||
|
||||
当内存中的进程过多时,访存操作会导致大量的缺页,频繁的页面置换使得进程的运行速度变慢,CPU利用率降低,这种现象就是抖动(thrashing)。
|
||||
|
||||
对于传统的缺页置换算法,是无法解决抖动的。此时需要负载控制来解决抖动问题。
|
||||
|
||||
> 什么是负载控制?
|
||||
|
||||
负载控制的本质,其实是操作系统需要在并发水平和缺页率之间达到一个平衡。具体的手段,就是调整并发进程数(MPL, Multi-Programming level),以获得较高的CPU利用率。
|
||||
|
||||
负载控制需要主要考虑两个指标,即平均缺页间隔时间(MTBF, Mean time Between page Faults)以及缺页异常处理时间(PFST, Page Fault Service Time)。考虑一种极端情况,即
|
||||
|
||||
$$
|
||||
\frac{PFST}{MTBF} = 1
|
||||
$$
|
||||
|
||||
此时,每次访存都会产生缺页,CPU的全部时间都用于处理缺页异常的换入换出了,对应于CPU的利用降到了零。负载控制就是通过调整并发进程数,从而改变这个比值,使之对应于CPU利用最高的位置,如图所示。
|
||||
|
||||

|
||||
|
||||
BIN
thu_os/images/PFF.png
Normal file
BIN
thu_os/images/PFF.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
thu_os/images/cpu_use.png
Normal file
BIN
thu_os/images/cpu_use.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
thu_os/images/load_control.png
Normal file
BIN
thu_os/images/load_control.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
thu_os/images/working_set.png
Normal file
BIN
thu_os/images/working_set.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Reference in New Issue
Block a user