1.如果访问的虚拟地址在进程空间没有对应的VMA(mmap和malloc可以分配vma),则缺页处理失败,程序出现段错误.
2.Linux把没有映射到文件的映射叫做匿名映射(malloc和mmap的匿名映射)。
3.remap_pfn_range把内核内存映射到用户空间,一般在设备驱动的mmap函数中调用.
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t prot)
4.mmap与malloc的区别
malloc只能用来分配匿名私有映射,而mmap可以分配四种不同的映射
分配内存小于128k时,malloc分配地址从start_brk开始,也就是数据段之后,mmap起始地址位于mmap区
malloc分配小于128k时,由brk系统调用实现,而大于128k,由mmap系统调用实现.
总之,mmap功能比malloc强大. malloc是mmap的特殊情况。
5.缺页中断分析
缺页中断,无非就是建立GPD->PMD->PTE到page的映射关系,malloc和mmap函数,只是给进程分配了虚拟地址空间(VMA),而并没有分配物理内存,更没有建立对应的页表映射.缺页中断主要函数:handle_mm_fault>handle_pte_fault,主要进行物理内存分配,预读文件,建立页表等,分四种情况:
私有匿名内存映射, 文件映射缺页中断, swap缺省中断,写时复制(COW)缺页中断.
5.1 私有匿名内存映射
私有的匿名内存映射又可以分为只读缺页,可写缺页中断两种情况,处理函数为do_anonymous_page,应用场景为malloc内存分配
只读缺页时,会从zero page分配一个page,并填充具体的pte表项
可写缺页时,直接从伙伴系统分配一个page,并填充pte表项
static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags)
{
/*只读缺页中断,直接分配一个zero page */
/* Use the zero-page for reads */
if (!(flags & FAULT_FLAG_WRITE) && !mm_forbids_zeropage(mm)) {
entry = pte_mkspecial(pfn_pte(my_zero_pfn(address),
vma->vm_page_prot));
page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
goto setpte;//设置pte页表项
}
/*可读的缺页中断,直接从伙伴系统分配页面 */
page = alloc_zeroed_user_highpage_movable(vma, address);
if (!page)
goto oom;
/*设置页面内容有效 */
__SetPageUptodate(page);
/*生成pte表项值 */
entry = mk_pte(page, vma->vm_page_prot);
if (vma->vm_flags & VM_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry));
/*关联匿名页表 */
page_add_new_anon_rmap(page, vma, address);
/*添加到lru链表 */
lru_cache_add_active_or_unevictable(page, vma);
setpte:
set_pte_at(mm, address, page_table, entry);
return 0;
}
5.2 文件映射缺页中断
可以分为三种情况,只读缺页中断(mmap读取文件), 写时复制缺页中断(加载动态库),共享缺页中断(进程通信),函数do_fault处理这三种情况
static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags, pte_t orig_pte)
{
pgoff_t pgoff = (((address & PAGE_MASK)
- vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff;
pte_unmap(page_table);
/*可读缺页中断*/
if (!(flags & FAULT_FLAG_WRITE))
return do_read_fault(mm, vma, address, pmd, pgoff, flags,
orig_pte);
/*写时复制缺页中断 */
if (!(vma->vm_flags & VM_SHARED))
return do_cow_fault(mm, vma, address, pmd, pgoff, flags,
orig_pte);
/*共享内存缺页中断 */
return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
}
5.2.1 只读缺页中断
先介绍struct vm_operations_struct *vm_ops的三个重要函数fault和map_pages,page_mkwrite
struct vm_operations_struct {
/*从page cache或者伙伴系统分配页面,进行文件预读(readahead),并填充页面内容 */
int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
/*从page cache获取页面,并建立页表映射 */
void (*map_pages)(struct vm_area_struct *vma, struct vm_fault *vmf);
/* notification that a previously read-only page is about to become
* writable, if an error is returned it will cause a SIGBUS */
/* 标记页面可写,并回写页面内容到磁盘*/
int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);
}
文件映射的缺页异常处理,基本就是调用这三个函数,进程页面分配和文件预读,页表映射.
static int do_read_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd,
pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) {
pte = pte_offset_map_lock(mm, pmd, address, &ptl);
/*do_fault_around函数会预先映射缺页页面周围的16页面,这样可以减少缺页中断次数 */
do_fault_around(vma, address, pte, pgoff, flags);
if (!pte_same(*pte, orig_pte))
goto unlock_out;
pte_unmap_unlock(pte, ptl);
}
/*do_fault函数主要调用文件系统对应的fault函数,从page cache分配页面(如果page cache没有页面缓存,则从伙伴系统分配),从文件预读,并填充页面 */
ret = __do_fault(vma, address, pgoff, flags, NULL, &fault_page);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
return ret;
/*建立页表 */
pte = pte_offset_map_lock(mm, pmd, address, &ptl);
do_set_pte(vma, address, fault_page, pte, false, false);
return ret;
}
5.2.2 写时复制缺页处理(加载动态库)
处理函数do_cow_fault主要分配new page,并从old page复制内容到new page,并且会调用__do_fault进行文件预读.
5.2.3 共享文件缺页处理(进程间通信)
处理函数do_shared_fault主要调用__do_fault函数预读文件,并回写内容到磁盘
5.3 交换缺页异常
内存页面被换出到磁盘,处理函数do_swap_page把页面内容从磁盘换回内存.
5.4 写时复制缺页(fork)
这里的写时复制缺页异常,主要用于处理子父进程(fork)的缺页.与文件的写时复制缺页处理和应用场景不一样。函数do_wp_page处理这种异常.