当没有找到对应的PTE并且没有设置vma->vm_ops时候,就会触发匿名缺页异常,那么如果vm_ops操作函数存在的情况下,该如何处理呢?本章主要是学习文件映射存在的情况下,会产生文件映射缺页异常的处理流程。
1. 代码流程
对于文件映射缺页异常,内核会调用do_fault()函数进行处理,do_fault()函数处理大致分为以下三种情况
static int do_fault(struct fault_env *fe)
{
struct vm_area_struct *vma = fe->vma;
pgoff_t pgoff = linear_page_index(vma, fe->address);
/* The VMA was not fully populated on mmap() or missing VM_DONTEXPAND */
if (!vma->vm_ops->fault)
return VM_FAULT_SIGBUS;
if (!(fe->flags & FAULT_FLAG_WRITE))
return do_read_fault(fe, pgoff);
if (!(vma->vm_flags & VM_SHARED))
return do_cow_fault(fe, pgoff);
return do_shared_fault(fe, pgoff);
}
- 首先,检查VMA的vm_ops方法中是否提供fault(),如果没有,就直接返回VM_FAULT_SIGBUS
- 如果需要获取的页面不具备可写属性,就执行do_read_fault,一般这次缺页是由于读内存导致的缺页异常
- 如果需要获取的页面具有可写属性,但时VMA的属性中没有设置VM_SHARED,即这个VMA是属于私有映射,则执行do_cow_fault
- 其他情况就属于可写的共享页面,即属于共享映射并且这次异常是可写内存导致的缺页异常,则执行do_shared_fault
1.1 do_read_fault
do_read_fault处理因为读内存导致的缺页中断,该函数实现在mm/memory.c文件中
static int do_read_fault(struct fault_env *fe, pgoff_t pgoff)
{
struct vm_area_struct *vma = fe->vma;
struct page *fault_page;
int ret = 0;
/*
* Let's call ->map_pages() first and use ->fault() as fallback
* if page by the offset is not ready to be mapped (cold cache or
* something).
*/
if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) { -----------(1)
ret = do_fault_around(fe, pgoff);
if (ret)
return ret;
}
ret = __do_fault(fe, pgoff, NULL, &fault_page, NULL); -----------(2)
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
return ret;
ret |= alloc_set_pte(fe, NULL, fault_page); -----------(3)
if (fe->pte)
pte_unmap_unlock(fe->pte, fe->ptl);
unlock_page(fault_page);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
put_page(fault_page);
return ret;
}
- 如果定义了map_pages函数,那么可以在缺页异常地址附件提前映射尽可能多的页面。提前建立进程地址空间与页面高速缓存的映射关系有利于减小因为发生缺页的次数,提高了效率。do_fault_around函数只是建立映射关系,而不会创建页面高速缓存,需要在__do_fault函数中创建新的页面高速缓存
- __do_fault是真正为异常地址分配物理页面
- 调用alloc_set_pte函数为物理页面和缺页异常发生的虚拟地址建立映射关系,即使用这个物理页面来创建一个PTE,然后设置PTE,然后释放页锁,并且唤醒等待这个页锁的进程
下面来看看核心函数__do_fault,其实现在mm/memory.c中
static int __do_fault(struct fault_env *fe, pgoff_t pgoff,
struct page *cow_page, struct page **page, void **entry)
{
struct vm_area_struct *vma = fe->vma;
struct vm_fault vmf;
int ret;
vmf.virtual_address = (void __user *)(fe->address & PAGE_MASK);
vmf.pgoff = pgoff;
vmf.flags = fe->flags;
vmf.page = NULL;
vmf.gfp_mask = __get_fault_gfp_mask(vma);
vmf.cow_page = cow_page;
ret = vma->vm_ops->fault(vma, &vmf); -------------(1)
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
return ret;
if (ret & VM_FAULT_DAX_LOCKED) {
*entry = vmf.entry;
return ret;
}
if (unlikely(PageHWPoison(vmf.page))) {
if (ret & VM_FAULT_LOCKED)
unlock_page(vmf.page);
put_page(vmf.page);
return VM_FAULT_HWPOISON;
}
if (unlikely(!(ret & VM_FAULT_LOCKED))) -------------(2)
lock_page(vmf.page);
else
VM_BUG_ON_PAGE(!PageLocked(vmf.page), vmf.page);
*page = vmf.page;
return ret;
}
- 这里调用了struct vm_operations_struct vm_ops的fault函数,还记得咱们上面用mmap映射文件的时候,对于ext4文件系统,vm_ops指向了ext4_file_vm_ops也就是调用了函数ext4_filemap_fault
static const struct vm_operations_struct ext4_file_vm_ops = {
.fault = ext4_filemap_fault,
.map_pages = filemap_map_pages,
.page_mkwrite = ext4_page_mkwrite,
};
- 此处如果没有返回VM_FAULT_LOCKED,则调用lock_page()设置PG_locked标志位,而此处的lock_page在拿不到PG_locked flag时会导致系统睡眠,将进程设置为UNINTERRUPTABLE sleep
ext4_filemap_fault里面的逻辑我们很容易就能读懂,vm_file就是咱们当时mmap的时候映射的那个文件,然后我们需要调用filemap_fault
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yB3kdkQu-1610634779816)(D:\学习总结\内存管理单元\image-20210113223040776.png)]
对于文件映射来说,一般这个文件会在物理内存里面有页面作为它的缓存,find_get_page就是找那个页,如果找到了,就调用,预读一些数据到内存里面;如果没有,就跳到no_cached_page,就调用page_cache_read,在这里显示分配一个缓存页。
页面缓存基于内存管理系统,同时又和文件系统打交道,是两者之间的一个重要的纽带,应用层读取文件的方法有mmap和read/write。linux一般会利用空闲的内存进file cache,只有接收到内存申请时,才会清理页面缓存,这个后面章节中单独学习。
1.2 do_cow_fault
do_cow_fault函数主要处理由写内存导致的缺页异常,而且VMA的属性是具有所有映射的,页就是处理私有文件映射的VMA中发生了写时复制。
其流程与上基本类似,主要是包括以下几个方面
- 申请一个新的页面,然后通过__do_fault将文件内容读取到fault page页面
- 如果fault page页面存在,则将fault page的内容复制到new page中,并将new page对应的PTE entry设置到硬件页表中
1.3 do_shared_fault
do_shared_fault函数处理共享文件映射时发生的缺页异常的情况,其代码实现在mm/memory.c中
static int do_shared_fault(struct fault_env *fe, pgoff_t pgoff)
{
struct vm_area_struct *vma = fe->vma;
struct page *fault_page;
struct address_space *mapping;
int dirtied = 0;
int ret, tmp;
ret = __do_fault(fe, pgoff, NULL, &fault_page, NULL); --------(1)
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
return ret;
if (vma->vm_ops->page_mkwrite) { --------(2)
unlock_page(fault_page);
tmp = do_page_mkwrite(vma, fault_page, fe->address);
if (unlikely(!tmp ||
(tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
put_page(fault_page);
return tmp;
}
}
ret |= alloc_set_pte(fe, NULL, fault_page); --------(3)
if (fe->pte)
pte_unmap_unlock(fe->pte, fe->ptl);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE |
VM_FAULT_RETRY))) {
unlock_page(fault_page);
put_page(fault_page);
return ret;
}
if (set_page_dirty(fault_page)) --------(4)
dirtied = 1;
mapping = page_rmapping(fault_page);
unlock_page(fault_page);
if ((dirtied || vma->vm_ops->page_mkwrite) && mapping) { --------(5)
/*
* Some device drivers do not set page.mapping but still
* dirty their pages
*/
balance_dirty_pages_ratelimited(mapping);
}
if (!vma->vm_ops->page_mkwrite)
file_update_time(vma->vm_file);
return ret;
}
- 调用__do_fault函数,并通过vma->vm_ops->fault回调函数来读取文件内容到vmf->page里
- 如果VMA的操作函数中定义了page_mkwrite方法,那么调用page_mkwrite来通知进程地址空间,页面将变成可写的。如果一个页面变成可写的,那么进程可能需要等待这个页面的内容回写成功
- 将新生成的PTE entry设置到硬件页表中
- 将page标记为dirty,然后将这个fault_page添加到文件页面的RMAP机制中
- 通过balance_dirty_pages_ratelimited()来平衡并回写一部分脏页
2 总结
do_fault
函数用于处理文件页异常,包括以下三种情况:
-
读文件页错误:
-
写私有文件页错误:
-
写共享文件页错误: