做人呢,最紧要就系开心啦

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

2,632次阅读
没有评论

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

正文完
 
admin
版权声明:本站原创文章,由 admin 2022-03-26发表,共计4286字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)