linux内存源码分析 - 内存压缩(同步关系)

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/

 

 

概述 

  最近在看内存回收,内存回收在进行同步的一些情况非常复杂,然后就想,不会内存压缩的页面迁移过程中的同步关系也那么复杂吧,带着好奇心就把页面迁移的源码都大致看了一遍,还好,不复杂,也容易理解,这里我们就说说在页面迁移过程中是如何进行同步的。不过首先可能没看过的朋友需要先看看linux内存源码分析 - 内存压缩(一),因为会涉及里面的一些知识。

  其实一句话可以概括页面迁移时是如何进行同步的,就是:我要开始对这个页进行页面迁移处理了,你们这些访问此页的进程都给我加入等待队列等着,我处理完了就会唤醒你们。

  如果需要详细些说,那就是内存压缩中即将对一个页进行迁移工作,首先会对这个旧页上锁(置位此页的PG_locked标志),然后新建一个页表项数据,这个页表项数据属于swap类型,这个页表项数据映射的是这个旧页,然后把这个页表项数据写入所有映射了此旧页的进程页表项中,将旧页数据和参数复制到新页里,再新建一个页表项数据,这个页表项数据就是常规的页表项数据,这个页表项数据映射的是这个新页,然后把这个页表项数据写入到之前所有映射了旧页的进程页表项中,释放锁,唤醒等待的进程。

  使用一张图就可以说明整个过程:

linux内存源码分析 - 内存压缩(同步关系)

  这里我们简单说一下什么叫做swap类型的页表项数据,我们知道,页表项中保存的一个重要的数据就是页内偏移量,还有一个重要标志位是此页在不在内存中,当我们将一个匿名页写入swap分区时,会将此匿名页在内存中占用的页框进行释放,而这样,映射了此匿名页的进程就没办法访问到处于磁盘上的匿名页了,内核需要提供一些手段,让这些进程能够有办法知道此匿名页不在内存中,然后尝试把这个匿名页放入内存中。内核提供的手段就是将映射了此页的进程页表项修改成一个特殊的页表项,当进程访问此页时,此特殊的页表项就会造成缺页异常,在缺页异常中,此特殊页表项会引领走到相应处理的地方。这个特殊的页表项就是swap类型的页表项,对于swap类型的页表项,它又分为2种,一种是它会表示此页不在内存中,并且页表项偏移量是匿名页所在swap分区页槽的索引。这种swap类型页表项能够正确引领缺页异常将应该换入的匿名页换入内存。而另一种,就是我们需要使用的页面迁移类型的页表项,它页会表示此页不在内存中,并且页表项偏移量是旧页的页框号,同样,这种页表项也会引领缺页异常将当前发生缺页异常的进程加入到此旧页的等待PG_locked清除的等待队列中。

  

页面迁移

  接下来我们可以直接上源码了,因为之前的文章也分析了很多,这篇我们只讲当一个页开始进行页面迁移时,内核的处理,我们可以直接从__unmap_and_move()函数看,此函数已经从上级函数中传入了待移动的页框和准备移入的页框的描述符,并且提供了内存压缩模式,这里的内存压缩模式几乎不会对我们本次分析造成实质性的影响,但是这里还是要说说几种区别:

  • 异步模式:不会进行任何阻塞操作,尝试移动的页都是MIGRATE_MOVABLE和MIGRATE_CMA类型的页框
  • 轻同步模式:会进行阻塞操作(比如设备繁忙,会等待一小会,锁繁忙,会阻塞直到拿到锁为止),但是不会阻塞在等待页面回写完成的路径上,会直接跳过正在回写的页,尝试移动的页是MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE和MIGRATE_CMA类型的页框
  • 同步模式:在轻同步模式的基础上,会阻塞在等待页面回写完成,然后再对此页进行处理。如果需要,也会对脏文件页进行回写,回写完成后再对此页进行移动(这种情况视文件系统而定)

  待移动的页框,一定是MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE和MIGRATE_CMA类型中的一种(文件页和匿名页),而准备移入的页框,肯定是一个空闲页框,并且相对于待移动的页框,它更靠近zone的末尾。

linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)
static int __unmap_and_move(struct page *page, struct page *newpage,
                int force, enum migrate_mode mode)
{
    int rc = -EAGAIN;
    int remap_swapcache = 1;
    struct anon_vma *anon_vma = NULL;

    /* 获取这个page锁(PG_locked标志)是关键,是整个回收过程中同步的保证 
     * 当我们对所有映射了此页的进程进行unmap操作时,会给它们一个特殊的页表项
     * 当这些进程再次访问此页时,会由于访问了这个特殊的页表项进入到缺页异常,然后在缺页异常中等待此页的这个锁释放
     * 当此页的这个锁释放时,页面迁移已经完成了,这些进程的此页表项已经在释放锁前就映射到了新页上,这时候已经可以唤醒这些等待着此页的锁的进程
     * 这些进程下次访问此页表项时,就是访问到了新页
     */
    if (!trylock_page(page)) {
        /* 异步此时一定需要拿到锁,否则就返回,因为下面还有一个lock_page(page)获取锁,这个有可能会导致阻塞等待 */
        if (!force || mode == MIGRATE_ASYNC)
            goto out;

        if (current->flags & PF_MEMALLOC)
            goto out;

        /* 同步和轻同步的情况下,都有可能会为了拿到这个锁而阻塞在这 */
        lock_page(page);
    }

    /* 此页正在回写到磁盘 */
    if (PageWriteback(page)) {
        /* 异步和轻同步模式都不会等待 */
        if (mode != MIGRATE_SYNC) {
            rc = -EBUSY;
            goto out_unlock;
        }
        if (!force)
            goto out_unlock;
        /* 同步模式下,等待此页回写完成 */
        wait_on_page_writeback(page);
    }

    /* 匿名页并且不使用于ksm的情况 */
    if (PageAnon(page) && !PageKsm(page)) {
        /* 获取匿名页所指向的anon_vma,如果是文件页,则返回NULL */
        anon_vma = page_get_anon_vma(page);
        if (anon_vma) {
            /*
             * 此页是匿名页,不做任何处理
             */
        } else if (PageSwapCache(page)) {
            /* 此页是已经加入到swapcache,并且进行过unmap的匿名页(因为anon_vma为空,才到这里,说明进行过unmap了),现在已经没有进程映射此页 */
            remap_swapcache = 0;
        } else {
            goto out_unlock;
        }
    }

    /* balloon使用的页 */
    if (unlikely(isolated_balloon_page(page))) {
        rc = balloon_page_migrate(newpage, page, mode);
        goto out_unlock;
    }

    /* page->mapping为空的情况,有两种情况
     * 1.此页是已经加入到swapcache,并且进行过unmap的匿名页,现在已经没有进程映射此页
     * 2.一些特殊的页,这些页page->mapping为空,但是page->private指向一个buffer_head链表(日志缓冲区使用的页?)
     */
    if (!page->mapping) {
        VM_BUG_ON_PAGE(PageAnon(page), page);
        /* page->private有buffer_head */
        if (page_has_private(page)) {
            /* 释放此页所有的buffer_head,之后此页将被回收 */
            try_to_free_buffers(page);
            goto out_unlock;
        }
        goto skip_unmap;
    }

    /* Establish migration ptes or remove ptes */
    /* umap此页,会为映射了此页的进程创建一个迁移使用的swp_entry_t,这个swp_entry_t指向的页就是此page 
     * 将此swp_entry_t替换映射了此页的页表项
     * 然后对此页的页描述符的_mapcount进行--操作,表明反向映射到的一个进程取消了映射
     */
    try_to_unmap(page, TTU_MIGRATION|TTU_IGNORE_MLOCK|TTU_IGNORE_ACCESS);

skip_unmap:
    /* 将page的内容复制到newpage中,会进行将newpage重新映射到page所属进程的pte中 */
    if (!page_mapped(page))
        rc = move_to_new_page(newpage, page, remap_swapcache, mode);

    /* 当在move_to_new_page()中进行remove_migration_ptes()失败时,这里才会执行 
     * 这里是将所有映射了旧页的进程页表项再重新映射到旧页上,也就是本次内存迁移失败了。
     */
    if (rc && remap_swapcache)
        remove_migration_ptes(page, page);

    /* Drop an anon_vma reference if we took one */
    if (anon_vma)
        put_anon_vma(anon_vma);

out_unlock:
    /* 释放此页的锁(PG_locked清除)
     * 在unmap后,所有访问此页的进程都会阻塞在这里,等待此锁释放
     * 这里释放后,所有访问此页的进程都会被唤醒
     */
    unlock_page(page);
out:
    return rc;
}
linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)

  这段代码一前一后的两个上锁,就是之前说的页面迁移时同步的重点,而且通过代码也可以看到,这个锁是一定要获取,才能够继续进行页面迁移的。当处于异步模式时,如果没获取到锁,就直接跳出,取消对此页的处理了。而轻同步和同步模式时,就会对此锁不拿到不死心。对于这个函数主要的函数入口就两个,一个try_to_unmap(),一个是move_to_new_page()。

  try_to_unmap()函数是对此页进行反向映射,对每一个映射了此页的进程页表进行处理,注意TTU_MIGRATION标志,代表着这次反向映射是为了页面迁移而进行的,而TTU_IGNORE_MLOCK标志,也代表着内存压缩是可以对mlock在内存中的页框进行的。如之前所说,在try_to_unmap()函数中,主要工作就是一件事情,生成一个swap类型的页表项数据,将此页表项数据设置为页面迁移使用的数据,然后将此页表项数据写入到每一个映射了此待移动页的进程页表项中。我们进入此函数看看:

linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)
int try_to_unmap(struct page *page, enum ttu_flags flags)
{
    int ret;
    /* 反向映射控制结构 */
    struct rmap_walk_control rwc = {
        /* 对一个vma所属页表进行unmap操作
         * 每次获取一个vma就会对此vma调用一次此函数,在函数里第一件事就是判断获取的vma有没有映射此page
         */
        .rmap_one = try_to_unmap_one,
        .arg = (void *)flags,
        /* 对一个vma进行unmap后会执行此函数 */
        .done = page_not_mapped,
        .file_nonlinear = try_to_unmap_nonlinear,
        /* 用于对整个anon_vma的红黑树进行上锁,用读写信号量,锁是aon_vma的rwsem */
        .anon_lock = page_lock_anon_vma_read,
    };

    VM_BUG_ON_PAGE(!PageHuge(page) && PageTransHuge(page), page);

    if ((flags & TTU_MIGRATION) && !PageKsm(page) && PageAnon(page))
        rwc.invalid_vma = invalid_migration_vma;

    /* 里面会对所有映射了此页的vma进行遍历,具体见反向映射 */
    ret = rmap_walk(page, &rwc);

    /* 没有vma要求此页锁在内存中,并且page->_mapcount为-1了,表示没有进程映射了此页 */
    if (ret != SWAP_MLOCK && !page_mapped(page))
        ret = SWAP_SUCCESS;
    return ret;
}
linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)

  反向映射原理具体见linux内存源码分析 - 内存回收(匿名页反向映射),这里就不详细说明了,说说这个函数,这个函数有一个最重要的函数指针,就是rmap_one,它指向try_to_unmap_one()函数,这个函数在每访问一个vma时,就会调用一次,无论此vma有没有映射此页,而反向映射走的流程都在rmap_walk中,这里我们就不看了,主要看try_to_unmap_one()函数:

linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)
/*
 * 对vma进行unmap操作,并对此页的page->_mapcount--,这里面的页可能是文件页也可能是匿名页
 * page: 目标page
 * vma: 获取到的vma
 * address: page在vma所属的进程地址空间中的线性地址
 */
static int try_to_unmap_one(struct page *page, struct vm_area_struct *vma,
             unsigned long address, void *arg)
{
    struct mm_struct *mm = vma->vm_mm;
    pte_t *pte;
    pte_t pteval;
    spinlock_t *ptl;
    int ret = SWAP_AGAIN;
    enum ttu_flags flags = (enum ttu_flags)arg;

    /* 先检查此vma有没有映射此page,有则返回此page在此进程地址空间的页表项 */
    /* 检查page有没有映射到mm这个地址空间中
     * address是page在此vma所属进程地址空间的线性地址,获取方法: address = vma->vm_pgoff + page->pgoff << PAGE_SHIFT;
     * 通过线性地址address获取对应在此进程地址空间的页表项,然后通过页表项映射的页框号和page的页框号比较,则知道页表项是否映射了此page
     * 会对页表上锁
     */
    pte = page_check_address(page, mm, address, &ptl, 0);
    /* pte为空,则说明page没有映射到此mm所属的进程地址空间,则跳到out */
    if (!pte)
        goto out;

    /* 如果flags没有要求忽略mlock的vma */
    if (!(flags & TTU_IGNORE_MLOCK)) {
        /* 如果此vma要求里面的页都锁在内存中,则跳到out_mlock */
        if (vma->vm_flags & VM_LOCKED)
            goto out_mlock;

        /* flags标记了对vma进行mlock释放模式,则跳到out_unmap,因为这个函数中只对vma进行unmap操作 */
        if (flags & TTU_MUNLOCK)
            goto out_unmap;
    }
    /* 忽略页表项中的Accessed */
    if (!(flags & TTU_IGNORE_ACCESS)) {
        /* 清除页表项的Accessed标志 */
        if (ptep_clear_flush_young_notify(vma, address, pte)) {
            /* 清除失败,发生在清除后检查是否为0 */
            ret = SWAP_FAIL;
            goto out_unmap;
        }
      }

    /* Nuke the page table entry. */
    /* 空函数 */
    flush_cache_page(vma, address, page_to_pfn(page));
    /* 获取页表项内容,保存到pteval中,然后清空页表项 */
    pteval = ptep_clear_flush(vma, address, pte);

    /* Move the dirty bit to the physical page now the pte is gone. */
    /* 如果页表项标记了此页为脏页 */
    if (pte_dirty(pteval))
        /* 设置页描述符的PG_dirty标记 */
        set_page_dirty(page);

    /* Update high watermark before we lower rss */
    /* 更新进程所拥有的最大页框数 */
    update_hiwater_rss(mm);

    /* 此页是被标记为"坏页"的页,这种页用于内核纠正一些错误,是否用于边界检查? */
    if (PageHWPoison(page) && !(flags & TTU_IGNORE_HWPOISON)) {
        /* 非大页 */
        if (!PageHuge(page)) {
            /* 是匿名页,则mm的MM_ANONPAGES-- */
            if (PageAnon(page))
                dec_mm_counter(mm, MM_ANONPAGES);
            else
                /* 此页是文件页,则mm的MM_FILEPAGES-- */
                dec_mm_counter(mm, MM_FILEPAGES);
        }
        /* 设置页表项新的内容为 swp_entry_to_pte(make_hwpoison_entry(page)) */
        set_pte_at(mm, address, pte,
               swp_entry_to_pte(make_hwpoison_entry(page)));
    } else if (pte_unused(pteval)) {
        /* 一些架构上会有这种情况,X86不会调用到这个判断中 */
        if (PageAnon(page))
            dec_mm_counter(mm, MM_ANONPAGES);
        else
            dec_mm_counter(mm, MM_FILEPAGES);
    } else if (PageAnon(page)) {
        /* 此页为匿名页处理 */

        /* 获取page->private中保存的内容,调用到try_to_unmap()前会把此页加入到swapcache,然后分配一个以swap页槽偏移量为内容的swp_entry_t */
        swp_entry_t entry = { .val = page_private(page) };
        pte_t swp_pte;

        /* 对于内存回收,基本都是这种情况,因为page在调用到这里之前已经被移动到了swapcache 
         * 而对于内存压缩,
         */
        if (PageSwapCache(page)) {
            /* 检查entry是否有效
              * 并且增加entry对应页槽在swap_info_struct的swap_map的数值,此数值标记此页槽的页有多少个进程引用
              */
            if (swap_duplicate(entry) < 0) {
                /* 检查失败,把原来的页表项内容写回去 */
                set_pte_at(mm, address, pte, pteval);
                /* 返回值为SWAP_FAIL */
                ret = SWAP_FAIL;
                goto out_unmap;
            }
            
            /* entry有效,并且swap_map中目标页槽的数值也++了 */
            /* 这个if的情况是此vma所属进程的mm没有加入到所有进程的mmlist中(init_mm.mmlist) */
            if (list_empty(&mm->mmlist)) {
                spin_lock(&mmlist_lock);
                if (list_empty(&mm->mmlist))
                    list_add(&mm->mmlist, &init_mm.mmlist);
                spin_unlock(&mmlist_lock);
            }
            /* 减少此mm的匿名页统计 */
            dec_mm_counter(mm, MM_ANONPAGES);
            /* 增加此mm的页表中标记了页在swap的页表项的数量 */
            inc_mm_counter(mm, MM_SWAPENTS);
        } else if (IS_ENABLED(CONFIG_MIGRATION)) {
            /* 执行到这里,就是对匿名页进行页面迁移工作(内存压缩时使用) */
            
            /* 如果flags没有标记此次是在执行页面迁移操作 */
            BUG_ON(!(flags & TTU_MIGRATION));
            /* 为此匿名页创建一个页迁移使用的swp_entry_t,此swp_entry_t指向此匿名页 */
            entry = make_migration_entry(page, pte_write(pteval));
        }
        /*
         * 这个entry有两种情况,保存在page->private中的以在swap中页槽偏移量为数据的swp_entry_t
         * 另一种是一个迁移使用的swp_entry_t
         */
        /* 将entry转为一个页表项 */
        swp_pte = swp_entry_to_pte(entry);
        /* 页表项有一位用于_PAGE_SOFT_DIRTY,用于kmemcheck */
        if (pte_soft_dirty(pteval))
            swp_pte = pte_swp_mksoft_dirty(swp_pte);
        /* 将配置好的新的页表项swp_pte写入页表项中 */
        set_pte_at(mm, address, pte, swp_pte);

        /* 如果页表项表示映射的是一个文件,则是一个bug。因为这里处理的是匿名页,主要检查页表项中的_PAGE_FILE位 */
        BUG_ON(pte_file(*pte));
    } else if (IS_ENABLED(CONFIG_MIGRATION) &&
           (flags & TTU_MIGRATION)) {
        /* 本次调用到此是对文件页进行页迁移操作的,会为映射了此文件页的进程创建一个swp_entry_t,这个swp_entry_t指向此文件页 */
        /* Establish migration entry for a file page */
        swp_entry_t entry;
        
        /* 建立一个迁移使用的swp_entry_t,用于文件页迁移 */
        entry = make_migration_entry(page, pte_write(pteval));
        /* 将此页表的pte页表项写入entry转为的页表项内容 */
        set_pte_at(mm, address, pte, swp_entry_to_pte(entry));
    } else
        /* 此页是文件页,仅对此mm的文件页计数--,文件页不需要设置页表项,只需要对页表项进行清空 */
        dec_mm_counter(mm, MM_FILEPAGES);

    /* 如果是匿名页,上面的代码已经将匿名页对应于此进程的页表项进行修改了 */

    /* 主要对此页的页描述符的_mapcount进行--操作,当_mapcount为-1时,表示此页已经没有页表项映射了 */
    page_remove_rmap(page);
    /* 每个进程对此页进行了unmap操作,此页的page->_count--,并判断是否为0,如果为0则释放此页,一般这里不会为0 */
    page_cache_release(page);

out_unmap:
    pte_unmap_unlock(pte, ptl);
    if (ret != SWAP_FAIL && !(flags & TTU_MUNLOCK))
        mmu_notifier_invalidate_page(mm, address);
out:
    return ret;

out_mlock:
    pte_unmap_unlock(pte, ptl);

    if (down_read_trylock(&vma->vm_mm->mmap_sem)) {
        if (vma->vm_flags & VM_LOCKED) {
            mlock_vma_page(page);
            ret = SWAP_MLOCK;
        }
        up_read(&vma->vm_mm->mmap_sem);
    }
    return ret;
}
linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)

  此函数很长,原因是把所有可能进行反向映射unmap操作的情况都写进去了,比如说内存回收和我们现在说的页面迁移。需要注意,此函数一开始第一件事情,就是判断此vma是否映射了此页,通过page_check_address()进行判断,判断条件也很简单,通过page->index保存的虚拟页框号,与此vma起始的虚拟页框号相减,得到一个以页为单位的偏移量,这个偏移量与vma起始线性地址相加,就得到了此页在此进程地址空间的线性地址,然后通过线性地址找到对应的页表项,页表项中映射的物理页框号是否与此页的物理页框号相一致,一致则说明此vma映射了此页。其实对我们页面迁移来说,涉及到的代码并不多,如下:

linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)
            。。。。。。

        } else if (IS_ENABLED(CONFIG_MIGRATION)) {
            /* 执行到这里,就是对匿名页进行页面迁移工作(内存压缩时使用) */
            
            /* 如果flags没有标记此次是在执行页面迁移操作 */
            BUG_ON(!(flags & TTU_MIGRATION));
            /* 为此匿名页创建一个页迁移使用的swp_entry_t,此swp_entry_t指向此匿名页 */
            entry = make_migration_entry(page, pte_write(pteval));
        }
        /*
         * 这个entry有两种情况,保存在page->private中的以在swap中页槽偏移量为数据的swp_entry_t
         * 另一种是一个迁移使用的swp_entry_t
         */
        /* 将entry转为一个页表项 */
        swp_pte = swp_entry_to_pte(entry);
        /* 页表项有一位用于_PAGE_SOFT_DIRTY,用于kmemcheck */
        if (pte_soft_dirty(pteval))
            swp_pte = pte_swp_mksoft_dirty(swp_pte);
        /* 将配置好的新的页表项swp_pte写入页表项中 */
        set_pte_at(mm, address, pte, swp_pte);

        /* 如果页表项表示映射的是一个文件,则是一个bug。因为这里处理的是匿名页,主要检查页表项中的_PAGE_FILE位 */
        BUG_ON(pte_file(*pte));
    } else if (IS_ENABLED(CONFIG_MIGRATION) &&
           (flags & TTU_MIGRATION)) {
        /* 本次调用到此是对文件页进行页迁移操作的,会为映射了此文件页的进程创建一个swp_entry_t,这个swp_entry_t指向此文件页 */
        /* Establish migration entry for a file page */
        swp_entry_t entry;
        
        /* 建立一个迁移使用的swp_entry_t,用于文件页迁移 */
        entry = make_migration_entry(page, pte_write(pteval));
        /* 将此页表的pte页表项写入entry转为的页表项内容 */
        set_pte_at(mm, address, pte, swp_entry_to_pte(entry));
    } else
        /* 此页是文件页,仅对此mm的文件页计数--,文件页不需要设置页表项,只需要对页表项进行清空 */
        dec_mm_counter(mm, MM_FILEPAGES);

    /* 如果是匿名页,上面的代码已经将匿名页对应于此进程的页表项进行修改了 */

    /* 主要对此页的页描述符的_mapcount进行--操作,当_mapcount为-1时,表示此页已经没有页表项映射了 */
    page_remove_rmap(page);
    /* 每个进程对此页进行了unmap操作,此页的page->_count--,并判断是否为0,如果为0则释放此页,一般这里不会为0 */
    page_cache_release(page);

out_unmap:
    pte_unmap_unlock(pte, ptl);
    if (ret != SWAP_FAIL && !(flags & TTU_MUNLOCK))
        mmu_notifier_invalidate_page(mm, address);
out:
    return ret;

out_mlock:
    pte_unmap_unlock(pte, ptl);

    if (down_read_trylock(&vma->vm_mm->mmap_sem)) {
        if (vma->vm_flags & VM_LOCKED) {
            mlock_vma_page(page);
            ret = SWAP_MLOCK;
        }
        up_read(&vma->vm_mm->mmap_sem);
    }
    return ret;
}        
linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)

  这里的代码就将文件页和匿名页的页面迁移的情况都包括了,这里是通过make_migration_entry()生成了之前说的用于页面迁移的swap类型页表项,然后通过set_pte_at()写入到进程对应的页表项中。经过这里的处理,这个旧页里的数据已经没有进程能够访问到了,当进程此时尝试访问此页框时,就会被加入到等待消除此页PG_locked的等待队列中。这里注意:是根据映射了此旧页的进程页表项而生成一个迁移使用的swap类型的页表项,也就是进程页表项中一些标志会保存到了swap类型页表项中。并且文件页和非文件页都会生成一个迁移使用的swap类型的页表项。而在内存回收过程中,也会使用这个swap类型的页表项,但是不是迁移类型的,并且只会是用于非文件页。

  好的,这时候所有的进程都没办法访问这个旧页了,下面的工作就是建立一个新页,将旧页的数据参数移动到新页上,这个工作是由move_to_new_page()函数来做,在调用move_to_new_page()前会通过page_mapped(page)判断这个旧页还有没有进程映射了它,没有才能进行,这里我们直接看move_to_new_page()函数:

linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)
static int move_to_new_page(struct page *newpage, struct page *page,
                int remap_swapcache, enum migrate_mode mode)
{
    struct address_space *mapping;
    int rc;

    /* 对新页上锁,这里应该100%上锁成功,因为此页是新的,没有任何进程和模块使用 */
    if (!trylock_page(newpage))
        BUG();

    /* Prepare mapping for the new page.*/
    /* 将旧页的index、mapping和PG_swapbacked标志复制到新页 
     * 对于复制index和mapping有很重要的意义
     * 通过index和mapping,就可以对新页进行反向映射了,当新页配置好后,对新页进行反向映射,找到的就是映射了旧页的进程,然后将它们的对应页表项映射到新页
     */
    newpage->index = page->index;
    newpage->mapping = page->mapping;
    if (PageSwapBacked(page))
        SetPageSwapBacked(newpage);

    /* 获取旧页的mapping */
    mapping = page_mapping(page);
    /* 如果mapping为空,则执行默认的migrate_page() 
     * 注意到这里时,映射了此页的进程已经对此页进行了unmap操作,而进程对应的页表项被设置为了指向page(而不是newpage)的swp_entry_t
     */
    if (!mapping)
        /* 未加入到swapcache中的匿名页会在这里进行页面迁移 */
        rc = migrate_page(mapping, newpage, page, mode);
    else if (mapping->a_ops->migratepage)
        /* 文件页,和加入到swapcache中的匿名页,都会到这里 
         * 对于匿名页,调用的是swap_aops->migrate_page()函数,而这个函数,实际上就是上面的migrate_page()函数
         * 根据文件系统的不同,这里可能会对脏文件页造成回写,只有同步模式才能进行回写
         */
        rc = mapping->a_ops->migratepage(mapping,
                        newpage, page, mode);
    else
        /* 当文件页所在的文件系统没有支持migratepage()函数时,会调用这个默认的函数,里面会对脏文件页进行回写,只有同步模式才能进行 */
        rc = fallback_migrate_page(mapping, newpage, page, mode);

    if (rc != MIGRATEPAGE_SUCCESS) {
        newpage->mapping = NULL;
    } else {
        mem_cgroup_migrate(page, newpage, false);
        /* 这个remap_swapcache默认就是1
         * 这里做的工作就是将之前映射了旧页的页表项,统统改为映射到新页,会使用到反向映射
         */
        if (remap_swapcache)
            remove_migration_ptes(page, newpage);
        page->mapping = NULL;
    }

    /* 释放newpage的PG_locked标志 */
    unlock_page(newpage);

    return rc;
}
linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)

  这里有两个重要函数,一个是文件系统对应的migrate_page()函数,一个就是后面的remove_migration_ptes()函数,对于migrate_page()函数,实质就是将旧页的参数和数据复制到新页中,而remove_migration_ptes()函数,是对新页进行一次反向映射(新页已经从旧页中复制好了,新的的反向映射效果和旧页的反向映射效果一模一样),然后将所有被修改为swap类型的进程页表项都重新设置为映射了新页的页表项。

  我们先看migrate_page(),这里只拿匿名页的migrate_page()函数进行说明,因为比较清晰易懂:

linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)
/* 未加入到swapcache和加入到swapcache中的匿名页都会在这里进行页面迁移 */
int migrate_page(struct address_space *mapping,
        struct page *newpage, struct page *page,
        enum migrate_mode mode)
{
    int rc;

    /* 页都没加入到swapcache,更不可能会正在进行回写 */
    BUG_ON(PageWriteback(page));    /* Writeback must be complete */

    /* 此函数主要工作就是如果旧页有加入到address_space的基树中,那么就用新页替换这个旧页的slot,新页替换旧页加入address_space的基树中
      * 并且会同步旧匿名页的PG_swapcache标志和private指针内容到新页
     * 对旧页会page->_count--(从基树中移除)
     * 对新页会page->_count++(加入到基树中)
     */
    rc = migrate_page_move_mapping(mapping, newpage, page, NULL, mode, 0);

    if (rc != MIGRATEPAGE_SUCCESS)
        return rc;
    
    /* 将page页的内容复制的newpage
     * 再对一些标志进行复制
     */
    migrate_page_copy(newpage, page);
    return MIGRATEPAGE_SUCCESS;
}
linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)

  这里面又有两个函数,migrate_page_move_mapping()和migrate_page_copy(),先看第一个,migrate_page_move_mapping()的作用是将旧页在address_space的基树结点中的数据替换为新页

linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)
/* 此函数主要工作就是如果旧页有加入到address_space的基树中,那么就用新页替换这个旧页的slot,新页替换旧页加入address_space的基树中
 * 并且会同步旧匿名页的PG_swapcache标志和private指针内容到新页
 * 对于未加入到swapcache中的匿名页,head = NULL,extra_count = 0 
 */
int migrate_page_move_mapping(struct address_space *mapping,
        struct page *newpage, struct page *page,
        struct buffer_head *head, enum migrate_mode mode,
        int extra_count)
{
    int expected_count = 1 + extra_count;
    void **pslot;

    /* 这里主要判断未加入swapcache中的旧匿名页(page)
     * 对于未加入到swapcache中的旧匿名页,只要page->_count为1,就说明可以直接进行迁移
     * page->_count为1说明只有隔离函数对此进行了++,其他地方没有引用此页
     * page->_count为1,直接返回MIGRATEPAGE_SUCCESS
     */
    if (!mapping) {
        /* Anonymous page without mapping */
        if (page_count(page) != expected_count)
            return -EAGAIN;
        return MIGRATEPAGE_SUCCESS;
    }

    /* 以下是对page->mapping不为空的情况 */

    /* 对mapping中的基树上锁 */
    spin_lock_irq(&mapping->tree_lock);

    /* 获取此旧页所在基树中的slot */
    pslot = radix_tree_lookup_slot(&mapping->page_tree,
                     page_index(page));


    /* 对于加入了address_space的基树中的旧页
     * 判断page->_count是否为2 + page_has_private(page)
     * 如果正确,则往下一步走
     * 如果不是,可能此旧页被某个进程映射了
     */
    expected_count += 1 + page_has_private(page);
    if (page_count(page) != expected_count ||
        radix_tree_deref_slot_protected(pslot, &mapping->tree_lock) != page) {
        spin_unlock_irq(&mapping->tree_lock);
        return -EAGAIN;
    }

    /* 这里再次判断,这里就只判断page->_count是否为2 + page_has_private(page)了
     * 是的话就继续往下走
     * 如果不是,可能此旧页被某个进程映射了
     */
    if (!page_freeze_refs(page, expected_count)) {
        spin_unlock_irq(&mapping->tree_lock);
        return -EAGAIN;
    }

    if (mode == MIGRATE_ASYNC && head &&
            !buffer_migrate_lock_buffers(head, mode)) {
        page_unfreeze_refs(page, expected_count);
        spin_unlock_irq(&mapping->tree_lock);
        return -EAGAIN;
    }

    /* 如果走到这,上面的代码得出一个结论,page是处于page->mapping指向的address_space的基树中的,并且没有进程映射此页 
     * 所以以下要做的,就是用新页(newpage)数据替换旧页(page)数据所在的slot
     */

    /* 新的页的newpage->_count++,因为后面要把新页替换旧页所在的slot */
    get_page(newpage);    
    /* 如果是匿名页,走到这,此匿名页必定已经加入了swapcache */
    if (PageSwapCache(page)) {
        /* 设置新页也在swapcache中,后面会替换旧页,新页就会加入到swapcache中 */
        SetPageSwapCache(newpage);
        /* 将旧页的private指向的地址复制到新页的private 
         * 对于加入了swapcache中的页,这项保存的都是以swap分区页槽为索引的swp_entry_t
         * 这里注意与在内存压缩时unmap时写入进程页表项的swp_entry_t的区别,在内存压缩时,写入进程页表项的swp_entry_t是以旧页(page)为索引
         */
        set_page_private(newpage, page_private(page));
    }

    /* 用新页数据替换旧页的slot */
    radix_tree_replace_slot(pslot, newpage);

    /* 设置旧页的page->_count为expected_count - 1 
     * 这个-1是因为此旧页已经算是从address_space的基树中拿出来了
     */
    page_unfreeze_refs(page, expected_count - 1);

    /* 统计,注意,加入到swapcache中的匿名页,也算作NR_FILE_PAGES的数量 */
    __dec_zone_page_state(page, NR_FILE_PAGES);
    __inc_zone_page_state(newpage, NR_FILE_PAGES);
    if (!PageSwapCache(page) && PageSwapBacked(page)) {
        __dec_zone_page_state(page, NR_SHMEM);
        __inc_zone_page_state(newpage, NR_SHMEM);
    }
    spin_unlock_irq(&mapping->tree_lock);

    return MIGRATEPAGE_SUCCESS;
}
linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)

  而migrate_page_copy()则非常简单,通过memcpy()将旧页的数据拷贝到新页中,然后将一些旧页的参数也拷贝到新页的页描述符中

linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)
/* 将page页的内容复制的newpage
 * 再对一些标志进行复制
 */
void migrate_page_copy(struct page *newpage, struct page *page)
{
    int cpupid;

    if (PageHuge(page) || PageTransHuge(page))
        /* 大页调用 */
        copy_huge_page(newpage, page);
    else
        /* 普通页调用,主要就是通过永久映射分配给两个页内核的线性地址,然后做memcpy,将旧页内容拷贝到新页 
         * 对于64位机器,就没必要使用永久映射了,直接memcpy
         */
        copy_highpage(newpage, page);

    /* 对页标志的复制 */
    if (PageError(page))
        SetPageError(newpage);
    if (PageReferenced(page))
        SetPageReferenced(newpage);
    if (PageUptodate(page))
        SetPageUptodate(newpage);
    if (TestClearPageActive(page)) {
        VM_BUG_ON_PAGE(PageUnevictable(page), page);
        SetPageActive(newpage);
    } else if (TestClearPageUnevictable(page))
        SetPageUnevictable(newpage);
    if (PageChecked(page))
        SetPageChecked(newpage);
    if (PageMappedToDisk(page))
        SetPageMappedToDisk(newpage);

    /* 如果页标记了脏页 */
    if (PageDirty(page)) {
        /* 清除旧页的脏页标志 */
        clear_page_dirty_for_io(page);

        /* 设置新页的脏页标志 */
        if (PageSwapBacked(page))
            SetPageDirty(newpage);
        else
            __set_page_dirty_nobuffers(newpage);
     }

    /* 还是复制一些标志 */
    cpupid = page_cpupid_xchg_last(page, -1);
    page_cpupid_xchg_last(newpage, cpupid);

    /* 这里也是做一些标志的复制 
     * 主要是PG_mlocked和ksm的stable_node
     */
    mlock_migrate_page(newpage, page);
    ksm_migrate_page(newpage, page);

    /* 清除旧页的几个标志,这几个标志在之前都赋给了新页了 */
    ClearPageSwapCache(page);
    ClearPagePrivate(page);
    set_page_private(page, 0);

    /*
     * If any waiters have accumulated on the new page then
     * wake them up.
     */
    /* 这里主要用于唤醒等待新页的等待者 */
    if (PageWriteback(newpage))
        end_page_writeback(newpage);
}
linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)

  好了,到这里,实际上整个新页已经设置好了,只不过因为页表项的关系,也没有进程能够访问这个新页,最后一个处理过程,就是重新将那些进程的页表项设置为映射到新页上,这个工作在之前列出的move_to_new_page()中的remove_migration_ptes()函数中进行,在remove_migration_ptes()中,也是进行了一次反向映射:

linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)
static void remove_migration_ptes(struct page *old, struct page *new)
{
    /* 反向映射控制结构 */
    struct rmap_walk_control rwc = {
        /* 每获取一个vma就会调用一次此函数 */
        .rmap_one = remove_migration_pte,
        /* rmap_one的最后一个参数为旧的页框 */
        .arg = old,
        .file_nonlinear = remove_linear_migration_ptes_from_nonlinear,
    };

    /* 反向映射遍历vma函数 */
    rmap_walk(new, &rwc);
}
linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)

  这里就直接看remove_migration_pte()函数了,也不难,直接看:

linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)
static int remove_migration_pte(struct page *new, struct vm_area_struct *vma,
                 unsigned long addr, void *old)
{
    struct mm_struct *mm = vma->vm_mm;
    swp_entry_t entry;
     pmd_t *pmd;
    pte_t *ptep, pte;
     spinlock_t *ptl;

    /* 新的页是大页(新的页与旧的页大小一样,说明旧的页也是大页) */
    if (unlikely(PageHuge(new))) {
        ptep = huge_pte_offset(mm, addr);
        if (!ptep)
            goto out;
        ptl = huge_pte_lockptr(hstate_vma(vma), mm, ptep);
    } else {
        /* 新的页是普通4k页 */
        /* 这个addr是new和old在此进程地址空间中对应的线性地址,new和old会有同一个线性地址,因为new是old复制过来的 */
        /* 获取线性地址addr对应的页中间目录项 */
        pmd = mm_find_pmd(mm, addr);
        if (!pmd)
            goto out;

        /* 根据页中间目录项和addr,获取对应的页表项指针 */
        ptep = pte_offset_map(pmd, addr);

        /* 获取页中间目录项的锁 */
        ptl = pte_lockptr(mm, pmd);
    }

    /* 上锁 */
     spin_lock(ptl);
    /* 获取页表项内容 */
    pte = *ptep;

    /* 页表项内容不是swap类型的页表项内容(页迁移页表项属于swap类型的页表项),则准备跳出 */
    if (!is_swap_pte(pte))
        goto unlock;

    
    /* 根据页表项内存转为swp_entry_t类型 */
    entry = pte_to_swp_entry(pte);

    /* 如果这个entry不是页迁移类型的entry,或者此entry指向的页不是旧页,那就说明有问题,准备跳出 */
    if (!is_migration_entry(entry) ||
        migration_entry_to_page(entry) != old)
        goto unlock;

    /* 新页的page->_count++ */
    get_page(new);
    /* 根据新页new创建一个新的页表项内容 */
    pte = pte_mkold(mk_pte(new, vma->vm_page_prot));
    /* 这个好像跟numa有关,先不用理,无伤大雅 */
    if (pte_swp_soft_dirty(*ptep))
        pte = pte_mksoft_dirty(pte);

    /* Recheck VMA as permissions can change since migration started  */
    /* 如果获取的entry标记了映射页可写 */
    if (is_write_migration_entry(entry))
        /* 给新页的页表项增加可写标志 */
        pte = maybe_mkwrite(pte, vma);

#ifdef CONFIG_HUGETLB_PAGE
    /* 大页的情况,先不看 */
    if (PageHuge(new)) {
        pte = pte_mkhuge(pte);
        pte = arch_make_huge_pte(pte, vma, new, 0);
    }
#endif
    flush_dcache_page(new);
    /* 将设置好的新页的页表项内容写到对应页表项中,到这里,此页表项原来映射的是旧页,现在变成映射了新页了 */
    set_pte_at(mm, addr, ptep, pte);

    /* 大页,先不看 */
    if (PageHuge(new)) {
        if (PageAnon(new))
            hugepage_add_anon_rmap(new, vma, addr);
        else
            page_dup_rmap(new);
    /* 针对匿名页 */
    } else if (PageAnon(new))
        /* 主要对page->_count++,因为多了一个进程映射此页 */
        page_add_anon_rmap(new, vma, addr);
    else
        /* 针对文件页,同样,也是对page->_count++,因为多了一个进程映射此页 */
        page_add_file_rmap(new);

    /* No need to invalidate - it was non-present before */
    /* 刷新tlb */
    update_mmu_cache(vma, addr, ptep);
unlock:
    /* 释放锁 */
    pte_unmap_unlock(ptep, ptl);
out:
    return SWAP_AGAIN;
}
linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)

  好的,到这里整个流程就理了一遍,不过我们发现,这整个流程都没有将旧页框释放的过程,实际上,这个旧页框释放过程在最开始看的函数__unmap_and_move()的上一级,因为此旧页是从lru中隔离出来的。所以在它已经迁移到新页后,它的page->_count为1,当从隔离状态放回lru时,这个page->_count会--,这时候系统会发现此页框的page->_count为0,就直接释放到伙伴系统中了。

  最后我们再简单看看当进程设置了swap类型的页面迁移页表项时,在缺页中断中走的路径,由于前面的路径太长,我主要把后面的路径列出来,而前面的路径是:do_page_fault() -> __do_page_fault() -> handle_mm_fault() -> __handle_mm_fault() -> handle_pte_fault() -> do_swap_page();最后到do_swap_page()就可以看到是怎么处理,这里只截一部分代码:

linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)
static int do_swap_page(struct mm_struct *mm, struct vm_area_struct *vma,
        unsigned long address, pte_t *page_table, pmd_t *pmd,
        unsigned int flags, pte_t orig_pte)
{
    spinlock_t *ptl;
    struct page *page, *swapcache;
    struct mem_cgroup *memcg;
    swp_entry_t entry;
    pte_t pte;
    int locked;
    int exclusive = 0;
    int ret = 0;

    if (!pte_unmap_same(mm, pmd, page_table, orig_pte))
        goto out;

    entry = pte_to_swp_entry(orig_pte);
    /* 这个entry不是swap类型的entry,但是此页表项是swap类型的页表项 */
    if (unlikely(non_swap_entry(entry))) {
        /* 是页面迁移类型的entry */
        if (is_migration_entry(entry)) {
            /* 进入处理 */
            migration_entry_wait(mm, pmd, address);
        } else if (is_hwpoison_entry(entry)) {
            ret = VM_FAULT_HWPOISON;
        } else {
            print_bad_pte(vma, address, orig_pte, NULL);
            ret = VM_FAULT_SIGBUS;
        }
        goto out;
    }

        。。。。。。
linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)

  好了最后会有migration_entry_wait()函数进行处理:

linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)
void migration_entry_wait(struct mm_struct *mm, pmd_t *pmd,
                unsigned long address)
{
    /* 获取锁(并不是上锁) */
    spinlock_t *ptl = pte_lockptr(mm, pmd);
    /* 获取发生缺页异常的对应页表项 */
    pte_t *ptep = pte_offset_map(pmd, address);
    /* 处理 */
    __migration_entry_wait(mm, ptep, ptl);
}
linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)

  再往下看__migration_entry_wait():

linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)
static void __migration_entry_wait(struct mm_struct *mm, pte_t *ptep,
                spinlock_t *ptl)
{
    pte_t pte;
    swp_entry_t entry;
    struct page *page;

    /* 上锁 */
    spin_lock(ptl);
    /* 页表项对应的页表项内容 */
    pte = *ptep;
    /* 不是对应的swap类型的页表项内容,则是错误的
     * 注意,页面迁移的页表项内容是属于swap类型
     * 但是页面迁移的entry类型是不属于swap类型
     */
    if (!is_swap_pte(pte))
        goto out;

    /* 页表项内容转为swp_entry_t */
    entry = pte_to_swp_entry(pte);
    /* 如果不是页面迁移的entry类型,则错误 */
    if (!is_migration_entry(entry))
        goto out;

    /* entry指定的页描述符,这个页是旧页的,也就是即将被移动的页 */
    page = migration_entry_to_page(entry);

    /* 此页的page->_count++ */
    if (!get_page_unless_zero(page))
        goto out;
    /* 释放锁 */
    pte_unmap_unlock(ptep, ptl);
    /* 如果此页的PG_locked置位了,则加入此页的等待队列,等待此位被清除 */
    wait_on_page_locked(page);
    /* 经过一段时间的阻塞,到这里PG_locked被清除了,page->_count-- */
    put_page(page);
    return;
out:
    pte_unmap_unlock(ptep, ptl);
}
linux内存源码分析 - 内存压缩(同步关系)
linux内存源码分析 - 内存压缩(同步关系)

  看到后面的wait_on_page_locked(page):

static inline void wait_on_page_locked(struct page *page)
{
    if (PageLocked(page))
        wait_on_page_bit(page, PG_locked);
}

  现在知道为什么页面迁移类型的页表项需要拿旧页作为页表项偏移量了吧,是为了这个方便获取旧页的页描述符,然后加入到这个等待PG_locked清除的等待队列中。

 

最后总结这个流程:

  1. 置位旧页的PG_locked
  2. 对旧页进行反向映射对每个映射了此页的进程页表项进行处理
    • 根据旧页的进程页表项生成一个迁移使用的swap类型的页表项(文件页和非文件页都会分配),这里需要使用到旧页的进程页表项,相当于将旧页的进程页表项中一些标志也保存到了这个swap类型的页表项中
    • 将此迁移使用的swap类型的页表项写入到所用映射了此页的进程页表项中。
  3. 调用迁移函数实现迁移,将旧页的页描述符数据和页内数据复制到新页中,对于不同状态的页,还有不同的处理
    • 没有加入到address_space中的页,使用默认迁移函数,直接复制
    • 加入到address_space中的页,不使用默认迁移函数,而是使用address_space中的迁移函数,主要会更新旧页在address_space中对应slot,让其指向新页
  4. 对新页进行反向映射,将之前修改为可迁移类型的swap页表项的进程页表项重新映射到新页。由于新页的页描述符中的数据与旧页一致,可以进行反向映射,然后通过此页在不同进程vma中的线性地址,可以找到对应的页表项,然后判断此页表项是否为可迁移swap类型的页表项,并且指向的是否是旧页(这里并不是映射到旧页),就可以判断此进程的vma在迁移前是否映射了旧页。
  5. 清除旧页的PG_locked标志
  6. 在上层此旧页就会被释放掉。
上一篇:参与冬季实战营《Linux操作系统实战入门》


下一篇:Redis简单介绍