天天看點

《LINUX3.0核心源代碼分析》第四章:記憶體管理(5)

摘要:本文主要講述linux如何處理ARM cortex A9多核處理器的記憶體管理部分。主要包括對夥伴系統算法流程的介紹。

1.1.1      夥伴系統 1.1.1.1         從緩存中釋放單個頁面到夥伴系統

當釋放單個頁面時,頁面首先放回到每CPU的頁面緩存中,以減少自旋鎖的使用。當頁面緩存中的頁面數量超過閥值時,再将頁面放回到夥伴系統中。相應的函數是free_pcppages_bulk:

/**

 * 從每CPU緩存中釋放一定數量的頁面到夥伴系統中。

 */

static void free_pcppages_bulk(struct zone *zone, int count,

                    struct per_cpu_pages *pcp)

{

    int migratetype = 0;

    int batch_free = 0;

    int to_free = count;

    /**

     * 雖然管理區可以按照CPU節點分類,但是也可以跨CPU節點進行記憶體配置設定,是以這裡需要用自旋鎖保護管理區

     * 使用每CPU緩存的目的,也是為了減少使用這把鎖。

     */

    spin_lock(&zone->lock);

    /* all_unreclaimable代表了記憶體緊張程度,釋放記憶體後,将此标志清除 */

    zone->all_unreclaimable = 0;

    zone->pages_scanned = 0;/* pages_scanned代表最後一次記憶體緊張以來,頁面回收過程已經掃描的頁數。目前正在釋放記憶體,将此清0,待回收過程随後回收時重新計數 */

    while (to_free) {/* 連續釋放指定數量的頁面到夥伴系統 */

        struct page *page;

        struct list_head *list;

        /* 這裡是從MIGRATE_UNMOVABLE到MIGRATE_MOVABLE三個每CPU緩存連結清單中依次搜尋一個可釋放的頁面,直到找到一個可以回收的緩存連結清單。 */

        do {

            batch_free++;/* batch_free是最後掃描的連結清單索引 */

            if (++migratetype == MIGRATE_PCPTYPES)/* 隻從MIGRATE_UNMOVABLE..MIGRATE_MOVABLE三個連結清單中選擇頁面。從這三個連結清單中循環選擇回收的頁面 */

                migratetype = 0;

            list = &pcp->lists[migratetype];

        } while (list_empty(list));/* 如果目前連結清單為空,則從下一個緩存連結清單中釋放頁面 */

        /* This is the only non-empty list. Free them all. */

        if (batch_free == MIGRATE_PCPTYPES)/* 隻有一個緩存連結清單中有可回收頁面,則從該連結清單中釋放所有頁面 */

            batch_free = to_free;

        do {/* 本循環從最後一個頁面緩存連結清單中釋放多個頁面到夥伴系統,越靠後的緩存連結清單,釋放的頁面數量越多 */

            page = list_entry(list->prev, struct page, lru);/* 取緩存連結清單的最後一個元素,要麼是最早放到緩存中的頁面(其硬體緩存的熱度也最低),要麼就是"冷"頁,不需要考慮硬體緩存 */

            /* must delete as __free_one_page list manipulates */

            list_del(&page->lru);/* 将頁面從連結清單中摘除 */

            /* MIGRATE_MOVABLE list may include MIGRATE_RESERVEs */

            /* 将頁面釋放回夥伴系統。注意這裡沒有用migratetype變量,而是用頁面屬性中的migratetype。原因請參見英文注釋 */

            __free_one_page(page, zone, 0, page_private(page));

            trace_mm_page_pcpu_drain(page, 0, page_private(page));/* 調試代碼 */

        } while (--to_free && --batch_free && !list_empty(list));

    }

    /* 空閑頁面計數 */

    __mod_zone_page_state(zone, NR_FREE_PAGES, count);

    spin_unlock(&zone->lock);

}

1.1.1.2         釋放多個頁面到夥伴系統

一次性配置設定和釋放多個頁面,都不經過每CPU緩存,而是直接從夥伴系統中配置設定、直接傳回給夥伴系統。

    向夥伴系統釋放頁面的函數是free_one_page,這個函數有點名不副實。它并不是指釋放一個頁面,而是釋放多個頁面,不知道核心為何取如此奇怪的名字。

 * 将多個頁面釋放到夥伴系統。

static void free_one_page(struct zone *zone, struct page *page, int order,

                int migratetype)

    spin_lock(&zone->lock);/* 獲得管理區的自旋鎖 */

    zone->all_unreclaimable = 0;/* 隻要是釋放了頁面,都需要将此兩個标志清0,表明記憶體不再緊張的事實 */

    zone->pages_scanned = 0;

    /* 在鎖的保護下,調用__free_one_page操作夥伴系統,将頁面釋放回夥伴系統 */

    __free_one_page(page, zone, order, migratetype);

    /* 管理區空閑頁面計數 */

    __mod_zone_page_state(zone, NR_FREE_PAGES, 1 order);

    spin_unlock(&zone->lock);/* 釋放管理區自旋鎖 */

在獲得管理區鎖的情況下,不管是釋放一個頁還是多個頁到夥伴系統,均使用函數__free_one_page:

 * 在獲得管理區自旋鎖的情況下,将頁面歸還給夥伴系統。

static inline void __free_one_page(struct page *page,

        struct zone *zone, unsigned int order,

        int migratetype)

    unsigned long page_idx;

    unsigned long combined_idx;

    unsigned long uninitialized_var(buddy_idx);

    struct page *buddy;

    if (unlikely(PageCompound(page)))/* 要釋放的頁是巨頁的一部分 */

        if (unlikely(destroy_compound_page(page, order)))/* 解決巨頁标志,如果巨頁标志有問題,則退出 */

            return;

    VM_BUG_ON(migratetype == -1);

    page_idx = page_to_pfn(page) & ((1 MAX_ORDER) - 1);

    VM_BUG_ON(page_idx & ((1 order) - 1));/* 如果被釋放的頁不是所釋放階的第一個頁,則說明參數有誤 */

    VM_BUG_ON(bad_range(zone, page));/* 檢查頁面是否處于zone之中 */

    while (order MAX_ORDER-1) {/* 釋放頁以後,目前頁面可能與前後的空閑頁組成更大的空閑頁面,直到放到最大階的夥伴系統中 */

        buddy_idx = __find_buddy_index(page_idx, order);/* 找到與目前頁屬于同一個階的夥伴頁面索引 */

        buddy = page + (buddy_idx - page_idx);/* 計算夥伴頁面的頁位址 */

        if (!page_is_buddy(page, buddy, order))/* 判斷預期的夥伴頁面是否與目前頁處于同一個管理區,并且是否處于空閑狀态。如果不是,則不能與該頁合并為大的夥伴,退出 */

            break;

        /* Our buddy is free, merge with it and move up one order. */

        list_del(&buddy->lru);/* 如果能夠合并,則将夥伴頁從夥伴系統中摘除 */

        zone->free_area[order].nr_free--;/* 同時減少目前階中的空閑頁計數 */

        rmv_page_order(buddy);/* 清除夥伴頁的夥伴标志,因為該頁會被合并 */

        combined_idx = buddy_idx & page_idx;

        /* 将目前頁與夥伴頁合并後,新的頁面起始位址 */

        page = page + (combined_idx - page_idx);

        page_idx = combined_idx;

        order++;

    /* 設定夥伴頁中第一個空閑頁的階 */

    set_page_order(page, order);

     * 如果目前合并後的頁不是最大階的,那麼将目前空閑頁放到夥伴連結清單的最後。

     * 這樣,它将不會被很快被配置設定,更有可能與更高階頁面進行合并。

     */

    if ((order MAX_ORDER-2) && pfn_valid_within(page_to_pfn(buddy))) {

        struct page *higher_page, *higher_buddy;

        /* 計算更高階的頁面索引及頁面位址 */

        higher_page = page + (combined_idx - page_idx);

        buddy_idx = __find_buddy_index(combined_idx, order + 1);

        higher_buddy = page + (buddy_idx - combined_idx);

        if (page_is_buddy(higher_page, higher_buddy, order + 1)) {/* 更高階的頁面是空閑的,屬于夥伴系統 */

            list_add_tail(&page->lru,

                &zone->free_area[order].free_list[migratetype]);/* 将目前頁面合并到空閑連結清單的最後,盡量避免将它配置設定出去 */

            goto out;

        }

    /* 更高階的頁面已經配置設定出去,那麼将目前頁面放到連結清單前面 */

    list_add(&page->lru, &zone->free_area[order].free_list[migratetype]);

out:

    /* 将目前階的空閑計數加1 */

    zone->free_area[order].nr_free++;

1.1.1.3         從夥伴系統中獲得頁面

從夥伴系統中配置設定頁面的函數是buffered_rmqueue:

 * 從夥伴系統中配置設定頁面

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;

    int cold = !!(gfp_flags & __GFP_COLD);/* 如果配置設定參數指定了__GFP_COLD标志,則設定cold标志 */

again:

    if (likely(order == 0)) {/* 配置設定單頁,需要管理每CPU頁面緩存 */

        struct per_cpu_pages *pcp;

        /* 這裡需要關中斷,因為記憶體回收過程可能發送核間中斷,強制每個核從每CPU緩存中釋放頁面。而且中斷處理函數也會配置設定單頁。 */

        local_irq_save(flags);

        /* 取得本CPU的頁面緩存對象 */

        pcp = &this_cpu_ptr(zone->pageset)->pcp;

        /* 根據使用者指定的遷移類型,從指定的遷移緩存連結清單中配置設定 */

        list = &pcp->lists[migratetype];

        if (list_empty(list)) {/* 緩存為空,需要擴大緩存的大小 */

            /* 從夥伴系統中摘除一批頁面到緩存中,補充的頁面個數由每CPU緩存的batch字段指定 */

            pcp->count += rmqueue_bulk(zone, 0,

                    pcp->batch, list,

                    migratetype, cold);

            /* 如果連結清單仍然為空,那麼說明夥伴系統中頁面也沒有了,配置設定失敗。 */

            if (unlikely(list_empty(list)))

                goto failed;

        if (cold)

            /* 如果配置設定的頁面不需要考慮硬體緩存(注意不是每CPU頁面緩存),則取對外連結表的最後一個節點傳回給上層 */

            page = list_entry(list->prev, struct page, lru);

        else

            /* 如果要考慮硬體緩存,則取對外連結表的第一個頁面,這個頁面是最近剛釋放到每CPU緩存的,緩存熱度更高 */

            page = list_entry(list->next, struct page, lru);

        /* 将頁面從每CPU緩存連結清單中取出,并将每CPU緩存計數減1 */

        list_del(&page->lru);

        pcp->count--;

    } else {/* 配置設定的是多個頁面,不需要考慮每CPU頁面緩存,直接從系統中配置設定 */

        if (unlikely(gfp_flags & __GFP_NOFAIL)) {/* __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);

        /* 調用__rmqueue從夥伴系統中配置設定頁面 */

        page = __rmqueue(zone, order, migratetype);

        /* 這裡僅僅打開自旋鎖,待後面統計計數設定完畢後再開中斷 */

        spin_unlock(&zone->lock);

        if (!page)

            goto failed;

        /* 已經配置設定了1

        __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));

     * 這裡進行安全性檢查,并進行一些善後工作。

     * 如果頁面标志破壞,傳回的頁面出現了問題,則傳回試圖配置設定其他頁面。

     * 核心會有這樣的BUG存在嗎?除了驅動或者子產品有問題外?

    if (prep_new_page(page, order, gfp_flags))

        goto again;

    return page;

failed:

    local_irq_restore(flags);

    return NULL;

這裡使用了一個輔助函數rmqueue_bulk:

 * 從夥伴系統中批量取出多個頁面。

 * 用于從夥伴系統中批量取出頁面放到每CPU頁面緩存中

static int rmqueue_bulk(struct zone *zone, unsigned int order,

            unsigned long count, struct list_head *list,

            int migratetype, int cold)

    int i;

    spin_lock(&zone->lock);/* 上層函數已經關了中斷,這裡需要操作管理區,擷取管理區的自旋鎖 */

    for (i = 0; i count; ++i) {/* 重複指定的次數,從夥伴系統中配置設定頁面 */

        /* 從夥伴系統中取出頁面 */

        struct page *page = __rmqueue(zone, order, migratetype);

        /* 夥伴系統中沒有空閑頁了,退出 */

        if (unlikely(page == NULL))

        /*

         * Split buddy pages returned by expand() are received here

         * in physical page order. The page is added to the callers and

         * list and the list head then moves forward. From the callers

         * perspective, the linked list is ordered by page number in

         * some conditions. This is useful for IO devices that can

         * merge IO requests if the physical pages are ordered

         * properly.

         */

        if (likely(cold == 0))/* 根據調用者的要求,将頁面放到每CPU緩存連結清單的頭部或者尾部 */

            list_add(&page->lru, list);

            list_add_tail(&page->lru, list);

        /* 設定頁面的遷移類型 */

        set_page_private(page, migratetype);

        list = &page->lru;

    /* 遞減管理區的空閑頁面計數。 */

    __mod_zone_page_state(zone, NR_FREE_PAGES, -(i order));

    spin_unlock(&zone->lock);/* 釋放管理區的自旋鎖 */

    return i;

1.1.1.4         __rmqueue函數

夥伴系統配置設定頁面的總入口是__rmqueue函數:

 * 從管理區的夥伴系統中取出頁面。該函數是配置設定夥伴頁的真正入口。

 *         zone:        從該管理區的夥伴系統中配置設定頁面.

 *        order:        要配置設定的頁面階數.即配置設定的頁面數是2^order

 *        migratetype:    優先從本參數指定的遷移類型中配置設定頁面.如果失敗,再從備用遷移清單中配置設定.

static struct page *__rmqueue(struct zone *zone, unsigned int order,

                        int migratetype)

retry_reserve:

    /* 優先從指定的遷移類型連結清單中配置設定頁面 */

    page = __rmqueue_smallest(zone, order, migratetype);

     * 如果滿足以下兩個條件,就從備用連結清單中配置設定頁面:

     *        快速流程沒有配置設定到頁面,需要從備用遷移連結清單中配置設定.

     *        目前不是從保留的連結清單中配置設定.因為保留的連結清單是最後可用的連結清單,不能從該連結清單配置設定的話,說明本管理區真的沒有可用記憶體了.

    if (unlikely(!page) && migratetype != MIGRATE_RESERVE) {

        /* 從備用連結清單中配置設定頁面. */

        page = __rmqueue_fallback(zone, order, migratetype);

         * Use MIGRATE_RESERVE rather than fail an allocation. goto

         * is used because __rmqueue_smallest is an inline function

         * and we want just one call site

        if (!page) {/* 備用連結清單中沒有配置設定到頁面,看來得吃老本,從保留連結清單中配置設定頁面了 */

            migratetype = MIGRATE_RESERVE;

            goto retry_reserve;/* 跳轉到retry_reserve,從保留的連結清單中配置設定頁面 */

    /* 調試代碼,很高興可以飄過 */

    trace_mm_page_alloc_zone_locked(page, order, migratetype);

快速流程的處理函數是__rmqueue_smallest:

 * 周遊指定遷移類型的夥伴系統連結清單,從連結清單中移動最小數量的頁面傳回給調用者.這是夥伴系統的快速處理流程.

 *        zone:        在該管理區的夥伴系統中配置設定頁面

 *        order:        要配置設定的頁面數量階.

 *        migratetype:    在該遷移類型的連結清單中擷取頁面

struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,

    unsigned int current_order;

    struct free_area * area;

    /* Find a page of the appropriate size in the preferred list */

    /* 從指定的階到最大階進行周遊,直到找到一個可以配置設定的連結清單 */

    for (current_order = order; current_order MAX_ORDER; ++current_order) {

        /* 找到該階對應的空閑頁面連結清單 */

        area = &(zone->free_area[current_order]);

        /* 指定遷移類型的空閑連結清單為空,搜尋下一階的連結清單 */

        if (list_empty(&area->free_list[migratetype]))

            continue;

        /* 取對外連結表的第一個元素 */

        page = list_entry(area->free_list[migratetype].next,

                            struct page, lru);

        /* 将第一個元素從連結清單中摘除 */

        /* rmv_page_order将頁面的夥伴标志位清0,表示該頁不再屬于夥伴系統.同時将Private标志清0 */

        rmv_page_order(page);

        /* 目前階的空閑計數減1 */

        area->nr_free--;

        /**

         * 如果目前階沒有可用的空閑頁,而必須将高階的空閑頁面分割,則expand将高階的頁面分割後放回夥伴系統

         * 如:我們将階為4的頁面分割,并向上層傳回階為2的頁面,那麼,我們需要将其中階為3,2的頁面放回夥伴系統.

        expand(zone, page, order, current_order, area, migratetype);

        return page;

輔助函數expand:

 * 将高階的頁面分割成低階的頁面,并放回夥伴系統.

 *        low:        配置設定出去的低階

 *        high:        被分割的高階

static inline void expand(struct zone *zone, struct page *page,

    int low, int high, struct free_area *area,

    int migratetype)

    unsigned long size = 1 high;/* 要被分割的高階頁面數量 */

     * 我們假設将階為4的頁面組分割,并配置設定出去階為2的頁

     * 那麼我們需要向夥伴系統中分别加入階為2,3的頁面組.

     * 這種情況下,high=4,low=2

    while (high > low) {

        area--;/* area,high,size分别表示本次要放回夥伴系統的連結清單指針,階,目前階的頁面數量 */

        high--;

        size >>= 1;

        /* 檢查要加入夥伴系統的第一頁是否在管理區的範圍内 */

        VM_BUG_ON(bad_range(zone, &page[size]));

        /* 将第一頁鍊入相應的遷移類型連結清單中 */

        list_add(&page[size].lru, &area->free_list[migratetype]);

        /* 該階的空閑頁面計數值加1 */

        area->nr_free++;

        /* 标記頁面的階 */

        set_page_order(&page[size], high);

夥伴系統的慢速處理流程是:

 * 如果管理區的目前遷移連結清單中沒有可配置設定的頁面時,則從備用遷移連結清單中配置設定

static inline struct page *

__rmqueue_fallback(struct zone *zone, int order, int start_migratetype)

    int current_order;

    int migratetype, i;

    /* Find the largest possible block of pages in the other list */

    /* 從最高階搜尋,這樣可以盡量的将其他遷移清單中的大塊分割,避免形成過多的碎片 */

    for (current_order = MAX_ORDER-1; current_order >= order;

                        --current_order) {

        for (i = 0; i MIGRATE_TYPES - 1; i++) {/* 周遊備用的遷移連結清單 */

            migratetype = fallbacks[start_migratetype][i];

            /* MIGRATE_RESERVE handled later if necessary */

            /* 本函數不處理MIGRATE_RESERVE類型的遷移連結清單,如果本函數傳回NULL,則上層函數直接從MIGRATE_RESERVE中配置設定 */

            if (migratetype == MIGRATE_RESERVE)

                continue;

            area = &(zone->free_area[current_order]);

            /* 備用連結清單沒有可用的頁面,繼續搜尋下一連結清單 */

            if (list_empty(&area->free_list[migratetype]))

            /* 取備用連結清單的第一個元素 */

            page = list_entry(area->free_list[migratetype].next,

                    struct page, lru);

            /* 備用連結清單的目前階計數減1 */

            area->nr_free--;

             * If breaking a large block of pages, move all free

             * pages to the preferred allocation list. If falling

             * back for a reclaimable kernel allocation, be more

             * aggressive about taking ownership of free pages

            if (unlikely(current_order >= (pageblock_order >> 1)) ||/* 要分割的頁面是一個大頁面,則将整個頁面全部遷移到目前遷移類型的連結清單中,這樣可以避免過多的碎片 */

                    start_migratetype == MIGRATE_RECLAIMABLE ||/* 目前配置設定的是可回收頁面,這類頁面有突發的特點,将頁面全部遷移到可回收連結清單中,可以避免将其他遷移連結清單分割成太多的碎片 */

                    page_group_by_mobility_disabled) {/* 指定了遷移政策,總是将被分割的頁面遷移 */

                unsigned long pages;

                /* 将頁面從目前遷移連結清單轉換到start_migratetype指定的遷移連結清單中 */

                pages = move_freepages_block(zone, page,

                                start_migratetype);

                /* Claim the whole block if over half of it is free */

                /* pages是移動的頁面數,如果可移動的頁面數量較多,則将整個大記憶體塊的遷移類型修改 */

                if (pages >= (1 (pageblock_order-1)) ||

                        page_group_by_mobility_disabled)

                    set_pageblock_migratetype(page,

                migratetype = start_migratetype;

            }

            /* Remove the page from the freelists */

            /* 将頁面從夥伴夥伴系統連結清單中移除 */

            list_del(&page->lru);

            rmv_page_order(page);

            /* Take ownership for orders >= pageblock_order */

            if (current_order >= pageblock_order)

                change_pageblock_range(page, current_order,

                            start_migratetype);

            /* 将大階的頁面塊中未用的部分還回夥伴系統 */

            expand(zone, page, order, current_order, area, migratetype);

            trace_mm_page_alloc_extfrag(page, order, current_order,

                start_migratetype, migratetype);

            return page;

繼續閱讀