文章目录前言换入换入概述一个实际系统的请求调页换出FIFO(先入先出)MIN算法LRU算法LRU算法的准确实现:用时间戳LRU算法的准确时间:用页码栈clock算法二次机会算法给进程分配多少个页框参考资料
前言
前面说过为了保证内存在用户程序看起来是分段,而实际是分页的效果,引入了虚拟内存。对于用户来说,虚拟内存是一个完整的内存,用户可以随意使用该“内存”,假设为4G,那么对于用户来说就有4G的空间可以使用,即使实际内存只有2G甚至1G。那么这是如何实现的呢?这就引出了换入和换出。
换入
假设虚拟内存4G,实际内存2G。首先进程访问0 ~ 1G这段虚拟内存空间的时候,就将这一段内存与物理内存建立映射。接着如果进程访问3~4G这段虚拟内存空间的时候,就将这段内存与物理内存建立映射,即只有在访问的时候才映射。
换入概述
基本流程是:一个逻辑地址CS:IP,首先根据CS在段表中找到对应的基址,加上偏移得到虚拟地址:页号+偏移。然后根据页号在页表中找到对应的页框号,加上偏移得到物理地址。但是如果在页表中找不到对应的页号对应的页框地址的话,就要从磁盘上将这一页换入了。这个换入采用的是中断的形式,如果load[addr]的时候,发现addr在页表里面没有对应映射,那么就将中断向量寄存器的某一位置为一,说明有中断产生。然后在中断服务函数里面将addr导入到物理内存中。然后再次执行load[addr]这条语句。当然查表的操作是MMU做的。
一个实际系统的请求调页
主要是看一下是如何将某页从磁盘换入到内存的,从中断服务函数开始。cpu知道有个中断只会就去查找这个中断的中断号,然后转去执行该中断服务程序。这些东西是在系统初始化的时候就做好了。
void trap_init(void)
{
set_trap_gate(14, &page_fault);
}
# define set_trap_gate(n, addr)\
_set_gate(&idt[n], 15, 0, addr);
//在linux/mm/page.s中
.globl _page_fault
xchgl %eax,(%esp)
pushl %ecx
pushl %edx
push %ds
push %es
push %fs
movl $0x10, %edx
mov %dx, %ds
mov %dx, %es
mov %dx, %fs
movl %cr2, %edx
首先push一些信息到堆栈中(包括错误码),然后mov一些东西。毕竟从用户态到内核态需要保留一下用户态的情况。
pushl %edx
pushl %eax
testl $1, %eax// 测试标志
jne 1f
call _do_no_page
jmp 2f
1: call _do_wp_page //保护
2: add $8, %esp
pop %fs
pop %es
pop %ds
pop %edx
pop %ecx
pop %eax
iret
一般push后面加了一个call调用c函数这种,前面push的都是压入参数。调do_no_page();
//在linux/mm/memory.c中
void do_no_page(unsigned long error_code, unsigned long address)
{
address&=0xfffff000; //页面地址
tmp=address–current->start_code; //页面对应的偏移
if(!current->executable||tmp>=current->end_data) // 不是代码和数据
{
get_empty_page(address);
return;
}
page=get_free_page();
bread_page(page, current->executable->i_dev, nr);
put_page(page, address);
……
}
从这个函数名字也能看出 是当页不存在的时候做的事情。
主要是这三句
page=get_free_page();
bread_page(page, current->executable->i_dev, nr);
put_page(page, address);
第一句得到一个空闲页赋值给page;第二句磁盘里面的页读到内存中;第三句建立映射。然后再次执行load[addr]; 再次执行load[addr] 这个要硬件设计好,因为一般情况下pc指针会加一。
//在linux/mm/memory.c中
unsigned long put_page(unsigned long page, //物理地址
unsigned long address)
{
unsigned long tmp, *page_table;
page_table=(unsigned long *)((address>>20)&ffc);
if((*page_table)&1)
page_table=(unsigned long*)(0xfffff000&*page_table);
else{
tmp=get_free_page();
*page_table=tmp|7;
page_table=(unsigned long*)tmp;}
page_table[(address>>12)&0x3ff] = page|7;
return page;
}
换出
前面说了页换入,但是内存并不是无限的,有换入就会有换出,换出容易,但是找到哪一页换出不容易,所以就出现了很多替换算法。首先应该明白替换算法的好坏是怎么评判的,我们希望的应该是替换次数尽可能少,什么算法能满足这点就是好的算法。下面是几种替换算法:
FIFO(先入先出)
即每次缺页的时候就替换掉最开始的那一页,这种算法肯定在这个方面肯定不是最好的算法,因为它没有任何机制保证替换次数尽可能少。
MIN算法
选择最远将使用的页淘汰,什么意思呢?假设这个进程只分配了三个页框,现在分别存储了A、B、C三页,假设进程后面使用的页数依次是:C、B、A、D、C、B、A、B、C,使用C、B、A的时候不用换页,当使用D的时候,先看一下后面最远使用的页是哪个,然后将它换掉;对于上例,最远使用的页是A,于是便将A换出,将D换入。可以看出这种方法是最好的方案,但是它实现不了。因为它需要事先知道进程后续要使用哪些页,而这点是做不到的。
LRU算法
唐太宗李世民在《旧唐书·魏徵传》中说过:“以铜为镜,可以正衣冠;以古为镜,可以知兴替;以人为镜,可以明得失。”;其中“以古为镜,可以知兴替”说的就是可以通过历史来预测未来(当然原句的翻译是:用历史当作镜子,可以知道国家兴亡的原因;)。虽然不能知晓未来,但是可以通过前面调用的页的顺序来推测未来哪些页是常用的。理论基础就是程序的空间局部性。
LRU算法就是这样:选最近最长一段时间没有使用的页淘汰(最近最少使用)。
LRU算法的准确实现:用时间戳
用时间戳来记录每页的访问时间,比如某个进程访问页的顺序为:A 、B 、C 、A 、B 、D 、A 、D 、B 、C 、B;该进程只有三个页框。那么使用时间戳
实现,如下图:
第一次将A放入页框中,并记录当前时间为1;第二次将B放入页框中,并记录当前时间为2;第三次将C放入页框中,并记录当前时间为3;第四次又是访问A页,更新A页访问时间,第五次访问B页,更新B页访问时间;第六次访问D页,不存在,那么就在A、B、C页中选择一个最早使用的也就是数字最小的替换,即C页。这种方式理论上是可行的,但是每次地址访问都要修改时间戳,需要维护一个全局时钟,需要找到最小值……实现代价太大了。
LRU算法的准确时间:用页码栈
还是上面的例子,
每次地址访问都需要修改栈,实现代价也比较大。其实LRU准确实现用的少,因此维护代价太高了。从上面两种算法可以看出,主要是在维护时间戳上面花费的时间比较多,但是能不能将LRU算法做一个优化呢?或者说近似实现?
clock算法
LRU的近似实现 - 将时间计数变为是和否
二次机会算法
具体操作是这样的,每页增加一个引用位( R ),每一次访问该页时,就将该位置为1。当发生缺页时用一个指针查看每一页的引用位,如果是1则将其置为0,如果是0就直接淘汰。如下图:
用这种方式实现就不必维护时间戳了,提高了内存使用效率。但是这种算法真的可以使用吗?在实际中,缺页的情况肯定不会很多;如果缺页很多了,说明内存太小了或者算法不行。那当这个算法缺页很少的情况会怎么样呢?假设初始状态所有的页的引用位都是1,这是很有可能的,因为缺页情况少,进程使用的一直是内存里面存在的页。那么当发生缺页时,指针转一圈之后将所有的页的引用位都置为0,没找到能替换的,继续转,这时候发现最开始那个页引用位为0,将其置换出去,指针后移;然后又一段时间没有发生缺页,所有页的引用位都为1,当发生缺页之后,又会将这一轮最开始的那一页置换出去;然后指针后移,一段时间之后发生缺页,又会将这一轮最开始的那一页置换出去。wait…,这不是变成FIFO了吗。
究其原因还是因为指针扫描的时间太长了,也就是记录了太长的历史信息。其中一个解决办法是再加一个指针用来清除每一页的引用位,可以放在时钟中断里面,定时清除;这个时间可以事先设置好,也可以在软件里面实现。
给进程分配多少个页框
嗯嗯,现在置换策略有了,但是还有一个问题需要解决:给进程分配多少个页框。
如果分配的多了,那么请求调页导致的内存高效利用就没有了。而且内存就那么大,如果每一个进程分配很多的话,跑的进程数量就少了。如果分配的少,系统内进程增多,每个进程的缺页率增大,当缺页率大到一定程度,进程就会总等待调页完成,导致cpu利用率降低。如下图
中间那个急剧下降的状态称为颠簸。一种可以采用的方法是,先给进程分配一定数量的页框,如果增加页框能增加cpu利用率,就慢慢增加,如果导致cpu利用率减少,就降低页框分配。当然实际情况下每个进程对应的页框数量肯定是得动态调整的。
参考资料
哈工大李志军操作系统