缺页中断发生在系统对虚拟地址转换成物理地址的过程中。如果对应的页目录或者页表项没有对应有效的物理内存,则会发生缺页中断。
系统在初始化的时候注册了缺页中断的处理函数。中断号是14。
// 缺页和写保护异常处理函数
set_trap_gate(14,&page_fault);
page_fault是汇编实现的。
_page_fault:
// 交换两个寄存器的值,esp指向的位置保存了错误码
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
// 如果是缺页异常,cr2保存了引起缺页的线性地址
movl %cr2,%edx
// 线性地址(有的话)和错误码入参
pushl %edx
pushl %eax
// 1和eax与,结果放到ZF中
testl $1,%eax
// zf=0则跳转,即0是写异常,1是缺页异常
jne 1f
call _do_no_page
// 跳到标签2
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
// 缺页处理,进程的内容还没有加载到内存,访问的时候导致缺页异常
void do_no_page(unsigned long error_code,unsigned long address)
{
int nr[4];
unsigned long tmp;
unsigned long page;
int block,i;
// 取得线性地址对应页的页首地址,与0xfffff000即减去页偏移
address &= 0xfffff000;
// 算出离代码段首地址的偏移
tmp = address - current->start_code;
// tmp大于等于end_data说明是访问堆或者栈的空间时发生的缺页,直接申请一页
if (!current->executable || tmp >= current->end_data) {
get_empty_page(address);
return;
}
// 是否有进程已经使用了
if (share_page(tmp))
return;
// 获取一页,4kb
if (!(page = get_free_page()))
oom();
/* remember that 1 block is used for header */
/*
算出要读的硬盘块号,但是最多读四块。
tmp/BLOCK_SIZE算出线性地址对应页的
页首地址离代码块距离了多少块,然后读取页首
地址对应的块号,所以需要加一。比如距离2块的距离,则
需要读取的块是第三块
*/
block = 1 + tmp/BLOCK_SIZE;
// 查找文件前4块对应的硬盘号
for (i=0 ; i<4 ; block++,i++)
// bmap算出逻辑块号对应的物理块号
nr[i] = bmap(current->executable,block);
// 从硬盘读四块数据进来,并且复制到物理页中
bread_page(page,current->executable->i_dev,nr);
/*
tmp是小于end_data的,因为从tmp开始加载了4kb的数据,
所以tmp+4kb(4096)后大于end_data,所以大于的部分需要清0,
i即超出的字节数
*/
i = tmp + 4096 - current->end_data;
// page是物理页首地址,加上4kb,从后往前清0
tmp = page + 4096;
while (i-- > 0) {
tmp--;
*(char *)tmp = 0;
}
// 建立线性地址和物理地址的映射
if (put_page(page,address))
return;
// 失败则是否刚才申请的物理页
free_page(page);
oom();
}
1 如果缺页的是堆、栈的空间,则直接分配一页新的物理地址。
// 给address分配一个新的页,并且把页对应的物理地址存储在页面项中
void get_empty_page(unsigned long address)
{
unsigned long tmp;
if (!(tmp=get_free_page()) || !put_page(tmp,address)) {
free_page(tmp); /* 0 is ok - ignored */
oom();
}
}
/*
* This function puts a page in memory at the wanted address.
* It returns the physical address of the page gotten, 0 if
* out of memory (either when trying to access page-table or
* page.)
*/
// page是物理地址,address是线性地址。建立物理地址和线性地址的关联,即给页表和页目录项赋值
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);
// page对应的物理页面没有被分配则说明有问题
if (mem_map[(page-LOW_MEM)>>12] != 1)
printk("mem_map disagrees with %p at %p\n",page,address);
// 计算页目录项的偏移地址,页目录首地址再物理地址0处。这里算出偏移地址后,就是绝对地址,与0xffc即四字节对齐
page_table = (unsigned long *) ((address>>20) & 0xffc);
// 页目录项已经指向了一个有效的页表
if ((*page_table)&1)
// 算出页表首地址,*page_table的高20位是有效地址
page_table = (unsigned long *) (0xfffff000 & *page_table);
else {
// 页目录项还没有指向有效的页表,分配一个新的物理页
if (!(tmp=get_free_page()))
return 0;
// 把页表地址写到页目录项,tmp为页表的物理地址,或7代表页面是用户级、可读、写、执行、有效
*page_table = tmp|7;
// 页目录项指向页表的物理地址
page_table = (unsigned long *) tmp;
}
/*
address是32位,右移12为变成20位,再与3ff就是取得低10位,
即address在页表中的索引,或7代表该页面是用户级、可读、写、执行、有效
*/
page_table[(address>>12) & 0x3ff] = page | 7;
/* no need for invalidate */
// 返回线性地址
return page;
}
2 否则先判断是否有另一个进程和当前进程使用了同一个执行文件。是的话,则判断是否可以共享。
/*
* share_page() tries to find a process that could share a page with
* the current one. Address is the address of the wanted page relative
* to the current data space.
*
* We first check if it is at all feasible by checking executable->i_count.
* It should be >1 if there are other tasks sharing this inode.
*/
// 判断有没有多个进程执行了同一个可执行文件
static int share_page(unsigned long address)
{
struct task_struct ** p;
if (!current->executable)
return 0;
// 只有当前进程使用这个可执行文件则返回
if (current->executable->i_count < 2)
return 0;
for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
if (!*p)
continue;
if (current == *p)
continue;
if ((*p)->executable != current->executable)
continue;
// 找到一个不是当前进程,但都执行了同一个可执行文件的进程
if (try_to_share(address,*p))
return 1;
}
return 0;
}
/*
* try_to_share() checks the page at address "address" in the task "p",
* to see if it exists, and if it is clean. If so, share it with the current
* task.
*
* NOTE! This assumes we have checked that p != current, and that they
* share the same executable.
*/
// 使得另一个进程的页目录和页表项指向另一个进程的正在使用的物理地址
static int try_to_share(unsigned long address, struct task_struct * p)
{
unsigned long from;
unsigned long to;
unsigned long from_page;
unsigned long to_page;
unsigned long phys_addr;
/*
address是距离start_code的偏移。这里算出这个距离跨了多少个页目录项,
然后加上start_code的页目录偏移就得到address在页目录里的绝对偏移
*/
from_page = to_page = ((address>>20) & 0xffc);
// p进程的代码开始地址(线性地址),取得p进程的页目录项地址,再加上address算出的偏移
from_page += ((p->start_code>>20) & 0xffc);
// 取得当前进程的页目录项地址,页目录物理地址是0,所以这里就是该地址对应的页目录项的物理地址
to_page += ((current->start_code>>20) & 0xffc);
/* is there a page-directory at from? */
// from是页表的物理地址和标记位
from = *(unsigned long *) from_page;
// 没有指向有效的页表则返回
if (!(from & 1))
return 0;
// 取出页表地址
from &= 0xfffff000;
// 算出address对应的页表项地址,((address>>10) & 0xffc)算出页表项偏移,0xffc说明是4字节对齐
from_page = from + ((address>>10) & 0xffc);
// 页表项的内容,包括物理地址和标记位信息
phys_addr = *(unsigned long *) from_page;
/* is the page clean and present? */
// 是否有效和是否是脏的,如果不是有效并且干净的则返回
if ((phys_addr & 0x41) != 0x01)
return 0;
// 取出物理地址的页首地址
phys_addr &= 0xfffff000;
if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
return 0;
// 目的页目录项内容
to = *(unsigned long *) to_page;
// 目的页目录项是否指向有效的页表
if (!(to & 1))
// 没有则新分配一页,并初始化标记位
if (to = get_free_page())
*(unsigned long *) to_page = to | 7;
else
oom();
// 取得页表地址
to &= 0xfffff000;
// 取得address对应的页表项地址
to_page = to + ((address>>10) & 0xffc);
// 是否指向了有效的物理页,是的话说明不需要再建立线性地址到物理地址的映射了
if (1 & *(unsigned long *) to_page)
panic("try_to_share: to_page already exists");
/* share them: write-protect */
// 标记位不可写
*(unsigned long *) from_page &= ~2;
// 把address对应的源页表项内容复制到目的页表项中
*(unsigned long *) to_page = *(unsigned long *) from_page;
// 使tlb失效
invalidate();
// 算出页数,物理页引用数加一
phys_addr -= LOW_MEM;
phys_addr >>= 12;
mem_map[phys_addr]++;
return 1;
}
3 1,2都不满足,则到硬盘中把一页内容加载到内存中,并且修改页表项内容。
最后重新执行触发缺页中断的地址。这时候可以找到对应的内容了。