14.1 LRU链表

在最近几十年操作系统的发展过程中,有很多页面交换算法,其中每个算法都有各自的优点和缺点。linux内核中采用的页面交换算法主要是LRU算法和第二次机会法(second chance)。

  1. LRU链表

    LRU是least recently used(最近最少使用)的缩写,LRU假定最近不使用的页在较短的时间内也不会频繁使用。在内存不足时,这些页面将成为被换出的候选者。内核使用双向链表来定义LRU链表,并且根据页面的类型分为LRU_ANON和LRU_FILE。每种类型根据页面的活跃分为活跃LRU和不活跃LRU,所以内核中一共有如下5个LRU链表。

  • 不活跃匿名页面链表LRU_INACTIVE_ANON

  • 活跃匿名页面链表LRU_ACTIVE_ANON

  • 不活跃文件映射页面链表LRU_INACTIVE_FILE

  • 活跃文件映射页面链表LRU_ACTIVE_FILE

  • 不可回收页面链表LRU_UNEVICTABLE

    LRU链表之所以要分成这样,是因为当内存紧缺时总是优先换出page cache页面,而不是匿名页面。因为大多数情况page cache页面下不需要回写磁盘,除非页面内容被修改,而匿名页面总是要被写入交换分区才能被换出。LRU链表按照zone来配置(在linux4.8内核中已改为基于node的LRU链表),也就是每个zone中都有一整套LRU链表,因此zone数据结构中有一个成员lruvec指向这些链表。枚举类型变量lru_list列举出上述各种LRU链表的类型,struct lruvec 数据结构中定义了上述各种LRU类型的链表。

[include/linux/mmzone.h]

#define LRU_BASE 0
#define LRU_ACTIVE 1
#define LRU_FILE 2

enum lru_list {
    LRU_INACTIVE_ANON = LRU_BASE, //0
    LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE, //1
    LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE, //2
    LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE, //3
    LRU_UNEVICTABLE, //4
    NR_LRU_LISTS //5
};
struct lruvec {
    struct list_head lists[NR_LRU_LISTS];
    struct zone_reclaim_stat reclaim_stat;
#ifdef CONFIG_MEMCG
    struct zone *zone;
#endif
};
struct zone {
    ......
        /* Fields commonly accessed by the page reclaim scanner */
    spinlock_t      lru_lock;
    struct lruvec       lruvec;
    .......
}

LRU链表是如何实现页面老化的呢?

    这需要从页面如何加入LRU链表,以及LRU链表摘取页面说起。加入LRU链表的常用API是lru_cache_add().

lru_cache_add()->__lru_cache_add()

void lru_cache_add(struct page *page)
{
    VM_BUG_ON_PAGE(PageActive(page) && PageUnevictable(page), page);
    VM_BUG_ON_PAGE(PageLRU(page), page);
    __lru_cache_add(page);
}
/*这里使用了页向量(pagevec)数据结构,借助一个数组来保存特定数目的页,可以对这些页面执行同样
的操作。页向量会以"批处理的方式"执行,比单独处理一个页的方式效率要高,查看下面页向量数据结构*/
static void __lru_cache_add(struct page *page)
{
    /*lru_add_pvec是从哪里来的页呢?*/
    struct pagevec *pvec = &get_cpu_var(lru_add_pvec);

    page_cache_get(page);
    /*判断页向量pagevec是否还有空间,如果没有空间,那么首先调用__pagevec_lru_add()函数把
    原有的page加入到LRU链表中,然后把新页面添加到页向量pagevec中*/
    if (!pagevec_space(pvec)){
        /*下面查看__pagevec_lru_add实现:将原有的page加入到LRU链表*/
        __pagevec_lru_add(pvec);
    }
    /*将新页面添加到页向量pagevec*/
    pagevec_add(pvec, page);
    put_cpu_var(lru_add_pvec);
}

页向量数据结构定义:

/* 14 pointers + two long's align the pagevec structure to a power of two */
#define PAGEVEC_SIZE    14
struct pagevec {
    unsigned long nr;/*已经使用的空间,剩余的空间 = PAGEVEC_SIZE - nr*/
    unsigned long cold;
    struct page *pages[PAGEVEC_SIZE];
};
回到__lru_cache_add()函数

__pagevec_lru_add函数:把原有的page添加到LRU

并例举出了相关的函数__pagevec_lru_add_fn()和pagevec_lru_move_fn()

/*
 * Add the passed pages to the LRU, then drop the caller's refcount
 * on them.  Reinitialises the caller's pagevec.
 */
void __pagevec_lru_add(struct pagevec *pvec)
{
    /*注意__pagevec_lru_add_fn回调函数*/
    pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL);
}

static void pagevec_lru_move_fn(struct pagevec *pvec,
    void (*move_fn)(struct page *page, struct lruvec *lruvec, void *arg),
    void *arg)
{
    int i;
    struct zone *zone = NULL;
    struct lruvec *lruvec;
    unsigned long flags = 0;

    /*遍历页向量,将所有的页加入到LRU*/
    for (i = 0; i < pagevec_count(pvec); i++) {
        struct page *page = pvec->pages[i];
        struct zone *pagezone = page_zone(page);

        if (pagezone != zone) {
            if (zone)
                spin_unlock_irqrestore(&zone->lru_lock, flags);
            zone = pagezone;
            spin_lock_irqsave(&zone->lru_lock, flags);
        }

        lruvec = mem_cgroup_page_lruvec(page, zone);
        /*调用__pagevec_lru_add_fn函数,将页加入到LRU*/
        (*move_fn)(page, lruvec, arg);
    }
    if (zone)
        spin_unlock_irqrestore(&zone->lru_lock, flags);
    release_pages(pvec->pages, pvec->nr, pvec->cold);
    pagevec_reinit(pvec);
}


static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,
                 void *arg)
{
    int file = page_is_file_cache(page);
    int active = PageActive(page);
    enum lru_list lru = page_lru(page);

    VM_BUG_ON_PAGE(PageLRU(page), page);

    SetPageLRU(page);
    add_page_to_lru_list(page, lruvec, lru);
    update_page_reclaim_stat(lruvec, file, active);
    trace_mm_lru_insertion(page, lru);
}

static __always_inline void add_page_to_lru_list(struct page *page,
                struct lruvec *lruvec, enum lru_list lru)
{
    int nr_pages = hpage_nr_pages(page);
    mem_cgroup_update_lru_size(lruvec, lru, nr_pages);
    /*加入到LRU中,并且是将成员加入到了链表头*/
    list_add(&page->lru, &lruvec->lists[lru]);
    __mod_zone_page_state(lruvec_zone(lruvec), NR_LRU_BASE + lru, nr_pages);
}
不用回到__lru_cache_add()函数,继续往下

lru_to_page(&lru_list)和list_del(&page->lru)函数组合实现从LRU链表摘取页面,其中lru_to_page()的实现如下:

[mm/vmscan.c]

#define lru_to_page(_head) (list_entry((_head)->prev, struct page, lru))

    lru_to_page()使用了head->prev,从链表的末尾摘取页面,因此,LRU链表实现了先进先出(FIFO)算法。最先进入LRU的链表的页面,在LRU宏的时间会越长,老化时间也越长。

    在系统运行过程中,页面总是在活跃LRU链表和不活跃LRU链表之间转移,不是每次访问内存页面都会发生这种转移。而是发生的时间间隔比较长,随着时间的推移,导致一种热平衡,最不常用的页面将慢慢移动到不活跃LRU链表的末尾,这些页面正是页面回收最合适的候选者。

经典LRU链表算法如下图:

2. 第二次机会法

第二次机会法(second chance)在经典LRU算法基础上做了一些改进。在经典LRU链表(FIFO)中,新产生的页面加入到LRU链表的开头,将LRU链表中现存的页面向后移动了一个位置。当系统内存短缺时,LRU链表尾部的页面将会离开被换出。当系统再需要这些页面时,这些页面会重新置于LRU链表的开头。显然这个设计不是很巧妙,在换出页面时,没有考虑该页面的使用情况是频繁使用,还是很少使用。也就是说,频繁使用的页面依然会在LRU链表末尾而被换出。

    第二次机会算法的改进是为了避免把经常使用的页面置换出去。当选择置换页面时,依然和LRU算法一样,选择最早置入链表的页面,即在链表末尾的页面。二次机会法设置了一个访问状态位( 硬件控制的比特位,对于linux内核来说,PTE_YOUNG标志位是硬件的比特位,PG_active和PG_referenced是软件比特位),所以要检查页面的访问位。如果访问位是0,就淘汰这页面;如果访问位是1,就给它第二次机会,并选择下一个页面来换出。当该页面得到第二次机会时,它的访问位被清0,如果该页在此期间再次被访问过,则访问位置为1。这样给了第二次机会的页面将不会被淘汰,直至所有其他页面被淘汰过(或者也给了第二次机会)。因此,如果一个页面经常被使用,其访问位总保持为1,它一直不会被淘汰出去。

    Linux内核使用PG_active和PG_referenced这两个标志位来实现第二次机会法。PG_active表示该页是否活跃,PG_referenced表示该页是否被引用过,主要函数如下:

  • mark_page_accessed() 标记此页面活跃  mm/swap.c

  • page_referenced() 判断page是否被访问引用过,返回的访问引用pte的个数 mm/rmap.c

  • page_check_references() mm/vmscan.c

3. mark_page_accessed()函数:标记此页面活跃

此函数实现

/*
 * Mark a page as having seen activity.
 *
 * inactive,unreferenced    ->  inactive,referenced
 * inactive,referenced      ->  active,unreferenced
 * active,unreferenced      ->  active,referenced
 *
 * When a newly allocated page is not yet visible, so safe for non-atomic ops,
 * __SetPageReferenced(page) may be substituted for mark_page_accessed(page).
 */
/*此函数的逻辑
(1) 如果PG_active == 0 && PG_referenced == 1 则:
把该页加入活跃LRU,并设置PG_active = 1
清PG_referenced标志位
(2) 如果PG_referenced == 0, 则:
    设置PG_referenced标志位
*/
void mark_page_accessed(struct page *page)
{
    /*PageUnevictable()判断页面不可以回收*/
    if (!PageActive(page) && !PageUnevictable(page) &&
            PageReferenced(page)) {

        /*
         * If the page is on the LRU, queue it for activation via
         * activate_page_pvecs. Otherwise, assume the page is on a
         * pagevec, mark it active and it'll be moved to the active
         * LRU on the next drain.
         */
        if (PageLRU(page))
            activate_page(page);
        else
            __lru_cache_activate_page(page);
        ClearPageReferenced(page);
        if (page_is_file_cache(page))
            workingset_activation(page);
    } else if (!PageReferenced(page)) {
        SetPageReferenced(page);
    }
}

4. page_check_references()函数: 扫描不活跃LRU链表时,此函数会被调用,返回值为一个page_references的枚举类型

enum page_references {
    PAGEREF_RECLAIM, //表示可以尝试回收该页面
    PAGEREF_RECLAIM_CLEAN,//表示可以尝试回收该页面
    PAGEREF_KEEP, //表示会继续保留在不活跃链表中
    PAGEREF_ACTIVATE, //表示该页面会迁移到活跃链表
};
/*在扫描不活跃LRU链表时,此函数会被调用,返回值为一个page_references的枚举类型*/
static enum page_references page_check_references(struct page *page,
                          struct scan_control *sc)
{
    int referenced_ptes, referenced_page;
    unsigned long vm_flags;
    /*page_referenced()检查该页有多少个访问引用pte(referenced_ptes)*/
    referenced_ptes = page_referenced(page, 1, sc->target_mem_cgroup,
                      &vm_flags);
    /*TestClearPageReferenced()返回该页面PG_referenced标志位的值,并且清除该标志位*/
    referenced_page = TestClearPageReferenced(page);

    /*
     * Mlock lost the isolation race with us.  Let try_to_unmap()
     * move the page to the unevictable list.
     */
    if (vm_flags & VM_LOCKED)
        return PAGEREF_RECLAIM;
    /*根据访问引用pte的数目(referenced_ptes变量)和PG_referenced标志位状态(referenced_page变量)
    来判断该页是留在活跃LRU、不活跃LRU,还是可以被回收。当该页有访问引用pte时,要被放回到活跃LRU
    链表中的情况如下:
    (1) 该页是匿名页面(PageSwapBacked(page))
    (2) 最近第二次访问的page cache 或共享的page cache
    (3) 可执行文件的page cache
    其余的有访问引用的页面将会继续保持在不活跃LRU链表中,最后剩下的页面就是可以回收页面的最佳候选者。
*/
    if (referenced_ptes) {
        if (PageSwapBacked(page)) /*该页面是匿名页面,直接放到活跃链表*/
            return PAGEREF_ACTIVATE;
        
        /* 如果有大量只访问一次的page cache充斥在活跃LRU链表中,那么在负载比较重的情况下,选择一个合适回收
        的候选者会变得越来越困难,并且引发分配内存的高延迟,将错误的页面换出。这里的设计是为了优化系统充斥着
        大量只使用一次的page cache页面的情况(通常是mmap映射的文件访问),在这种情况下,只访问一次的page cache
        页面涌入活跃LRU链表中,因为shrink_inactive_list()会把这些页面迁移到活跃链表,不利于页面回收。mmap
        映射的文件访问通常通过filemap_fault()函数产生page cache,在linux2.6.29以前的版本中,这些page cache
        将不会再调用mark_page_accessed()来设置PG_referenced。因此对于这种页面,第一次访问的状态是有访问引用
        pte,但是PG_referenced = 0,所以扫描不活跃链表时设置该页为PG_referenced,并且继续保留在不活跃链表中
        而没有被放入活跃链表。在第二次访问时,发现有访问引用pte但PG_referenced = 1,这时才把该页加入活跃链表
        中。因此利用PG_referenced做了一个page cache的访问次数的过滤器,过滤掉大量的短时间(多了一个不活跃链
        表老化的时间)只访问一次的page cache。这样在内存短缺的情况下,kswapd就巧妙地释放了大量短时间只访问一次
        的page cache。这种大量只访问一个的page cache在不活跃LRU链表中多待一点时间,就越有利于在系统内存短缺时
        首先把它们释放了,否者这些页面跑到活跃LRU链表,再想把它们释放,那么要经历一个:
        活跃LRU链表遍历时间+不活跃LRU链表遍历时间
         */
        SetPageReferenced(page);
        /*referenced_ptes > 1 表示那些第一次在不活跃LRU链表中shared page cache,也就是说
        如果有多个文件同时映射到该页面,它们应该晋升到活跃LRU链表中。因为它们应该多在LRU链表
        中一点时间,以便其他用户可以再次访问到。*/
        if (referenced_page || referenced_ptes > 1)
            return PAGEREF_ACTIVATE;

        /*
         * Activate file-backed executable pages after first usage.
         */
        if (vm_flags & VM_EXEC)
            return PAGEREF_ACTIVATE;

        return PAGEREF_KEEP;
    }

    /* Reclaim if clean, defer dirty pages to writeback */
    if (referenced_page && !PageSwapBacked(page))
        return PAGEREF_RECLAIM_CLEAN;

    return PAGEREF_RECLAIM;
}

总结page_check_references()函数的主要作用如下:

(1) 如果有访问引用pte,那么:

  • 该页是匿名页面(PageSwapBacked(page)),则加入活跃链表;

  • 最近第二次访问(根据PG_referenced来确定,防止只访问一次的page cache跑到活跃链表)的page cache或shared page cache,则加入活跃链表;

  • 可执行文件的page cache,则加入活跃链表;

  • 除上述三种情况外,继续留在不活跃链表,例如第一次访问的page cache。

(2) 如果没有访问引用pte,则表示可以尝试回收它。

5. page_referenced()函数:判断page是否被访问引用过,返回的访问引用pte的次数

page->_mapcount已经统计了page被pte引用的次数,为什么还要调用page_refrenced()函数来重新统计page被pte引用的次数呢?

原因是在page_referenced()函数中会去判断此页面最近是否被访问过,并且最近访问的情况不是顺序读,才会被算做一次pte的引用次数,可以防止大量一次访问的page cache充斥到活跃LRU链表。

[page_check_references()->page_referenced()]

/**
 * page_referenced - test if the page was referenced
 * @page: the page to test
 * @is_locked: caller holds lock on the page
 * @memcg: target memory cgroup
 * @vm_flags: collect encountered vma->vm_flags who actually referenced the page
 *
 * Quick test_and_clear_referenced for all mappings to a page,
 * returns the number of ptes which referenced the page.
 */
/*判断page是否被访问引用过,返回的访问引用pte的个数,即访问和引用(referenced)这个页面的用户进程
空间虚拟页面的个数。核心思想是利用反向映射系统来统计访问引用pte的用户个数。*/
int page_referenced(struct page *page,
            int is_locked,
            struct mem_cgroup *memcg,
            unsigned long *vm_flags)
{
    int ret;
    int we_locked = 0;
    struct page_referenced_arg pra = {
        .mapcount = page_mapcount(page),
        .memcg = memcg,
    };
    struct rmap_walk_control rwc = {
        .rmap_one = page_referenced_one,
        .arg = (void *)&pra,
        .anon_lock = page_lock_anon_vma_read,
    };

    *vm_flags = 0;
    /*判断page->_mapcount引用是否大于0*/
    if (!page_mapped(page))
        return 0;
    /*判断page->mapping是否有地址空间映射*/
    if (!page_rmapping(page))
        return 0;

    if (!is_locked && (!PageAnon(page) || PageKsm(page))) {
        we_locked = trylock_page(page);
        if (!we_locked)
            return 1;
    }

    /*
     * If we are reclaiming on behalf of a cgroup, skip
     * counting on behalf of references from different
     * cgroups
     */
    if (memcg) {
        rwc.invalid_vma = invalid_page_referenced_vma;
    }
    /*rmap_walk()遍历该页面所有映射的pte,然后调用rmap_one()函数,
    下面查看rmap_one函数实现,即page_referenced_one*/
    ret = rmap_walk(page, &rwc);
    *vm_flags = pra.vm_flags;

    if (we_locked)
        unlock_page(page);

    return pra.referenced;
}

rmap_walk()函数实现

[shrink_active_list()->page_referenced()->rmap_walk()]

int rmap_walk(struct page *page, struct rmap_walk_control *rwc)
{
    if (unlikely(PageKsm(page)))
        return rmap_walk_ksm(page, rwc);
    else if (PageAnon(page))
        return rmap_walk_anon(page, rwc);
    else
        return rmap_walk_file(page, rwc);
}

page_referenced_one函数

[shrink_active_list()->page_referenced()->rmap_walk()->rmap_one()即page_referenced_one]

/*
 * arg: page_referenced_arg will be passed
 */
static int page_referenced_one(struct page *page, struct vm_area_struct *vma,
            unsigned long address, void *arg)
{
    struct mm_struct *mm = vma->vm_mm;
    spinlock_t *ptl;
    int referenced = 0;
    struct page_referenced_arg *pra = arg;

    /*忽略huge*/
    if (unlikely(PageTransHuge(page))) {
        pmd_t *pmd;

        /*
         * rmap might return false positives; we must filter
         * these out using page_check_address_pmd().
         */
        pmd = page_check_address_pmd(page, mm, address,
                         PAGE_CHECK_ADDRESS_PMD_FLAG, &ptl);
        if (!pmd)
            return SWAP_AGAIN;

        if (vma->vm_flags & VM_LOCKED) {
            spin_unlock(ptl);
            pra->vm_flags |= VM_LOCKED;
            return SWAP_FAIL; /* To break the loop */
        }

        /* go ahead even if the pmd is pmd_trans_splitting() */
        if (pmdp_clear_flush_young_notify(vma, address, pmd))
            referenced++;
        spin_unlock(ptl);
    } else {
        pte_t *pte;

        /*
         * rmap might return false positives; we must filter
         * these out using page_check_address().
         */
        /*由mm和addr获取pte*/
        pte = page_check_address(page, mm, address, &ptl, 0);
        if (!pte)
            return SWAP_AGAIN;

        if (vma->vm_flags & VM_LOCKED) {
            pte_unmap_unlock(pte, ptl);
            pra->vm_flags |= VM_LOCKED;
            return SWAP_FAIL; /* To break the loop */
        }
        /*判断该pte entry最近是否被访问过,如果访问过,L_PTE_YOUNG比特位会被自动置位,并清空
        PTE中的L_PTE_YOUNG比特位。在x86处理器中指的是_PAGE_ACCESSED比特位,在ARM32 Linux中,
        硬件上没有L_PTE_YOUNG比特位,那么ARM32 linux如何模拟这个linux版本的L_PTE_YOUNG比特位?
            ARM32 Linux内核实现了两套页表,一套为了迎合linux内核,一套为了ARM硬件。L_PTE_YOUNG是
            linux版本页面表项的比特位,当内存映射建立时,会设置该比特位;当解除映射时,要清理掉该
            比特位。下面查看匿名页面映射时,观察L_PTE_YOUNG比特位
        */
        if (ptep_clear_flush_young_notify(vma, address, pte)) {
            /*
             * Don't treat a reference through a sequentially read
             * mapping as such.  If the page has been used in
             * another mapping, we will catch it; if this other
             * mapping is already gone, the unmap path will have
             * set PG_referenced or activated the page.
             */
            /*这里会排除顺序读的情况,因为顺序读的page cache是需要被回收的最佳候选者,因此对这些page cache
            做了弱访问引用处理(weak references),而其余的情况会当作pte被引用,最后增加pra->referenced计数
            和减少pra->mapcount的计数*/
            if (likely(!(vma->vm_flags & VM_SEQ_READ)))
                referenced++;
        }
        pte_unmap_unlock(pte, ptl);
    }

    if (referenced) {
        pra->referenced++;
        pra->vm_flags |= vma->vm_flags;
    }

    pra->mapcount--;
    if (!pra->mapcount)
        return SWAP_SUCCESS; /* To break the loop */

    return SWAP_AGAIN;
}

匿名页面初次映射时,观察L_PTE_YOUNG比特位在何时第一次置位的?

在do_brk()函数中,在新建一个VMA时会通过vm_get_page_prot()建立VMA属性

static unsigned long do_brk(unsigned long addr, unsigned long len)
{
    ......
    vma->vm_mm = mm;
    vma->vm_start = addr;
    vma->vm_end = addr + len;
    vma->vm_pgoff = pgoff;
    vma->vm_flags = flags;
    vma->vm_page_prot = vm_get_page_prot(flags);
    vma_link(mm, vma, prev, rb_link, rb_parent);
    ......
}

pgprot_t vm_get_page_prot(unsigned long vm_flags)
{
    return __pgprot(pgprot_val(protection_map[vm_flags &
                (VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)]) |
            pgprot_val(arch_vm_get_page_prot(vm_flags)));
}

在vm_get_page_prot()函数中,重要的是通过VMA属性来转换成PTE页表项的属性,可以通过查表的方式来获取,protection_map[]定义了很多种属性组合,这些属性组合最终转换为PTE页表的相关比特位。

[arch/arm/include/asm/pgtable.h]

#define _L_PTE_DEFAULT  L_PTE_PRESENT | L_PTE_YOUNG

#define __PAGE_NONE     __pgprot(_L_PTE_DEFAULT | L_PTE_RDONLY | L_PTE_XN | L_PTE_NONE)
#define __PAGE_SHARED       __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_XN)
#define __PAGE_SHARED_EXEC  __pgprot(_L_PTE_DEFAULT | L_PTE_USER)
#define __PAGE_COPY     __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY | L_PTE_XN)
#define __PAGE_COPY_EXEC    __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY)
#define __PAGE_READONLY     __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY | L_PTE_XN)
#define __PAGE_READONLY_EXEC    __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY)

上述属性组合都是会设置L_PTE_PRESENT | L_PTE_YOUNG这两个比特位vma->vm_page_prot中。

    在匿名页面缺页中断处理中,会根据vma->vm_page_prot来生成一个新的PTE页面表项。

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
        unsigned long address, pte_t *page_table, pmd_t *pmd,
        unsigned int flags)
{
    .....
    entry = mk_pte(page, vma->vm_page_prot);
    ......
    set_pte_at(mm, address, page_table, entry);
    ......
}

    因此,当匿名页面第一次建立映射时,会设置L_PTE_PRESENT|L_PTE_YOUNG这两个比特位到Linux版本的页面表项中。

    

    当page_referenced()函数计算访问引用PTE的页面个数时,通过RMAP反向映射遍历每个PTE,然后调用ptep_clear_flush_young_notify()函数来检查每个PTE最近是否被访问过。

[page_referenced()->rmap_one()->page_referenced_one()->pmdp_clear_flush_young_notify()]

include/linux/mmu_notifier.h

#define ptep_clear_flush_young_notify(__vma, __address, __ptep)     \
({                                  \
    int __young;                            \
    struct vm_area_struct *___vma = __vma;              \
    unsigned long ___address = __address;               \
    __young = ptep_clear_flush_young(___vma, ___address, __ptep);   \
    __young |= mmu_notifier_clear_flush_young(___vma->vm_mm,    \
                          ___address,       \
                          ___address +      \
                            PAGE_SIZE); \
    __young;                            \
})

ptep_clear_flush_young_notify()函数的核心是调用ptep_test_and_clear_young()函数

ptep_clear_flush_young_notify()->ptep_clear_flush_young()->ptep_test_and_clear_young()

mm/pgtable-generic.c

include/asm-generic/pgtable.h

static inline int ptep_test_and_clear_young(struct vm_area_struct *vma,
                        unsigned long address,
                        pte_t *ptep)
{
    pte_t pte = *ptep;
    int r = 1;
    /*利用pte_young宏判断linux版本的页表项中是否包含L_PTE_YOUNG比特位,如果没有设置
    该比特位,则返回0,表示映射PTE最近没有被访问引用过。如果L_PTE_YOUNG比特位置位,
    那么需要调用pte_mkold()宏来清这个比特位,然后调用set_pte_at()函数来写入ARM硬件页表*/
    if (!pte_young(pte))
        r = 0;
    else
        set_pte_at(vma->vm_mm, address, ptep, pte_mkold(pte));
    return r;
}

static inline pte_t pte_mkold(pte_t pte)
{
    return clear_pte_bit(pte, __pgprot(L_PTE_YOUNG));
}

ptep_test_and_clear_young()->set_pte_at()->cpu_v7_set_pte_ext()

ENTRY(cpu_v7_set_pte_ext)
#ifdef CONFIG_MMU
    str r1, [r0]            @ linux version

    bic r3, r1, #0x000003f0
    bic r3, r3, #PTE_TYPE_MASK
    orr r3, r3, r2
    orr r3, r3, #PTE_EXT_AP0 | 2

    tst r1, #1 << 4
    orrne   r3, r3, #PTE_EXT_TEX(1)

    eor r1, r1, #L_PTE_DIRTY
    tst r1, #L_PTE_RDONLY | L_PTE_DIRTY
    orrne   r3, r3, #PTE_EXT_APX

    tst r1, #L_PTE_USER
    orrne   r3, r3, #PTE_EXT_AP1

    tst r1, #L_PTE_XN
    orrne   r3, r3, #PTE_EXT_XN

    /*当L_PTE_YOUNG被清掉并且L_PTE_PRESENT还在时,这时候保存Linux版本页表不变
    ,把ARM硬件版本的页表清0*/
    tst r1, #L_PTE_YOUNG
    tstne   r1, #L_PTE_VALID
    eorne   r1, r1, #L_PTE_NONE
    tstne   r1, #L_PTE_NONE
    moveq   r3, #0

 ARM(   str r3, [r0, #2048]! ) //写入硬件页表,硬件页表在软件页表+2048Byte
 THUMB( add r0, r0, #2048 )
 THUMB( str r3, [r0] )
    ALT_SMP(W(nop))
    ALT_UP (mcr p15, 0, r0, c7, c10, 1)     @ flush_pte
#endif
    bx  lr
ENDPROC(cpu_v7_set_pte_ext)

    当L_PTE_YOUNG被清掉且L_PTE_PRESENT还在时,保存Linux版本页表不变,把ARM硬件版本的页表清0.

    因为ARM硬件版本的页表被清0之后,当应用程序再次访问这个页面时会触发缺页中断。注意,此时ARM硬件版本的页表项内容为0,linux版本的页表项内容还在。

[page_referenced()清了L_PTE_YOUNG和ARM硬件页表->应用程序再次访问该页->触发缺页中断]

为什么要清理ARM硬件页表?

在缺页中断中会重新设置Linux版本页表的L_PTE_YOUNG比特位,见下面的handle_pte_fault函数

static int handle_pte_fault(struct mm_struct *mm,
             struct vm_area_struct *vma, unsigned long address,
             pte_t *pte, pmd_t *pmd, unsigned int flags)
{
    pte_t entry;
    spinlock_t *ptl;

    /*
     * some architectures can have larger ptes than wordsize,
     * e.g.ppc44x-defconfig has CONFIG_PTE_64BIT=y and CONFIG_32BIT=y,
     * so READ_ONCE or ACCESS_ONCE cannot guarantee atomic accesses.
     * The code below just needs a consistent view for the ifs and
     * we later double check anyway with the ptl lock held. So here
     * a barrier will do.
     */
    entry = *pte;
    barrier();
    if (!pte_present(entry)) {
        if (pte_none(entry)) {
            if (vma->vm_ops) {
                if (likely(vma->vm_ops->fault))
                    return do_fault(mm, vma, address, pte,
                            pmd, flags, entry);
            }
            return do_anonymous_page(mm, vma, address,
                         pte, pmd, flags);
        }
        return do_swap_page(mm, vma, address,
                    pte, pmd, flags, entry);
    }

    if (pte_protnone(entry))
        return do_numa_page(mm, vma, address, entry, pte, pmd);

    ptl = pte_lockptr(mm, pmd);
    spin_lock(ptl);
    if (unlikely(!pte_same(*pte, entry)))
        goto unlock;
    if (flags & FAULT_FLAG_WRITE) {
        if (!pte_write(entry))
            return do_wp_page(mm, vma, address,
                    pte, pmd, ptl, entry);
        entry = pte_mkdirty(entry);
    }
    /*对于ARM平台,这里重新设置L_PTE_YOUNG比特位*/
    entry = pte_mkyoung(entry);
    if (ptep_set_access_flags(vma, address, pte, entry, flags & FAULT_FLAG_WRITE)) {
        update_mmu_cache(vma, address, pte);
    } else {
        /*
         * This is needed only for protection faults but the arch code
         * is not yet telling us if this is a protection fault or not.
         * This still avoids useless tlb flushes for .text page faults
         * with threads.
         */
        if (flags & FAULT_FLAG_WRITE)
            flush_tlb_fix_spurious_fault(vma, address);
    }
unlock:
    pte_unmap_unlock(pte, ptl);
    return 0;
}

总结page_referenced()函数所做的主要工作如下:

  • 利用RMAP系统遍历所有该页面的pte。

  • 对于每个pte,如果L_PTE_YOUNG比特位置位,说明之前被访问过,referenced计数加1。然后清空L_PTE_YOUNG比特位,对于ARM32处理器来说,会清空硬件页表项内容,人为制造一个缺页中断,当再次访问该pte时,缺页中断中设置L_PTE_YOUNG比特位。这样就可以很好的监听这个页面是否最近被访问过

  • 返回referenced计数,表示该页有多少个访问引用pte

小结:

    为了评估页的活动程序,kernel引入了PG_referenced和PG_active两个标志位。为什么需要两个标志位呢?假定只使用一个PG_active来标识页是活动,在页被访问时,设置该位,但是何时清除呢?为此需要维护大量的内核定时器,这种方法注定是要失败的。

    使用两个标志,可以实现一种更精巧的方法,其核心思想是:一个标识当前活跃程度,一个表示最近是否被引用过,下图说明了基本算法:

基本上有以下步骤:

(1) 如果页是活动的,设置PG_active,并保存在ACTIVE_LRU链表;反之在INACTIVE;

(2) 每次访问时,设置PG_referenced位,复制该工作的是mark_page_accessed()函数;

(3) PG_referenced以及由反向映射提供的信息用来确定页面活跃程度,每次清除该位时,都会检测页面活跃程序,page_referenced函数实现了该行为;

(4) 再次进入mark_page_accessed()。如果发现PG_refenced已被置位,意味着page_referenced没有执行检查,因而对于mark_page_accessed()的调用比page_referenced()更频繁,这意味着页面经常被访问。如果该页位于INACTIVE链表,将其移动到ACTIVE,此外还会设置PG_active标志位,清除PG_referenced;

(5) 反向的转移也可能的,在页面活动程度减少时,可能连续调用两次page_referenced而中间没有mark_page_accessed()。

如果对于内存页的访问是稳定的,那么对page_referenced和mark_page_accessed()的调用在本质上是均衡的,因而页面保持在当前LRU链表,这种方案同时确保了内存页不会再ACTIVE与INACTIVE链表间快速跳跃。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

byd yes

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值