date | comments | categories | tags | permalink | title | |||
---|---|---|---|---|---|---|---|---|
2020/3/5
|
true
|
|
|
3.21
|
内存的换入换出
|
换入换出概念的引入
前面说过为了保证内存在用户程序看起来是分段,而实际是分页的效果,引入了虚拟内存。对于用户来说,虚拟内存是一个完整的内存,用户可以随意使用该“内存”,假设为4G,那么对于用户来说就有4G的空间可以使用,即使实际内存只有2G甚至1G。那么这是如何实现的呢?这就引出了换入和换出。
换入
假设计算机的物理内存是1G,而给进程分配的逻辑内存是4G,那么当进程访问0-1G的逻辑内存时,就把这1G的内存与实际物理内存做映射。当进程访问1-2G的逻辑内存的时候,就把此时的1G逻辑内存与实际物理内存做映射。而之前占用着实际内存的0-1G的逻辑内存被换出到虚拟内存中(给1-2G腾出空间)。这样在用户看起来似乎,自己确实拥有了4G的内存空间。这个换入采用的是中断的形式,如果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();//得到一个空闲页赋值给page
bread_page(page, current->executable->i_dev, nr);//磁盘里面的页读到内存中
put_page(page, address);//建立内存映射
……
}
//在linux/mm/memory.c中
//参数1就是要计算后返回的物理地址
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算法
把暂时不使用或者最久时间不用的那个换出,但是在实际中无法预测哪一页是最近不访问的。
LRU算法
根据过往的经验,谁最不经常用就把谁换出。由于一个进程通常也就访问局部的一部分内存,例如一个for循环。这样把最长时间不用的那页换出似乎很合理。
LRU算法的时间戳实现
这种方式理论上是可行的,每次使用某页的时候记录下时间戳,但是每次地址访问都要修改时间戳,需要维护一个全局时钟,需要找到最小值……实现代价太大了。
LRU的近似实现 clock算法
LRU的近似实现 - 将时间计数变为是和否。
二次机会算法
具体操作是这样的,每页增加一个引用位R,每一次访问该页时,就将该位置为1。当发生缺页时用一个指针查看每一页的引用位,如果是1则将其置为0,如果是0就直接淘汰。在实际中,缺页的情况肯定不会很多;如果缺页很多了,说明内存太小了或者算法不行。发生缺页的间隔时间相对太长,这时候几乎可以肯定每个页的引用为都是1了,此时再把1都编程0,指针再转一圈,把第一个0的页换出——竟然编程了FIFO方式了。 如何解决这个问题呢?有一个解决办法是再加一个指针用来清除每一页的引用位,可以放在时钟中断里面,定时清除(变成0);这个时间可以事先设置好,也可以在软件里面实现。
给进程分配多少个页空间
如果分配的多了,那么请求调页导致的内存高效利用就没有了。而且内存就那么大,如果每一个进程分配很多的话,跑的进程数量就少了。如果分配的少,系统内进程增多,每个进程的缺页率增大,当缺页率大到一定程度,进程就会总等待调页完成,导致cpu利用率降低。如下图
中间那个急剧下降的状态称为颠簸。一种可以采用的方法是,先给进程分配一定数量的页框,如果增加页框能增加cpu利用率,就慢慢增加,如果导致cpu利用率减少,就降低页框分配。当然实际情况下每个进程对应的页框数量肯定是得动态调整的。