接上篇 https://www.daodaodao123.com/?p=776
本篇解析文件映射。
1.文件映射触发条件
(1)pte表项为空,且vma->vm_ops不为空,属于文件映射; (2)pte表项为空,且vma->vm_ops为空,属于匿名映射;
2.应用场景
(1)exec加载程序的时候,内核为代码段和数据段创建了私有的文件映射,映射到进程的虚拟地址空间,第一次访问时发生文件映射缺页异常。
(2)进程使用mmap创建文件映射,把一个文件的一个区间映射到进程的虚拟地址空间,第一次访问时发生文件映射缺页异常。
(3)匿名映射为共享时,走shmem,等同文件映射处理。
3.源码解析
do_fault()函数
static vm_fault_t do_fault(struct vm_fault *vmf)
{
...
if (!vma->vm_ops->fault) { ///处理没有实现fault()回调函数的情况,出错处理
...
} else if (!(vmf->flags & FAULT_FLAG_WRITE))
ret = do_read_fault(vmf); ///flags标记本次缺页异常由读内存引起
else if (!(vma->vm_flags & VM_SHARED))
ret = do_cow_fault(vmf); ///写内存引起,且为私有
else
ret = do_shared_fault(vmf); ///内存引起,且为共享
/* preallocated pagetable is unused: free it */
if (vmf->prealloc_pte) { ///释放prealloc_pte
pte_free(vm_mm, vmf->prealloc_pte);
vmf->prealloc_pte = NULL;
}
return ret;
}
重点三个函数,分别对应三种情形:
读页面
读取私有页面过程:
1.映射文件到vma,
char*p=mmap(NULL,SIZE,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0);
2.读访问
char a=*b;
3.触发缺页异常;
4.缺页异常处理: 4.1 在page cache查找虚拟地址附近页面,若存在,建立映射(预估虚拟地址附近页面很快会被读到,建立映射,减少缺页异常响应次数); 4.2 分配物理页面; 4.3 加入page cache; 4.4 从磁盘读取文件内容到page cache; 4.5 建立映射,设置权限只读;
5.返回异常发生处,继续执行,
char a=*b;
读取共享页面过程:
读取共享页面与私有页面流程几乎相同,唯一不同点:
读私有页面,降级为只读权限; 读共享页面,保持原有权限;
源码解析如下:
static vm_fault_t do_read_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
vm_fault_t 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).
*/
///如果定义了map_pages,将异常地址附近的页尽可能多的与高速缓存建立映射(只针对现存的页面缓存,不创建页面)
///预估异常地址附近页面高速缓存,可能很快会被读到
if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) { ///fault_around_bytes默认16页
ret = do_fault_around(vmf);
if (ret)
return ret;
}
ret = __do_fault(vmf); ///创建新的高速缓存,并将文件内容读到缓存
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
return ret;
ret |= finish_fault(vmf); ///填写表项,建立映射
unlock_page(vmf->page);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
put_page(vmf->page);
return ret;
}
写私有页面
写私有页面过程: 1.映射文件到vma,
char*p=mmap(NULL,SIZE,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0);
2.写访问
char *p=0x55;
3.触发缺页异常;
4.缺页异常处理: 4.1 分配物理页面cow_page(匿名页); 4.2 在page cache查找page; 4.3 若page已存在跳转4.5;若不存在分配page cache; 4.4 从磁盘读取文件内容到page cache; 4.5 复制page cache内容到cow_page; 4.6 建立映射,设置权限可写;
5.返回异常发生处,继续执行,
char *p=0x55;
注:此后除非回写,否则进程读写的数据只跟cow_page有关,其他进程从磁盘读取文件,还是文件原本内容;
源码解析:
static vm_fault_t do_cow_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
vm_fault_t ret;
if (unlikely(anon_vma_prepare(vma))) ///检查该vma是否初始化了RMAP
return VM_FAULT_OOM;
///分配一个物理页面,优先使用高端内存
vmf->cow_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
if (!vmf->cow_page)
return VM_FAULT_OOM;
if (mem_cgroup_charge(vmf->cow_page, vma->vm_mm, GFP_KERNEL)) {
put_page(vmf->cow_page);
return VM_FAULT_OOM;
}
cgroup_throttle_swaprate(vmf->cow_page, GFP_KERNEL);
ret = __do_fault(vmf); ///读取文件内容到vmf->page
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
goto uncharge_out;
if (ret & VM_FAULT_DONE_COW)
return ret;
///把vmf->page的内容复制到刚分配的cow_page
copy_user_highpage(vmf->cow_page, vmf-page, vmf->address, vma);
__SetPageUptodate(vmf->cow_page);
ret |= finish_fault(vmf); ///使用vmf->cow_page制作PTE,设置到物理页面,建立映射,并添加到RMAP机制
unlock_page(vmf->page);
put_page(vmf->page);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
goto uncharge_out;
return ret;
uncharge_out:
put_page(vmf->cow_page);
return ret;
}
写共享页面
写共享页面与写私有页面大致类似,不同点是: 1.不用分配cow_page; 2.写完后,会设置脏页;
写共享页面过程: 1.映射文件到vma,
char*p=mmap(NULL,SIZE,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
2.写访问
char *p=0x55;
3.触发缺页异常;
4.缺页异常处理: 4.2 在page cache查找page; 4.3 若page已存在跳转4.4;若不存在分配page cache; 4.4 从磁盘读取文件内容到page cache; 4.5 建立映射,设置权限可写; 4.6 设置脏页;
5.返回异常发生处,继续执行,
char *p=0x55;
注:设置脏页后,page cache会调度合适时机回写磁盘;
源码解析:
static vm_fault_t do_shared_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
vm_fault_t ret, tmp;
ret = __do_fault(vmf); ///读取文件内容到vmf->page
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
return ret;
/*
* Check if the backing address space wants to know that the page is
* about to become writable
*/ ///若定义了page_mkwrite,调用do_page_mkwrite通知进程地址空间,页面变成可写;若页面可写,进程需要等待这个页面的内容会写成功
if (vma->vm_ops->page_mkwrite) {
unlock_page(vmf->page);
tmp = do_page_mkwrite(vmf);
if (unlikely(!tmp ||
(tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
put_page(vmf->page);
return tmp;
}
}
ret |= finish_fault(vmf); ///使用vmf->page制作PTE,设置到物理页面,建立映射,并添加到RMAP机制
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE |
VM_FAULT_RETRY))) {
unlock_page(vmf->page);
put_page(vmf->page);
return ret;
}
ret |= fault_dirty_shared_page(vmf); ///设置脏页,回写部分脏页
return ret;
}