arch/i386/fault.c
页面异常, swap, address space, shmem, filemap
从文件开始,而不拘泥于文件。
此文件内只有两个函数。
1. __verify_write int __verify_write(const void * addr, unsigned long size)
此函数主要应用于检查用户传递的内存块是否"可写". 可写意味着:
a)内核容许其写,即vma的记录;
b)相应的物理页面已经分配,即虚存已经有了对应的映射. 主要使用者是
access_ok (include/asm-i386/uaccess.h).
access_ok 大量应用于和用户空间进行数据交换的时候检查用户传递的内
存是否合法.以避免在内核中产生页面访问异常,被别有用心的程序破坏了"大
好形势".
下面是此函数,以及注释.
int __verify_write(const void * addr, unsigned long size)
{
struct vm_area_struct * vma;
unsigned long start = (unsigned long) addr;
if (!size)
return 1;
vma = find_vma(current->mm, start); if (!vma)
goto bad_area; if (vma->vm_start > start) goto check_stack;
good_area:
if (!(vma->vm_flags & VM_WRITE))
goto bad_area;
size--;
size += start & ~PAGE_MASK;
size >>= PAGE_SHIFT;
start &= PAGE_MASK;
for (;;) {
if (handle_mm_fault(current->mm, vma, start, 1) <= 0) goto bad_area; if (!size) break;
size--;
start += PAGE_SIZE;
if (start < vma->vm_end)
continue;
vma = vma->vm_next;
if (!vma || vma->vm_start != start)
goto bad_area;
if (!(vma->vm_flags & VM_WRITE))
goto bad_area;;
}
return 1;
check_stack:
if (!(vma->vm_flags & VM_GROWSDOWN)) goto bad_area;
if (expand_stack(vma, start) == 0) goto good_area;
bad_area:
return 0;
}
就这个函数本身,其中的注释已经足以理解其逻辑. 需要着重强调的是:
a) vma
vma是内核管理虚拟内存的手段,其中记录了进程所拥有的虚拟地址的
范围和属性,是处理内存异常的依据之一. 如果进程所有的虚存较多,
vma还可以组织成平衡树,以加快查找速度,不过这是vma资源的管理算法,
而不是内核的逻辑. 逻辑和具体资源管理算法应该区别对待,以免陷入
万劫不复的细节中去.所以, find_vma 本身的细节不用去追究,否则分
析之粒度就过于详细,反而迷失于内核之中了.
b) handle_mm_fault (mm/memory.c)
此函数其使用在这里是"不务正业"的, 使用在本文件的下一个函数
do_page_fault才是正道.其逻辑是比较简单的. 内核假定有三级页面映
射,此函数顺着指定地址摸下去将涉及到的page table entry, page dir
entry, 逐个检查一遍如有未分配之page dir , page, 就分配一个并设
置好相应的entry.其复杂之处在于
handle_mm_fault->handle_pte_fault(也在memory.c之中):
先来注释一把:
static inline int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct * vma, unsigned long address,
int write_access, pte_t * pte)
{
pte_t entry;
spin_lock(&mm->page_table_lock);
entry = *pte;
if (!pte_present(entry)) {
spin_unlock(&mm->page_table_lock);
if (pte_none(entry)) return do_no_page(mm, vma, address, write_access,
pte );
return do_swap_page(mm, vma, address, pte,
pte_to_swp_entry(entry),
write_access
);
} if (write_access) { if (!pte_write(entry))
return do_wp_page( mm, vma, address, pte, entry );
entry = pte_mkdirty(entry);
}
entry = pte_mkyoung(entry);
establish_pte(vma, address, pte, entry);
spin_unlock(&mm->page_table_lock);
return 1;
}
这里要强调的是页面的换入. 有以下途经分配/换入页面,从而建立页面
映射.
I)第一种情况:根本没有建立,或者已经被断开.
换入函数 do_no_page(mm/memory.c), 首先尝试 vma->vm_ops->nopage,
如果vma没有提供nopage操作就使用do_anonymous_page分配一个空闲页面.
(本次分析细节到此结束)
II)第二种情况是被交换到了swap space
则通过 do_swap_page从swap 文件读入内存(或者从swap cache 找回).
此函数还涉及计COW, 这里不再讨论.
页面的释放和换出这里页简单提提,首先定义匿名页面是
page->mapping(address mapping)为NULL的页面.页面换出当然是try_to_swap_out
所为 .try_to_swap_out换出非匿名页的时候是直接断开页面映射.匿名的clean页
面的映射也是直接断开,因为不需要写入交换文件.
如果是dirty的匿名页,则分配一个swap页(swap file中的一个页面)然后将
swap 页的entry写入pte,并置pte为页面不在内存.
try_to_swap_out区别对待这三种页面,和 handle_pte_fault换入页面的不同
操作相对应.
总结一下页面的周转:从try_to_swap_out和handle_pte_fault看过去, 换
出要不就是swap space,要不就是page->mapping->a_ops->writepage(见mm/vmscan.c
函数page_launder,应该注意到无论是swap cache还是page cahche都使用writepage
换出页面).换入要不就是swap space,要不就是vma->vm_ops->nopage(分配干净页面
就不算进页面周转了).
下面的问题就是 page->mapping->a_ops->writepage 和 vma->vm_ops->nopage
到底是什么函数,怎样赋值?
先看page->mapping->a_ops->writepage,搜索page->mapping,在函数
do_generic_file_read(mm/filemap.c)中发现page->mapping来源于
inode->i_mapping. do_generic_file_read->__add_to_page_cache读入一个页面的
文件后,将页面加入page cache,同时赋予page->mapping以addr space. 另外在做搜
索的过程中应该注意函数add_to_page_cache_locked(mm/filemap.c)此函数建立page
cache内页面的page->mapping映射.搜索调用add_to_page_cache_locked的函数得知,
shmem_nopage(mm/shmem.c)建立的page->mapping来源于inode;add_to_swap_cache
(mm/swap_state.c)建立的的page->mapping映射是swapper_space(也在swap_state.c).
顺着这些线索,可以找到,其实写页面最终和一个inode相关联,这也于理相符. 我们推
断换入用的vma->vm_ops->nopage最终也和inode相关.
看看vma->vm_ops->nopage的情况. 直接搜索nopage,发现有两种vm_ops,
filemap_nopage(* area,address, no_share)(mm/filemap.c), shmem_nopage( * vma,
address, no_share)(mm/shmem.c).
现在看来,vma可以获取文件映射(mmap,参考函数generic_file_mmap),shemem,
或者匿名页,来建立其终物理页面的映射.从这些线索分析,filemap_nopage利用
page-mapping的readpage从文件读入页面内容,呼应了刚才的猜想.shmem_nopage
只是通过vma相关联的文的inode之mapping在swap space寻找被交换出去的shemem
页面,并不用来从文件读取页面内容.寻找到的页面(或者新分配的)就加入page cache
(如果是从swap cache找到的则需要从swap chache移出).到这里再看page cache和
swap cache的界定,page->maping的值如果是swapper_space, 则页面处于swap cache,
如果page->maping的值是其他add space,比如filemap, shemem设置的mapping,则称
页面在page cache 中.
希望还没有迷路.
2. asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
不是想在这里详细介绍i386 cpu的处理机制,而是给出中断处理在linux中的
线索,以资参考。
中断或异常发生后,i386终止执行当前运行的指令流(参考:),保存必要的
状态根据当前的运行级别(内核/用户),从idt选择合适的gate,执行指定地址
的处理函数。
我们以页面异常为例。
首先看页面异常门的设置。首先要看idt地址。在系统启动的时候,boot程序已
经初始化了一个idt,但是在kernel中,还要重新指定定idt地址并用lidt加载内核
的idt。从 arch/i386/kernel/traps.c 的函数trap_init 看过去 .
宏 set_trap_gate:
static void __init set_trap_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,0,addr);
}
引用了idt地址, idt_table,定义在traps.c 文件的开始 处:
struct desc_struct idt_table[256] __attribute__((__section__(".data.idt")))= { {0, 0}, };
由此得知在内核链接的时候预留了一个section .data.idt 作为idt表.
从init/main.c 的函数start_kernel开始内核 初始化经历 ->trap_init->cpu_init:
__asm__ __volatile__("lidt %0": "=m" (idt_descr));
至此,trap初始化,idt表设置已经完成. 深入下去的话include/asm-i386/desc.h
中定义:
#define idt_descr (*(struct Xgt_desc_struct *)((char *)&idt - 2))
并声明
extern struct desc_struct *idt, *gdt;
而idt是编译链接程序生成的符号,地址即是.data.idt
函数trap_init 中 初始化14号异常,即页面异常入口为page_fault
set_trap_gate(14,&page_fault);
page_falut定义在arch/i386/kernel/entry.s:
ENTRY(page_fault)
pushl $ SYMBOL_NAME(do_page_fault)
jmp error_code
在这里调用页面异常处理函数 do_page_fault(arch/i386/mm/fault.c).
下面分析此函数, 分析在代码中以注释形式出现.
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
struct task_struct *tsk;
struct mm_struct *mm;
struct vm_area_struct * vma;
unsigned long address;
unsigned long page;
unsigned long fixup;
int write;
siginfo_t info;
__asm__("movl %%cr2,%0":"=r" (address));
tsk = current;
if (address >= TASK_SIZE)
goto vmalloc_fault;
mm = tsk->mm;
info.si_code = SEGV_MAPERR;
if (in_interrupt() || !mm)
goto no_context;
down(&mm->mmap_sem);
vma = find_vma(mm, address); if (!vma)
goto bad_area; if (vma->vm_start <= address)
goto good_area; if (!(vma->vm_flags & VM_GROWSDOWN))
goto bad_area;
if (error_code & 4) {
if (address + 32 < regs->esp)
goto bad_area; } if (expand_stack(vma, address))
goto bad_area;
good_area: info.si_code = SEGV_ACCERR;
write = 0;
switch (error_code & 3) {
default:
#ifdef TEST_VERIFY_AREA
if (regs->cs == KERNEL_CS)
printk("WP fault at %08lx\n", regs->eip);
#endif
case 2:
if (!(vma->vm_flags & VM_WRITE))
goto bad_area; write++;
break;
case 1:
goto bad_area; case 0:
if (!(vma->vm_flags & (VM_READ | VM_EXEC)))
goto bad_area; }
switch (handle_mm_fault(mm, vma, address, write)) {
case 1:
tsk->min_flt++;
break;
case 2:
tsk->maj_flt++;
break;
case 0:
goto do_sigbus;
default:
goto out_of_memory;
}
if (regs->eflags & VM_MASK) {
unsigned long bit = (address - 0xA0000) >> PAGE_SHIFT;
if (bit < 32)
tsk->thread.screen_bitmap |= 1 << bit;
}
up(&mm->mmap_sem);
return;
bad_area:
up(&mm->mmap_sem);
bad_area_nosemaphore:
if (error_code & 4) {
tsk->thread.cr2 = address;
tsk->thread.error_code = error_code;
tsk->thread.trap_no = 14;
info.si_signo = SIGSEGV;
info.si_errno = 0;
info.si_addr = (void *)address;
force_sig_info(SIGSEGV, &info, tsk);
return;
}
if (boot_cpu_data.f00f_bug) {
unsigned long nr;
nr = (address - idt) >> 3;
if (nr == 6) {
do_invalid_op(regs, 0);
return;
}
}
no_context:
if ((fixup = search_exception_table(regs->eip)) != 0) {
regs->eip = fixup;
return;
}
bust_spinlocks();
if (address < PAGE_SIZE)
printk(KERN_ALERT "Unable to handle kernel NULL pointer dereference");
else
printk(KERN_ALERT "Unable to handle kernel paging request");
printk(" at virtual address %08lx\n",address);
printk(" printing eip:\n");
printk("%08lx\n", regs->eip);
asm("movl %%cr3,%0":"=r" (page));
page = ((unsigned long *) __va(page))[address >> 22];
printk(KERN_ALERT "*pde = %08lx\n", page);
if (page & 1) {
page &= PAGE_MASK;
address &= 0x003ff000;
page = ((unsigned long *) __va(page))[address >> PAGE_SHIFT];
printk(KERN_ALERT "*pte = %08lx\n", page);
}
die("Oops", regs, error_code);
do_exit(SIGKILL);
out_of_memory:
up(&mm->mmap_sem);
printk("VM: killing process %s\n", tsk->comm);
if (error_code & 4)
do_exit(SIGKILL);
goto no_context;
do_sigbus:
up(&mm->mmap_sem);
tsk->thread.cr2 = address;
tsk->thread.error_code = error_code;
tsk->thread.trap_no = 14;
info.si_code = SIGBUS;
info.si_errno = 0;
info.si_code = BUS_ADRERR;
info.si_addr = (void *)address;
force_sig_info(SIGBUS, &info, tsk);
if (!(error_code & 4))
goto no_context;
return;
vmalloc_fault:
{
int offset = __pgd_offset(address);
pgd_t *pgd, *pgd_k;
pmd_t *pmd, *pmd_k;
pgd = tsk->active_mm->pgd + offset;
pgd_k = init_mm.pgd + offset;
if (!pgd_present(*pgd)) {
if (!pgd_present(*pgd_k))
goto bad_area_nosemaphore;
set_pgd(pgd, *pgd_k);
return;
}
pmd = pmd_offset(pgd, address);
pmd_k = pmd_offset(pgd_k, address);
if (pmd_present(*pmd) || !pmd_present(*pmd_k))
goto bad_area_nosemaphore;
set_pmd(pmd, *pmd_k);
return;
}
}
英文说的比较明白的就不敢添足了。
幸亏上个函数对handle_mm_fault评论了一番,do_page_fault到这里也该收手了.