Linux中的冷熱頁機制概述

是非之地發表於2016-09-08

什麼是冷熱頁?

在Linux Kernel的實體記憶體管理的Buddy System中,引入了冷熱頁的概念。冷頁表示該空閒頁已經不再快取記憶體中了(一般是指L2 Cache),熱頁表示該空閒頁仍然在快取記憶體中。冷熱頁是針對於每CPU的,每個zone中,都會針對於所有的CPU初始化一個冷熱頁的per-cpu-pageset.

為什麼要有冷熱頁?

作用有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中的頁數少於其指定的閾值,便會將釋放的頁插入到冷熱頁連結串列的開始處。這樣,之前插入的熱頁便會隨著其後熱頁源源不斷的插入向後移動,其頁由熱變冷的機率便大大增加。

怎樣分配冷熱頁

在分配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;
}

參考:

相關文章