我們知道實體記憶體是以頁為機關進行管理的,每個記憶體頁大小預設是4K(大頁除外)。申請實體記憶體時,一般都是按順序配置設定的,但釋放記憶體的行為是随機的。随着系統運作時間變長後,将會出現以下情況:
如上圖所示,當使用者需要申請位址連續的 3 個記憶體頁時,雖然系統中空閑的記憶體頁數量足夠,但由于空閑的記憶體頁相對分散,進而導緻配置設定失敗。這些位址不連續的記憶體頁被稱為:記憶體碎片。
要解決這個問題也比較簡單,隻需要把空閑的記憶體塊移動到一起即可。如下圖所示:
網絡上有句很有名的話:理想很美好,現實很骨感。
記憶體整理也是這樣,看起來很簡單,但實作起來就不那麼簡單了。因為在記憶體整理後,需要修正程序的虛拟記憶體與實體記憶體之間的映射關系。如下圖所示:
但由于 Linux 核心有個名為 記憶體頁反向映射 的功能,是以記憶體整理就變得簡單起來。
接下來,我們将會分析記憶體碎片整理的原理與實作。
記憶體碎片整理原理
記憶體碎片整理的原理比較簡單:在記憶體碎片整理開始前,會在記憶體區的頭和尾各設定一個指針,頭指針從頭向尾掃描可移動的頁,而尾指針從尾向頭掃描空閑的頁,當他們相遇時終止整理。下面說說記憶體随便整理的過程(原理參考了核心文檔):
- 初始時記憶體狀态:
在上圖中,白色塊表示空閑的記憶體頁,而紅色塊表示已配置設定出去的記憶體頁。在初始狀态時,記憶體中存在多個碎片。如果此時要申請 3 個位址連續的記憶體頁,那麼将會申請失敗。
- 記憶體碎片整理掃描開始:
頭部指針從頭掃描可移動頁,而尾部指針從從尾掃描空閑頁。在整理時,将可移動頁的内容複制到空閑頁中。複制完成後,将可移動記憶體頁釋放即可。
- 最後結果:
經過記憶體碎片整理後,如果現在要申請 3 個位址連續的記憶體頁,就能申請成功了。
更多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記憶體碎片整理 - 圈點 - 核心技術中文網 - 建構全國最權威的核心技術交流分享論壇