概述
我们知道当我们使用内存管理策略时,我们编程中使用的地址在0-4G之间(对于32位机器来说),但是往往实际的内存没有那么大,可能只有1G左右。当pc指针移动取址时,发现取出的虚拟地址无法映射到实际的内存页上,这种情况就称为缺页。发生缺页的原因通常是一个程序的程序段在虚拟内存上排列的较为分散,起始范围与末梢范围太大,这种情况下,每次执行到不同的段时容易发生缺页。
请求调页
- 如果本身的程序很大,请求调页时,会由于申请不到空闲页而造成调页失败现象。
- 请求调页发生在虚拟地址无法映射到有效的物理页时。
- 请求调页时如果成功申请到物理页,则将从磁盘拷贝当前进程中可执行文件的一段内容到刚申请到的物理页。
调页换入代码分析
1、当mmu从虚拟地址中无法映射到有效的物理页时,发生页错误,会进入也错误中断:
// linux-0.11/kernel/traps.c
void trap_init(void)
{...
set_trap_gate(14,&page_fault);...
}
上面的初始化代码就是设置页错误中断入口的。
2、page_fault是也错误中断入口,这中断函数就是实现了调页换入的功能:
// linux-0.11/mm/page.s
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 // 将映射失败的虚拟地址保存到edx寄存器中
pushl %edx // 压栈edx中的内容,为了后面调用do_no_page提供参数
pushl %eax
testl $1,%eax
jne 1f
call do_no_page // 开始调用这个函数,这个函数会实现调页功能
jmp 2f
1: call do_wp_page
2: addl $8,%esp
pop %fs
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax
iret
上面一段代码是汇编代码,主要之后进入了 do_no_page这个c函数,并且提供了那个映射失败的虚拟地址参数,提供这个参数也是为了之后将换入的页重新与该地址建立映射。
3、do_no_page的实现:
// linux-0.11/mm/memory.c
void do_no_page(unsigned long error_code,unsigned long address)
{
int nr[4];
unsigned long tmp;
unsigned long page;
int block,i;
address &= 0xfffff000;
tmp = address - current->start_code;
if (!current->executable || tmp >= current->end_data) {
get_empty_page(address);
return;
}
if (share_page(tmp))
return;
if (!(page = get_free_page())) // 申请空闲页,此页是要拷贝磁盘中的程序段的
oom();
/* remember that 1 block is used for header */
block = 1 + tmp/BLOCK_SIZE;
for (i=0 ; i<4 ; block++,i++)
nr[i] = bmap(current->executable,block);
bread_page(page,current->executable->i_dev,nr); // 从磁盘拷贝程序段到page
i = tmp + 4096 - current->end_data;
tmp = page + 4096;
while (i-- > 0) {
tmp--;
*(char *)tmp = 0;
}
if (put_page(page,address)) // 将物理page与虚拟地址建立映射关系
return;
free_page(page);
oom();
}
从上面的注释可以发现put_page是建立映射关系的。
4、put_page的代码实现分析:
// linux-0.11/mm/memroy.c
unsigned long put_page(unsigned long page,unsigned long address)
{
unsigned long tmp, *page_table;
/* NOTE !!! This uses the fact that _pg_dir=0 */
if (page < LOW_MEM || page >= HIGH_MEMORY)
printk("Trying to put page %p at %p\n",page,address);
if (mem_map[(page-LOW_MEM)>>12] != 1)
printk("mem_map disagrees with %p at %p\n",page,address);
page_table = (unsigned long *) ((address>>20) & 0xffc); // 从页目录项获取页表
if ((*page_table)&1)
page_table = (unsigned long *) (0xfffff000 & *page_table);
else {
if (!(tmp=get_free_page())) // 申请一个物理页用来建立页表
return 0;
*page_table = tmp|7;
page_table = (unsigned long *) tmp;
}
page_table[(address>>12) & 0x3ff] = page | 7; // 将物理页框号映射到虚拟地址中
/* no need for invalidate */
return page;
}
上面的代码就是重新将换入的物理页的页框号与虚拟地址建立映射关系的,到此时请求换页已完成。