天天看點

一篇圖解Linux記憶體碎片整理

作者:linux上的碼農

我們知道實體記憶體是以頁為機關進行管理的,每個記憶體頁大小預設是4K(大頁除外)。申請實體記憶體時,一般都是按順序配置設定的,但釋放記憶體的行為是随機的。随着系統運作時間變長後,将會出現以下情況:

一篇圖解Linux記憶體碎片整理

如上圖所示,當使用者需要申請位址連續的 3 個記憶體頁時,雖然系統中空閑的記憶體頁數量足夠,但由于空閑的記憶體頁相對分散,進而導緻配置設定失敗。這些位址不連續的記憶體頁被稱為:記憶體碎片。

要解決這個問題也比較簡單,隻需要把空閑的記憶體塊移動到一起即可。如下圖所示:

一篇圖解Linux記憶體碎片整理

網絡上有句很有名的話:理想很美好,現實很骨感。

記憶體整理也是這樣,看起來很簡單,但實作起來就不那麼簡單了。因為在記憶體整理後,需要修正程序的虛拟記憶體與實體記憶體之間的映射關系。如下圖所示:

一篇圖解Linux記憶體碎片整理

但由于 Linux 核心有個名為 記憶體頁反向映射 的功能,是以記憶體整理就變得簡單起來。

接下來,我們将會分析記憶體碎片整理的原理與實作。

記憶體碎片整理原理

記憶體碎片整理的原理比較簡單:在記憶體碎片整理開始前,會在記憶體區的頭和尾各設定一個指針,頭指針從頭向尾掃描可移動的頁,而尾指針從尾向頭掃描空閑的頁,當他們相遇時終止整理。下面說說記憶體随便整理的過程(原理參考了核心文檔):

  1. 初始時記憶體狀态:
一篇圖解Linux記憶體碎片整理

在上圖中,白色塊表示空閑的記憶體頁,而紅色塊表示已配置設定出去的記憶體頁。在初始狀态時,記憶體中存在多個碎片。如果此時要申請 3 個位址連續的記憶體頁,那麼将會申請失敗。

  1. 記憶體碎片整理掃描開始:
一篇圖解Linux記憶體碎片整理

頭部指針從頭掃描可移動頁,而尾部指針從從尾掃描空閑頁。在整理時,将可移動頁的内容複制到空閑頁中。複制完成後,将可移動記憶體頁釋放即可。

  1. 最後結果:
一篇圖解Linux記憶體碎片整理

經過記憶體碎片整理後,如果現在要申請 3 個位址連續的記憶體頁,就能申請成功了。

更多linux核心視訊教程文檔資料免費領取背景私信【核心】自行擷取.

一篇圖解Linux記憶體碎片整理
一篇圖解Linux記憶體碎片整理

記憶體碎片整理實作

接下來,我們将會分析記憶體碎片整理的實作過程。

注:本文使用的是 Linux-2.6.36 版本的記憶體

1. 記憶體碎片整理時機

當要申請多個位址聯系的記憶體頁時,如果申請失敗,将會進行記憶體碎片整理。其調用鍊如下:

alloc_pages_node()
└→ __alloc_pages()
   └→ __alloc_pages_nodemask()
      └→ __alloc_pages_slowpath()
         └→ __alloc_pages_direct_compact()

           

當調用 alloc_pages_node() 函數申請多個位址連續的記憶體頁失敗時,将會觸發調用 __alloc_pages_direct_compact() 函數來進行記憶體碎片整理。我們來看看 __alloc_pages_direct_compact() 函數的實作:

static struct page *
__alloc_pages_direct_compact(gfp_t gfp_mask, 
                             unsigned int order, 
                             struct zonelist *zonelist, 
                             enum zone_type high_zoneidx, 
                             nodemask_t *nodemask, 
                             int alloc_flags,
                             struct zone *preferred_zone,
                             int migratetype, 
                             unsigned long *did_some_progress)
{
    struct page *page;

    // 1. 如果申請一個記憶體頁,那麼就沒有整理碎片的必要(這說明是記憶體不足,而不是記憶體碎片導緻)
    if (!order || compaction_deferred(preferred_zone))
        return NULL;

    // 2. 開始進行記憶體碎片整理
    *did_some_progress = try_to_compact_pages(zonelist, order, gfp_mask, nodemask);

    if (*did_some_progress != COMPACT_SKIPPED) {
        ...
        // 3. 整理完記憶體碎片後,繼續嘗試申請記憶體塊
        page = get_page_from_freelist(gfp_mask, nodemask, order, zonelist, 
                                      high_zoneidx, alloc_flags, preferred_zone, 
                                      migratetype);
        if (page) {
            ...
            return page;
        }
        ...
    }

    return NULL;
}

           

__alloc_pages_direct_compact() 函數是記憶體碎片整理的入口,其主要完成 3 個步驟:

  • 先判斷申請的記憶體塊是否隻有一個記憶體頁,如果是,那麼就沒有整理碎片的必要(這說明是記憶體不足,而不是記憶體碎片導緻)。
  • 如果需要進行記憶體碎片整理,那麼調用 try_to_compact_pages() 函數進行記憶體碎片整理。
  • 整理完記憶體碎片後,調用 get_page_from_freelist() 函數繼續嘗試申請記憶體塊。

2. 記憶體碎片整理過程

由于記憶體碎片整理的具體實作在 try_to_compact_pages() 函數中進行,是以我們繼續來看看 try_to_compact_pages() 函數的實作:

unsigned long
try_to_compact_pages(struct zonelist *zonelist, int order, gfp_t gfp_mask,
                     nodemask_t *nodemask)
{
    ...
    // 1. 周遊所有記憶體區(由于核心會把實體記憶體分成多個記憶體區進行管理)
    for_each_zone_zonelist_nodemask(zone, z, zonelist, high_zoneidx, nodemask) {
        ...
        // 2. 對記憶體區進行記憶體碎片整理
        status = compact_zone_order(zone, order, gfp_mask);
        ...
    }

    return rc;
}

           

可以看出,try_to_compact_pages() 函數最終會調用 compact_zone_order() 函數來進行記憶體碎片整理。我們隻能進行來分析 compact_zone_order() 函數:

static unsigned long
compact_zone_order(struct zone *zone, int order, gfp_t gfp_mask)
{
    struct compact_control cc = {
        .nr_freepages = 0,
        .nr_migratepages = 0,
        .order = order,
        .migratetype = allocflags_to_migratetype(gfp_mask),
        .zone = zone,
    };
    INIT_LIST_HEAD(&cc.freepages);
    INIT_LIST_HEAD(&cc.migratepages);

    return compact_zone(zone, &cc);
}

           

到這裡,我們還沒有看到記憶體碎片整理的具體實作(調用鍊可真深啊 ^_^!),compact_zone_order() 函數也是構造了一些參數,然後繼續調用 compact_zone() 來進行記憶體碎片整理:

static int compact_zone(struct zone *zone, struct compact_control *cc)
{
    ...
    while ((ret = compact_finished(zone, cc)) == COMPACT_CONTINUE) {
        ...
        // 1. 收集可移動的記憶體頁清單
        if (!isolate_migratepages(zone, cc))
            continue;
        ...
        // 2. 将可移動的記憶體頁清單遷移到空閑清單中
        migrate_pages(&cc->migratepages, compaction_alloc, (unsigned long)cc, 0);
        ...
    }
    ...
    return ret;
}

           

在 compact_zone() 函數裡,我們終于看到記憶體碎片整理的邏輯了。compact_zone() 函數主要完成 2 個步驟:

  • 調用 isolate_migratepages() 函數收集可移動的記憶體頁清單。
  • 調用 migrate_pages() 函數将可移動的記憶體頁清單遷移到空閑清單中。

這兩個函數非常重要,我們分别來分析它們是怎麼實作的。

isolate_migratepages() 函數

isolate_migratepages() 函數用于收集可移動的記憶體頁清單,我們來看看其實作:

static unsigned long
isolate_migratepages(struct zone *zone, struct compact_control *cc)
{
    unsigned long low_pfn, end_pfn;
    struct list_head *migratelist = &cc->migratepages;
    ...

    // 1. 掃描記憶體區所有的記憶體頁
    for (; low_pfn < end_pfn; low_pfn++) {
        struct page *page;
        ...

        // 2. 通過記憶體頁的編号擷取記憶體頁對象
        page = pfn_to_page(low_pfn);
       ...

        // 3. 判斷記憶體頁是否可移動記憶體頁,如果不是可移動記憶體頁,那麼就跳過
        if (__isolate_lru_page(page, ISOLATE_BOTH, 0) != 0)
            continue;

        // 4. 将記憶體頁從 LRU 隊列中删除
        del_page_from_lru_list(zone, page, page_lru(page));

        // 5. 添加到可移動記憶體頁清單中
        list_add(&page->lru, migratelist); 
        ...
        cc->nr_migratepages++;
        ...
    }
    ...
    return cc->nr_migratepages;
}

           

isolate_migratepages() 函數主要完成 5 個步驟,分别是:

  • 掃描記憶體區所有的記憶體頁(與記憶體碎片整理原理一緻)。
  • 通過記憶體頁的編号擷取記憶體頁對象。
  • 判斷記憶體頁是否可移動記憶體頁,如果不是可移動記憶體頁,那麼就跳過。
  • 将記憶體頁從 LRU 隊列中删除,這樣可避免被其他程序回收這個記憶體頁。
  • 添加到可移動記憶體頁清單中。

當完成這 5 個步驟後,核心就收集到可移動的記憶體頁清單。

migrate_pages() 函數

migrate_pages() 函數負責将可移動的記憶體頁清單遷移到空閑清單中,我們來分析一下其實作過程:

int migrate_pages(struct list_head *from, new_page_t get_new_page,
                  unsigned long private, int offlining)
{
    ...

    for (pass = 0; pass < 10 && retry; pass++) {
        retry = 0;

        // 1. 周遊可移動記憶體頁清單
        list_for_each_entry_safe(page, page2, from, lru) {
            ...
            // 2. 将可移動記憶體頁遷移到空閑記憶體頁中
            rc = unmap_and_move(get_new_page, private, page, pass > 2, offlining);
            switch(rc) {
            case -ENOMEM:
                goto out;
            case -EAGAIN:
                retry++;
                break;
            case 0:
                break;
            default:
                nr_failed++;
                break;
            }
        }
    }
    ...
    return nr_failed + retry;
}

           

migrate_pages() 函數的邏輯很簡單,主要完成 2 個步驟:

  • 周遊可移動記憶體頁清單,這個清單就是通過 isolate_migratepages() 函數收集的可移動記憶體頁清單。
  • 調用 unmap_and_move() 函數将可移動記憶體頁遷移到空閑記憶體頁中。

可以看出,具體的記憶體遷移過程在 unmap_and_move() 函數中實作。我們來看看 unmap_and_move() 函數的實作:

static int
unmap_and_move(new_page_t get_new_page, unsigned long private,
               struct page *page, int force, int offlining)
{
    ...
    // 1. 從記憶體區中找到一個空閑的記憶體頁
    struct page *newpage = get_new_page(page, private, &result);
    ...

    // 2. 解開所有使用了目前可移動記憶體頁的程序的虛拟記憶體映射(涉及到記憶體頁反向映射)
    try_to_unmap(page, TTU_MIGRATION|TTU_IGNORE_MLOCK|TTU_IGNORE_ACCESS);

skip_unmap:
    // 3. 将可移動記憶體頁的資料複制到空閑記憶體頁中
    if (!page_mapped(page))
        rc = move_to_new_page(newpage, page, remap_swapcache);
    ...
    return rc;
}

           

由于 unmap_and_move() 函數的實作比較複雜,是以我們對其進行了簡化。可以看出,unmap_and_move() 函數主要完成 3 個工作:

  • 從記憶體區中找到一個空閑的記憶體頁。根據記憶體碎片整理算法,會從記憶體區最後開始掃描,找到合适的空閑記憶體頁。
  • 由于将可移動記憶體頁遷移到空閑記憶體頁後,程序的虛拟記憶體映射将會發生變化。是以,這裡要調用 try_to_unmap() 函數來解開所有使用了目前可移動記憶體頁的映射。
  • 調用 move_to_new_page() 函數将可移動記憶體頁的資料複制到空閑記憶體頁中。在 move_to_new_page() 函數中,還會重建立立程序的虛拟記憶體映射,這樣使用了目前可移動記憶體頁的程序就能夠正常運作。

至此,記憶體碎片整理的過程已經分析完畢。

不過細心的讀者可能發現,在文中并沒有分析重新建構虛拟記憶體映射的過程。是的,因為重新建構虛拟記憶體映射要涉及到 記憶體頁反向映射 的知識點,後續的文章會介紹這個知識點,是以這裡就不作詳細分析了。

總結

從上面的分析可知,記憶體碎片整理 是為了解決:在申請多個位址連續的記憶體頁時,空閑記憶體頁數量充足,但還是配置設定失敗的情況。

但由于記憶體碎片整理需要消耗大量的 CPU 時間,是以我們在申請記憶體時,可以通過指定 __GFP_WAIT 标志位(不等待)來避免記憶體碎片整理過程。

首頁 - 核心技術中文網 - 建構全國最權威的核心技術交流分享論壇

轉載位址:一篇圖解Linux記憶體碎片整理 - 圈點 - 核心技術中文網 - 建構全國最權威的核心技術交流分享論壇

一篇圖解Linux記憶體碎片整理

繼續閱讀