接上篇
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 查找虚拟地址附近页面一般 16k,若存在,建立映射(预估虚拟地址附近页面很快会被读到,建立映射,减少缺页异常响应次数, 不创建 pagecache,不读磁盘);
4.2 若 page cache 不存在,分配物理页面;
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;
}