linux源码解析09–缺页异常之文件映射

时间:2023-02-14 18:14:24

接上篇 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;
}