什么是冷热页?

在Linux Kernel的物理内存管理的Buddy
System中,引入了冷热页的概念。冷页表示该空闲页已经不再高速缓存中了(一般是指L2
Cache),热页表示该空闲页仍然在高速缓存中。冷热页是针对于每CPU的,每个zone中,都会针对于所有的CPU初始化一个冷热页的per-cpu-pageset.

Linux内存释放函数之间的调用关系如下图所示

介绍过伙伴系统的原理和Linux伙伴系统的数据结构,现在来看伙伴系统是如何来分配页面的。实际上,伙伴系统分配页面的算法并不复杂,但是由于考虑到分配内存时要尽量减少碎片的产生(涉及迁移机制)以及当内存不足时需要采取各种更为积极的手段,使得内核分配页面的相关函数完整地分析起来比较复杂庞大。在这里,我们只关注分配时最一般的情况,而其他情况的处理在以后单独拿出来讨论。

为什么要有冷热页?

作用有3点:

  • Buddy
    Allocator在分配order为0的空闲页的时候,如果分配一个热页,那么由于该页已经存在于L2
    Cache中了。CPU写访问的时候,不需要先把内存中的内容读到Cache中,然后再写。如果分配一个冷页,说明该页不在L2
    Cache中。一般情况下,尽可能用热页,是容易理解的。什么时候用冷页呢?While
    allocating a physical page frame, there is a bit specifying whether
    we would like a hot or a cold page (that is, a page likely to be in
    the CPU cache, or a page not likely to be there). If the page will
    be used by the CPU, a hot page will be faster. If the page will be
    used for device DMA the CPU cache would be invalidated anyway, and
    a cold page does not waste precious cache contents.
    简单翻译一下:当内核分配一个物理页框时,有一些规范来约束我们是分配热页还是冷页。当页框是CPU使用的,则分配热页。当页框是DMA设备使用的,则分配冷页。因为DMA设备不会用到CPU高速缓存,所以没必要使用热页。
  • Buddy
    System在给某个进程分配某个zone中空闲页的时候,首先需要用自旋锁锁住该zone,然后分配页。这样,如果多个CPU上的进程同时进行分配页,便会竞争。引入了per-cpu-set后,当多个CPU上的进程同时分配页的时候,竞争便不会发生,提高了效率。另外当释放单个页面时,空闲页面首先放回到per-cpu-pageset中,以减少zone中自旋锁的使用。当页面缓存中的页面数量超过阀值时,再将页面放回到伙伴系统中。
  • 使用每CPU冷热页还有一个好处是,能保证某个页一直黏在1个CPU上,这有助于提高Cache的命中率。

 

 

冷热页的数据结构

struct per_cpu_pages {
        int count;              // number of pages in the list
        int high;               // high watermark, emptying needed
        int batch;              // chunk size for buddy add/remove
         // Lists of pages, one per migrate type stored on the pcp-lists
         每个CPU在每个zone上都有MIGRATE_PCPTYPES个冷热页链表(根据迁移类型划分)
         struct list_head lists[MIGRATE_PCPTYPES];
 };

在Linux中,对于UMA的架构,冷热页是在一条链表上进行管理。热页在前,冷页在后。CPU每释放一个order为0的页,如果per-cpu-pageset中的页数少于其指定的阈值,便会将释放的页插入到冷热页链表的开始处。这样,之前插入的热页便会随着其后热页源源不断的插入向后移动,其页由热变冷的几率便大大增加。

图片 1         

     
 我们从__alloc_pages_nodemask()这个函数开始分析,所有的分配页面的函数最终都会落到这个函数上面,它是伙伴系统的入口。

怎样分配冷热页

在分配order为0页的时候(冷热页机制只处理单页分配的情况),先找到合适的zone,然后根据需要的migratetype类型定位冷热页链表(每个zone,对于每个cpu,有3条冷热页链表,对应于:MIGRATE_UNMOVABLE、MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE)。若需要热页,则从链表头取下一页(此页最“热”);若需要冷页,则从链表尾取下一页(此页最“冷”)。

分配函数(关键部分已添加注释):

/*
 * Really, prep_compound_page() should be called from __rmqueue_bulk().  But
 * we cheat by calling it from here, in the order > 0 path.  Saves a branch
 * or two.
 */
static inline
struct page *buffered_rmqueue(struct zone *preferred_zone,
            struct zone *zone, int order, gfp_t gfp_flags,
            int migratetype)
{
    unsigned long flags;
    struct page *page;
    //分配标志是__GFP_COLD才分配冷页
    int cold = !!(gfp_flags & __GFP_COLD);
again:
    if (likely(order == 0)) {
        struct per_cpu_pages *pcp;
        struct list_head *list;
        local_irq_save(flags);
        pcp = &this_cpu_ptr(zone->pageset)->pcp;
        list = &pcp->lists[migratetype];
        if (list_empty(list)) {
          //如果缺少页,则从Buddy System中分配。
            pcp->count += rmqueue_bulk(zone, 0,
                    pcp->batch, list,
                    migratetype, cold);
            if (unlikely(list_empty(list)))
                goto failed;
        }
        if (cold)
        //分配冷页时,从链表尾部分配,list为链表头,list->prev表示链表尾
            page = list_entry(list->prev, struct page, lru);
        else
        //分配热页时,从链表头分配
            page = list_entry(list->next, struct page, lru);
       //分配完一个页框后从冷热页链表中删去该页
        list_del(&page->lru);
        pcp->count--;
    } else {//如果order!=0(页框数>1),则不从冷热页链表中分配
        if (unlikely(gfp_flags & __GFP_NOFAIL)) {
            /*
             * __GFP_NOFAIL is not to be used in new code.
             *
             * All __GFP_NOFAIL callers should be fixed so that they
             * properly detect and handle allocation failures.
             *
             * We most definitely don't want callers attempting to
             * allocate greater than order-1 page units with
             * __GFP_NOFAIL.
             */
            WARN_ON_ONCE(order > 1);
        }
        spin_lock_irqsave(&zone->lock, flags);
        page = __rmqueue(zone, order, migratetype);
        spin_unlock(&zone->lock);
        if (!page)
            goto failed;
        __mod_zone_page_state(zone, NR_FREE_PAGES, -(1 << order));
    }
    __count_zone_vm_events(PGALLOC, zone, 1 << order);
    zone_statistics(preferred_zone, zone, gfp_flags);
    local_irq_restore(flags);
    VM_BUG_ON(bad_range(zone, page));
    if (prep_new_page(page, order, gfp_flags))
        goto again;
    return page;
failed:
    local_irq_restore(flags);
    return NULL;
}

参考:

  • 认识Linux/ARM
    中的冷热页
  • Linux源码

 

 

可以看到,落脚点是__free_pages()这个函数,它执行的工作的流程图如下图所示
 

[cpp]  

图片 2        

<span style=”font-size:12px;”>struct page *  

 下面是该函数的具体代码

__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,  

 

            struct zonelist *zonelist, nodemask_t *nodemask)  

[cpp]  

{  

void __free_pages(struct page *page, unsigned int order)  

    /*根据gfp_mask确定分配页所处的管理区*/  

{  

    enum zone_type high_zoneidx = gfp_zone(gfp_mask);  

    if (put_page_testzero(page)) {/*判断页没有被使用*/  

    struct zone *preferred_zone;    www.2cto.com  

        trace_mm_page_free_direct(page, order);  

    struct page *page;  

        if (order == 0)/*单页则释放到每CPU页框高速缓存中*/  

    /*根据gfp_mask得到迁移类分配页的型*/  

            free_hot_page(page);    www.2cto.com  

    int migratetype = allocflags_to_migratetype(gfp_mask);  

        else           /*多页则释放到伙伴系统*/  

  

            __free_pages_ok(page, order);  

    gfp_mask &= gfp_allowed_mask;  

    }  

  

}  

    lockdep_trace_alloc(gfp_mask);  

 

  

首先该函数要通过页描述符的引用计数来判断该页是否是空闲的

    might_sleep_if(gfp_mask & __GFP_WAIT);  

确定页是空闲的后,再判断要释放多少个页面,如果是单个页面则将该页作为热页释放到pcp中,如果是多页则释放到伙伴系统中

  

 

    if (should_fail_alloc_page(gfp_mask, order))  

[cpp]  

        return NULL;  

void free_hot_page(struct page *page)  

  

{  

    /* 

    trace_mm_page_free_direct(page, 0);  

     * Check the zones suitable for the gfp_mask contain at least one 

    free_hot_cold_page(page, 0);  

     * valid zone. It’s possible to have an empty zonelist as a result 

}  

     * of GFP_THISNODE and a memoryless node 

 

     */  

free_hot_page是free_hot_cold_page的封装

    if (unlikely(!zonelist->_zonerefs->zone))  

 

        return NULL;  

[cpp]  

  

static void free_hot_cold_page(struct page *page, int cold)  

    /* The preferred zone is used for statistics later */  

{  

   
/*从zonelist中找到zone_idx与high_zoneidx相同的管理区,也就是之前认定的管理区*/
 

    struct zone *zone = page_zone(page);  

    first_zones_zonelist(zonelist, high_zoneidx, nodemask,
&preferred_zone);  

    struct per_cpu_pages *pcp;  

    if (!preferred_zone)  

    unsigned long flags;  

        return NULL;  

    int migratetype;  

  

    int wasMlocked = __TestClearPageMlocked(page);  

    /* First allocation attempt */  

    www.2cto.com  

    page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL,
nodemask, order,  

    kmemcheck_free_shadow(page, 0);  

            zonelist, high_zoneidx, ALLOC_WMARK_LOW|ALLOC_CPUSET,  

  

            preferred_zone, migratetype);  

    if (PageAnon(page))  

    if (unlikely(!page))  

        page->mapping = NULL;  

       
/*第一次分配失败的话则会用通过一条低速路径来进行第二次分配,包括唤醒页换出守护进程等等*/
 

    if (free_pages_check(page))  

        page = __alloc_pages_slowpath(gfp_mask, order,  

        return;  

                zonelist, high_zoneidx, nodemask,  

  

                preferred_zone, migratetype);  

    if (!PageHighMem(page)) {  

  

        debug_check_no_locks_freed(page_address(page), PAGE_SIZE);
 

    trace_mm_page_alloc(page, order, gfp_mask, migratetype);  

        debug_check_no_obj_freed(page_address(page), PAGE_SIZE);  

    return page;  

    }  

}</span>  
 

    arch_free_page(page, 0);  

首先要做的就是找到指定的分配管理区,管理区的编号保存在high_zoneidx中

    kernel_map_pages(page, 1, 0);  

然后就是尝试第一次分配,流程是从指定的管理区开始扫描管理区–>找到充足的管理区–>从指定的迁移类型链表中分配内存–>如果在指定迁移类型中找不到则到其他的迁移类型中去寻找

  

如果第二步在各个区域都找不到可以满足分配的内存了,那么说明管理区的内存已经确实不够了,于是开始启用一条慢速的途径来分配,包括尝试去换出一些不经常使用的页等等,内核会在这次分配中表现得更为积极,其中的细节涉及到了其他一些复杂的东西,以后再做分析

    /*获取对应的pcp结构*/  

 

    pcp = &zone_pcp(zone, get_cpu())->pcp;  

[cpp]  

    /*获取迁移类型*/  

static struct page *  

    migratetype = get_pageblock_migratetype(page);  

get_page_from_freelist(gfp_t gfp_mask, nodemask_t *nodemask,
unsigned int order,  

    set_page_private(page, migratetype);  

        struct zonelist *zonelist, int high_zoneidx, int alloc_flags,
 

    local_irq_save(flags);  

        struct zone *preferred_zone, int migratetype)    www.2cto.com
 

    if (unlikely(wasMlocked))  

{  

        free_page_mlock(page);  

    struct zoneref *z;  

    __count_vm_event(PGFREE);  

    struct page *page = NULL;  

  

    int classzone_idx;  

    /* 

    struct zone *zone;  

     * We only track unmovable, reclaimable and movable on pcp lists. 

    nodemask_t *allowednodes = NULL;/* zonelist_cache approximation
*/  

     * Free ISOLATE pages back to the allocator because they are being 

    int zlc_active = 0;     /* set if using zonelist_cache */  

     * offlined but treat RESERVE as movable pages so we can get those 

    int did_zlc_setup = 0;      /* just call zlc_setup() one time
*/  

     * areas back if necessary. Otherwise, we may have to free 

  

     * excessively into the page allocator 

    /*获取管理区的编号*/  

     */  

    classzone_idx = zone_idx(preferred_zone);  

   
 /*只有不可移动页,可回收页和可移动页才能放到每CPU页框高速缓存中,如果 

zonelist_scan:  

        迁移类型不属于这个范围,则要将该页释放回伙伴系统*/  

    /* 

    if (migratetype >= MIGRATE_PCPTYPES) {  

     * Scan zonelist, looking for a zone with enough free. 

        if (unlikely(migratetype == MIGRATE_ISOLATE)) {  

     * See also cpuset_zone_allowed() comment in kernel/cpuset.c. 

            free_one_page(zone, page, 0, migratetype);  

     */  

            goto out;  

    /*从认定的管理区开始遍历,直到找到一个拥有足够空间的管理区, 

        }  

     
例如,如果high_zoneidx对应的ZONE_HIGHMEM,则遍历顺序为HIGHMEM–>NORMAL–>DMA, 

        migratetype = MIGRATE_MOVABLE;  

      如果high_zoneidx对应ZONE_NORMAL,则遍历顺序为NORMAL–>DMA*/
 

    }  

    for_each_zone_zonelist_nodemask(zone, z, zonelist,  

  

                        high_zoneidx, nodemask) {  

    if (cold)/*冷页插入表尾*/  

        if (NUMA_BUILD && zlc_active &&  

        list_add_tail(&page->lru, &pcp->lists[migratetype]);  

            !zlc_zone_worth_trying(zonelist, z, allowednodes))  

    else     /*热页插入表头*/    www.2cto.com  

                continue;  

        list_add(&page->lru, &pcp->lists[migratetype]);  

  

    pcp->count++;  

        /*检查给定的内存域是否属于该进程允许运行的CPU*/  

    /*如果pcp中的页面数超过了high,则释放2^batch个单页给伙伴系统*/  

        if ((alloc_flags & ALLOC_CPUSET) &&  

    if (pcp->count >= pcp->high) {  

            !cpuset_zone_allowed_softwall(zone, gfp_mask))  

        free_pcppages_bulk(zone, pcp->batch, pcp);  

                goto try_next_zone;  

        pcp->count -= pcp->batch;  

  

    }  

        BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);  

  

        if (!(alloc_flags & ALLOC_NO_WATERMARKS)) {  

out:  

            unsigned long mark;  

    local_irq_restore(flags);  

            int ret;  

    put_cpu();  

              

}  

           
/*通过alloc_flags来确定是使用何种水印,pages_min?pages_low?pages_high? 

 

             
选择了一种水印,就要求分配后的空闲不低于该水印才能进行分配*/  

伙伴系统的分支__free_pages_ok()先对释放的页做了些检查,然后具体的释放通过调用free_one_page()–>__free_one_page()来完成

            mark = zone->watermark[alloc_flags &
ALLOC_WMARK_MASK];  

 

  

[cpp]  

            /*如果管理区的水位线处于正常水平,则在该管理区进行分配*/  

static inline void __free_one_page(struct page *page,  

            if (zone_watermark_ok(zone, order, mark,  

        struct zone *zone, unsigned int order,  

                    classzone_idx, alloc_flags))  

        int migratetype)  

                goto try_this_zone;  

{  

  

    unsigned long page_idx;  

            if (zone_reclaim_mode == 0)  

  

                goto this_zone_full;  

    if (unlikely(PageCompound(page)))  

  

        if (unlikely(destroy_compound_page(page, order)))  

            /*下面这部分都是针对NUMA架构的申请页面回收*/  

            return;  

            ret = zone_reclaim(zone, gfp_mask, order);  

  

            switch (ret) {    www.2cto.com  

    VM_BUG_ON(migratetype == -1);  

            case ZONE_RECLAIM_NOSCAN:/*没有进行回收*/  

Author

发表评论

电子邮件地址不会被公开。 必填项已用*标注