匿名和文件缺页中断

内存分类的方法有很多,其中有一种是怎么也绕不过去的分类方法:

  • 匿名页

  • 文件页

从前一直回避文件页的缺页处理,这次要好好梳理一下这两种缺页处理的区别。

mmap的预备知识

不论是匿名页还是文件页,都是通过mmap映射到进程空间,然后在访问时触发缺页中断。所以缺页中断中采用和中处理方式,在mmap时就已经定好了。

这部分内容在私有和共享映射有较为详细的描述。

缺页中断的区分

我们来看缺页中断中,是如何将文件页和匿名页区分处理的。

大页处理

static inline vm_fault_t create_huge_pmd(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	if (vma_is_anonymous(vma))
		return do_huge_pmd_anonymous_page(vmf);
	if (vma->vm_ops->huge_fault)
		return vma->vm_ops->huge_fault(vmf, PMD_ORDER);
	return VM_FAULT_FALLBACK;
}

PTE 处理

static vm_fault_t do_pte_missing(struct vm_fault *vmf)
{
	if (vma_is_anonymous(vmf->vma))
		return do_anonymous_page(vmf);
	else
		return do_fault(vmf);
}

从这两者我们可以看到,区分的重要判断是vma_is_anonymous()。

而从私有和共享映射中的分析,我们看到vma_is_anonymous()判断的是vma->vm_ops是否为空。而在四种组合中,只有私有匿名映射,vm_ops才为空。

文件页缺页中断

匿名页缺页中断比较熟悉了,这里我们重点看看文件页缺页中断。

do_fault
    if (!(vmf->flags & FAULT_FLAG_WRITE))
        do_read_fault()
    if (!(vma->vm_flags & VM_SHARED))
        do_cow_fault()
    else
        do_shared_fault()

到这里,我们终于能够理解四种映射情况对应的缺页中断处理了。

  • 私有匿名映射:do_anonymous_page()

  • 共享匿名映射:do_fault()

  • 私有文件映射:do_fault()

  • 共享文件映射:do_fault()

因为其余三者都有vm_ops,所以走的路径都是一样的。但是对操作类型:读/写,以及映射类型:共享/私有又做了区分。下面我们来仔细分析一下。

我们先来看看读。

do_read_fault()
    __do_fault()
        vma->vm_ops->fault() -> filemap_fault()
            folio = filemap_get_folio()
            vmf->page = folio_file_page(folio, index)
    finish_fault()
        folio_ref_add(folio, nr_pages - 1)
        set_pte_range()
            folio_add_file_rmap_ptes(folio, )

读很正常,这三种情况都会从文件中读取。

私有写

但是写就分为了私有写和共享写。当然私有写,只可能发生在私有文件映射。

有几种情况会走到私有写:

  • 原先这个页面从来没有读过,直接写了

  • 原先这个页面读取了,第一次写

  • 原先这个页面写过了,再次写

这几种情况会有区别吗?我们带着这些疑问展开代码。

do_cow_fault
    vmf_anon_prepare(vmf)
    folio = folio_prealloc()
    vmf->cow_page = &folio->page
    __do_fault()
        vma->vm_ops->fault() -> filemap_fault()
            folio = filemap_get_folio()
            vmf->page = folio_file_page(folio, index)
    copy_mc_user_highpage(vmf->cow_page, vmf->page...)
    finish_fault()
        folio_ref_add(folio, nr_pages - 1)
        set_pte_range()
            folio_add_new_anon_rmap(folio, )

首先看到的是,执行了vmf_anon_prepare()也就是准备了匿名反向映射的数据结构。但是这也满神奇的。因为一个filemap的vma,竟然还有anon_vma。

然后在finish_fault()->set_pte_range()中将folio->mapping设置为匿名映射。这样folio_test_anon()才会返回是匿名页。

中间通过filemap_fault()把原有的文件内容读取出来,并复制到了新分配的页面。这样就表示,私有写是在原有文件内容基础上的修改。

这样就回答了上面三个问题中的两个:从来没有度过或者是哪怕之前读过了,写的时候都是在原有文件内容基础上。

但是如果在同一页面的不同位置又写了一次呢?关键知识点:此时不会再进入缺页中断,因为第一次写时,页面被标记为可写。

至此,我们回答完了自己提出的三个问题。

共享写

现在就剩下一个共享写的缺页情况了。

do_shared_fault
    __do_fault()
    vma->vm_ops->page_mkwrite()
    finish_fault()

这么看好像挺简单的,不过我们目前为止一直没有打开背后真正的大boss -- filemap_fault()。

filemap_fault

在文件映射的缺页中断中,我们看到的是__do_fault()函数被调用。但是展开看实际调用的是vma->vm_ops->fault()。

大致扫了一眼,这个指针一般指向的是filemap_fault。就算不直接是filemap_fault,也会间接调用filemap_fault。所以文件缺页的关键处理绕不开这个函数。

下面只是一个粗略的大致流程。

filemap_fault
    file = vmf->vma->vm_file
    mapping = file->f_mapping
    index = vmf->pgoff
    folio = filemap_get_folio(mapping, index)                  // in pagecache?

    // if in pagecache
    do_async_mmap_readahead()
    // if not in pagecache
    fpin = do_sync_mmap_readahead()                            // sync readahead
        fpin = maybe_unlock_mmap_for_io(vmf, )
        page_cache_sync_ra(&ractl, ra->ra_pages)


    folio = __filemap_get_folio(mapping, index, FGP_CREAT|FGP_FOR_MMAP, vmf->gfp_mask)
        folio = filemap_get_entry(mapping, index)              // search in mapping

        // if not in pagecache, allocate one
        folio = filemap_alloc_folio(alloc_gfp, order)          // alloc and set refcount to 1
        filemap_add_folio(mapping, folio, index, gfp)          // add into pagecache
            __folio_set_locked(folio)
            __filemap_add_folio(mapping, folio, index, ...)
                folio_ref_add(folio, nr)                       // refcount = 1 + nr

    vmf->page = folio_file_page(folio, index)

finish_fault
    folio_ref_add(folio, nr_pages - 1)                         // refcount = 2 * nr

refcount变化

先来看看,如果是一个新的页面添加到pagecache时计数的变化:

  • filemap_alloc_folio: 1

  • __filemap_add_folio: +nr

  • finish_fault: +nr-1

所以最后计数值是2*nr,这个值和folio_expected_ref_count()得到值对应。

do_sync_mmap_readahead()

同步预读取也会分配folio,并添加到pagecache(同样是通过filemap_add_folio)。

所以__filemap_get_folio()拿不到内存,感觉是小概率事件。

Last updated

Was this helpful?