提要:本系列文章主要參考MIT 6.828課程
以及兩本書籍《深入理解Linux內(nèi)核》
《深入Linux內(nèi)核架構(gòu)》
對Linux內(nèi)核內(nèi)容進行總結(jié)。
內(nèi)存管理的實現(xiàn)覆蓋了多個領(lǐng)域:
- 內(nèi)存中的物理內(nèi)存頁的管理
- 分配大塊內(nèi)存的伙伴系統(tǒng)
- 分配較小內(nèi)存的slab、slub、slob分配器
- 分配非連續(xù)內(nèi)存塊的vmalloc分配器
- 進程的地址空間
內(nèi)核初始化后,內(nèi)存管理的工作就交由伙伴系統(tǒng)
來承擔,作為眾多內(nèi)存分配器的基礎(chǔ),我們必須要對其進行一個詳細的解釋。但是由于伙伴系統(tǒng)的復(fù)雜性,因此,本節(jié)會首先給出一個簡單的例子,然后由淺入深,逐步解析伙伴系統(tǒng)的細節(jié)。
伙伴系統(tǒng)簡介
伙伴系統(tǒng)將所有的空閑頁框分為了11個塊鏈表,每個塊鏈表分別包含大小為1,2,4,\(2^3\),\(2^4\),...,\(2^{10}\)個連續(xù)的頁框(每個頁框大小為4K),\(2^{n}\)中的n被稱為order
(分配階
),因此在代碼中這11個塊鏈表的表示就是一個長度為11的數(shù)組。考察表示Zone結(jié)構(gòu)的代碼,可以看到一個名為free_area
的屬性,該屬性用于保存這11個塊鏈表。
struct zone {
...
/*
* 不同長度的空閑區(qū)域
*/
struct free_area free_area[MAX_ORDER];
...
};
結(jié)合之前的知識,我們總結(jié)一下,Linux內(nèi)存管理的結(jié)構(gòu)形如下圖:
當然,這還不是完整的,我們本節(jié)就會將其填充完整。最后借用《深入理解Linux內(nèi)核》中的一個例子簡單介紹一下該算法的工作原理
進而結(jié)束簡介這一小節(jié)。
假設(shè)要請求一個256個頁框(2^8)
的塊(即1MB)。
- 算法先在256個頁的鏈表中檢查是否有一個空閑塊。
- 如果沒有這樣的塊,算法會查找下一個更大的頁塊,也就是,在512個頁框的鏈表中找一個空閑塊。
- 如果存在這樣的塊,內(nèi)核就把256的頁框分成兩等份,一半用作滿足請求,另一半插人到256個頁框的鏈表中。
- 如果在512個頁框的塊鏈表中也沒找到空閑塊,就繼續(xù)找更大的塊 一一1024個頁框的塊。
- 如果這樣的塊存在,內(nèi)核把1024個頁框塊的256個頁框用作請求,然后從剩余的768個頁框中拿512個插入到512個頁框的鏈表中
- 再把最后的256個插人到256個頁框的鏈表中。
- 如果1024個頁框的鏈表還是空的,算法就放棄并發(fā)出錯信號
以上過程的逆過程就是頁框塊的釋放過程,也是該算法名字的由來。內(nèi)核試圖把大小為b的一對空閑伙伴塊合并為一個大小為2b的單獨塊
。滿足以下條件的兩個塊稱為伙伴:
- 兩個塊具有相同的大小,記作 b。
- 它們的物理地址是連續(xù)的。
- 第一塊的第一個頁框的物理地址是2 x b x \(2^{12}\)的倍數(shù)。
注意:該算法是迭代的,如果它成功合并所釋放的塊,它會試圖合并2b的塊,以再次試圖形成更大的塊。然而伙伴系統(tǒng)的實現(xiàn)并沒有這么簡單。
避免碎片
伙伴系統(tǒng)作為內(nèi)存管理系統(tǒng),也難以逃脫一個經(jīng)典的難題,物理內(nèi)存的碎片問題
。尤其是在系統(tǒng)長期運行后,其內(nèi)存可能會變成如下的樣子:
為了解決這個問題,Linux提供了兩種避免碎片的方式:
- 可移動頁
- 虛擬可移動內(nèi)存區(qū)
可移動頁
物理內(nèi)存被零散的占據(jù),無法尋找到一塊連續(xù)的大塊內(nèi)存。內(nèi)核2.6.24版本,防止碎片的方法最終加入內(nèi)核。內(nèi)核采用的方法是反碎片
,即試圖從最初開始盡可能防止碎片
。因為許多物理內(nèi)存頁不能移動到任意位置,因此無法整理碎片
。
可以看到,內(nèi)核中內(nèi)存碎片難以處理的主要原因是許多頁無法移動到任意位置
,那么如果我們將其單獨管理,在分配大塊內(nèi)存時,嘗試從可以任意移動的內(nèi)存區(qū)域內(nèi)分配,是不是更好呢?
為了達成這一點,Linux首先要了解哪些頁是可移動的,因此,操作系統(tǒng)將內(nèi)核已分配的頁劃分為如下3種類型:
類別名稱 | 描述 |
---|---|
不可移動頁 | 在內(nèi)存中有固定位置,不能移動到其他地方。核心內(nèi)核分配的大多數(shù)內(nèi)存屬于該類別 |
可回收頁 | 不能直接移動,但可以刪除,其內(nèi)容可以從某些源重新生成 |
可移動頁 | 可以隨意移動。屬于用戶空間應(yīng)用程序的頁屬于該類別。它們是通過頁表映射的。如果它們復(fù)制到新位置,頁表項可以相應(yīng)地更新,應(yīng)用程序不會注意到任何事 |
內(nèi)核中定義了一系列宏來表示不同的遷移類型:
#define MIGRATE_UNMOVABLE 0 // 不可移動頁
#define MIGRATE_RECLAIMABLE 1 // 可回收頁
#define MIGRATE_MOVABLE 2 // 可移動頁
#define MIGRATE_RESERVE 3
#define MIGRATE_ISOLATE 4 /* 不能從這里分配 */
#define MIGRATE_TYPES 5
對于其他兩種類型(了解就好):
- MIGRATE_RESERVE:如果向具有特定可移動性的列表請求分配內(nèi)存失敗,這種緊急情況下可從MIGRATE_RESERVE分配內(nèi)存
- MIGRATE_ISOLATE:是一個特殊的虛擬區(qū)域,用于跨越NUMA結(jié)點移動物理內(nèi)存頁。在大型系統(tǒng)上,它有益于將物理內(nèi)存頁移動到接近于使用該頁最頻繁的CPU。
伙伴系統(tǒng)實現(xiàn)頁的可移動性特性,依賴于數(shù)據(jù)結(jié)構(gòu)free_area
,其代碼如下:
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
屬性名 | 描述 |
---|---|
free_list | 每種遷移類型對應(yīng)一個空閑頁鏈表 |
nr_free |
所有 列表上空閑頁的數(shù)目 |
與zone.free_area
一樣,free_area.free_list
也是一個鏈表,但這個鏈表終于直接連接struct page
了。因此,我們的內(nèi)存管理結(jié)構(gòu)圖就變成了如下的樣子:
與NUMA內(nèi)存域無法滿足分配請求時會有一個備用列表一樣,當一個遷移類型列表無法滿足分配請求時,同樣也會有一個備用列表,不過這個列表不用代碼生成,而是寫死的:
/*
* 該數(shù)組描述了指定遷移類型的空閑列表耗盡時,其他空閑列表在備用列表中的次序。
*/
static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = {
[MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE },
[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE },
[MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
[MIGRATE_RESERVE] = { MIGRATE_RESERVE, MIGRATE_RESERVE, MIGRATE_RESERVE },/* 從來不用 */
};
該數(shù)據(jù)結(jié)構(gòu)大體上是自明的:在內(nèi)核想要分配不可移動頁時,如果對應(yīng)鏈表為空,則后退到可回收頁鏈表,接下來到可移動頁鏈表,最后到緊急分配鏈表。
在各個遷移鏈表之間,當前的頁面分配狀態(tài)可以從/proc/pagetypeinfo獲得:
虛擬可移動內(nèi)存域
可移動頁給與內(nèi)存分配一種層級分配的能力(按照備用列表順序分配)。但是可能會導(dǎo)致不可移動頁侵入可移動頁區(qū)域。
內(nèi)核在2.6.23版本將虛擬可移動內(nèi)存域(ZONE_MOVABLE)
這一功能加入內(nèi)核。其基本思想為:可用的物理內(nèi)存劃分為兩個內(nèi)存域,一個用于可移動分配,一個用于不可移動分配。這會自動防止不可移動頁向可移動內(nèi)存域引入碎片。
取決于體系結(jié)構(gòu)和內(nèi)核配置,ZONE_MOVABLE內(nèi)存域可能位于高端或普通內(nèi)存域:
enum zone_type {
...
ZONE_NORMAL
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
MAX_NR_ZONES
};
與系統(tǒng)中所有其他的內(nèi)存域相反,ZONE_MOVABLE并不關(guān)聯(lián)到任何硬件上有意義的內(nèi)存范圍。實際上,該內(nèi)存域中的內(nèi)存取自高端內(nèi)存域或普通內(nèi)存域,因此我們在下文中稱ZONE_MOVABLE是一個虛擬內(nèi)存域。
那么用于可移動分配和不可移動分配的內(nèi)存域大小如何分配呢?系統(tǒng)提供了兩個參數(shù)用來分配這兩個區(qū)域的大?。?/p>
- kernelcore參數(shù)用來指定用于不可移動分配的內(nèi)存數(shù)量,即用于既不能回收也不能遷移的內(nèi)存數(shù)量。剩余的內(nèi)存用于可移動分配。
- 還可以使用參數(shù)movablecore控制用于可移動內(nèi)存分配的內(nèi)存數(shù)量
輔助函數(shù)find_zone_movable_pfns_for_nodes用于計算進入ZONE_MOVABLE的內(nèi)存數(shù)量。如果kernelcore和movablecore參數(shù)都沒有指定,find_zone_movable_pfns_for_nodes會使ZONE_MOVABLE保持為空,該機制處于無效狀態(tài)。
但是ZONE_MOVABLE內(nèi)存域的內(nèi)存會按照如下情況分配:
- 用于不可移動分配的內(nèi)存會平均地分布到所有內(nèi)存結(jié)點上。
- 只使用來自最高內(nèi)存域的內(nèi)存。在內(nèi)存較多的32位系統(tǒng)上,這通常會是ZONE_HIGHMEM,但是對于64位系統(tǒng),將使用ZONE_NORMAL或ZONE_DMA32。
為ZONE_MOVABLE內(nèi)存域分配內(nèi)存后,會保存在如下位置:
- 用于為虛擬內(nèi)存域ZONE_MOVABLE提取內(nèi)存頁的物理內(nèi)存域,保存在全局變量movable_zone中;
- 對每個結(jié)點來說,zone_movable_pfn[node_id]表示ZONE_MOVABLE在movable_zone內(nèi)存域中所取得內(nèi)存的起始地址。
伙伴系統(tǒng)頁面分配與回收
就伙伴系統(tǒng)的接口而言,NUMA或UMA體系結(jié)構(gòu)是沒有差別的,二者的調(diào)用語法都是相同的。所有函數(shù)的一個共同點是:只能分配2的整數(shù)冪個頁。本節(jié)我們會按照如下順序介紹伙伴系統(tǒng)頁面的分配與回收:
- 介紹伙伴系統(tǒng)API接口
- 介紹API的核心邏輯
我們會按照分配頁面與回收頁面兩節(jié)分別介紹。
分配頁面
分配頁面API
分配頁面的API包含如下4個:
API | 描述 |
---|---|
alloc_pages(mask, order) | 分配\(2^{order}\)頁并返回一個struct page的實例,表示分配的內(nèi)存塊的起始頁 |
alloc_page(mask) | alloc_pages(mask,0)的改寫,只分配1頁內(nèi)存 |
get_zeroed_page(mask) | 分配一頁并返回一個page實例,頁對應(yīng)的內(nèi)存填充0 |
__get_free_pages(mask, order) | 分配頁面,但返回分配內(nèi)存塊的虛擬地址 |
get_dma_pages(gfp_mask, order) | 用來獲得適用于DMA的頁 |
在空閑內(nèi)存無法滿足請求以至于分配失敗的情況下,所有上述函數(shù)都返回空指針(alloc_pages和alloc_page)或者0(get_zeroed_page、__get_free_pages和__get_free_page)。
可以看到,每個分配頁面的接口都包含一個mask參數(shù),該參數(shù)是內(nèi)存修飾符,用來控制內(nèi)存分配的邏輯,例如內(nèi)存在哪個內(nèi)存區(qū)分配等,為了控制這一點,內(nèi)核提供了如下宏:
/* GFP_ZONEMASK中的內(nèi)存域修飾符(參見linux/mmzone.h,低3位) */
#define __GFP_DMA ((__force gfp_t)0x01u)
#define __GFP_HIGHMEM ((__force gfp_t)0x02u)
#define __GFP_DMA32 ((__force gfp_t)0x04u)
...
#define __GFP_MOVABLE ((__force gfp_t)0x100000u) /* 頁是可移動的 */
注意:設(shè)置__GFP_MOVABLE不會影響內(nèi)核的決策,除非它與__GFP_HIGHMEM同時指定。在這種情況下,會使用特殊的虛擬內(nèi)存域ZONE_MOVABLE滿足內(nèi)存分配請求。
這里給出其他一些掩碼的含義(需要用時現(xiàn)查):
實際上,上面所有用于分配頁面的API,最終都是通過alloc_pages_node
方法進行內(nèi)存分配的,其調(diào)用關(guān)系如下:
后面我們將主要討論alloc_pages_node
方法的具體邏輯。
alloc_pages_node:分配頁面的具體邏輯
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,
unsigned int order)
{
if (unlikely(order >= MAX_ORDER))
return NULL;
/* 未知結(jié)點即當前結(jié)點 */
if(nid< 0)
nid = numa_node_id();
return __alloc_pages(gfp_mask, order,NODE_DATA(nid)->node_zonelists + gfp_zone(gfp_mask));
}
alloc_pages_node
方法很簡單,進行了一些簡單的檢查,并將頁面的分配邏輯交由__alloc_pages
方法處理。這里我們又見到了老朋友zonelist,如果不熟悉請參見該鏈接。gfp_zone方法,負責根據(jù)gfp_mask選擇分配內(nèi)存的內(nèi)存域,因此可以通過指針運算,選擇合適的zonelist(內(nèi)存區(qū)選擇備用列表)。
分配頁面需要大量的檢查以及選擇合適的內(nèi)存域進行分配,在完成這些工作之后,就可以進行真正的分配物理內(nèi)存。__alloc_pages方法就是按照這個邏輯編寫的。
__alloc_pages
會根據(jù)現(xiàn)實情況調(diào)用get_page_from_freelist
方法選擇合適的內(nèi)存域,進行內(nèi)存分配,然而內(nèi)存域是否有空閑空間,也有一定的條件,這個條件由zone_watermark_ok
函數(shù)判斷。這里的判斷條件主要和zone的幾個watermark
有關(guān),即pages_min、pages_low、pages_high,這三個參數(shù)的具體含義可以參考第二章的講解
內(nèi)核提供了如下幾個宏,用于控制到達各個水印指定的臨界狀態(tài)時的行為:
#define ALLOC_NO_WATERMARKS 0x01 /* 完全不檢查水印 */
#define ALLOC_WMARK_MIN 0x02 /* 使用pages_min水印 */
#define ALLOC_WMARK_LOW 0x04 /* 使用pages_low水印 */
#define ALLOC_WMARK_HIGH 0x08 /* 使用pages_high水印 */
#define ALLOC_HARDER 0x10 /* 試圖更努力地分配,即放寬限制 */
#define ALLOC_HIGH 0x20 /* 設(shè)置了__GFP_HIGH */
#define ALLOC_CPUSET 0x40 /* 檢查內(nèi)存結(jié)點是否對應(yīng)著指定的CPU集合 */
前幾個標志表示在判斷頁是否可分配時,需要考慮哪些水印。
- 默認情況下(即沒有因其他因素帶來的壓力而需要更多的內(nèi)存),只有內(nèi)存域包含頁的數(shù)目至少為zone->pages_high時,才能分配頁。這對應(yīng)于ALLOC_WMARK_HIGH標志。
- 如果要使用較低(zone->pages_low)或最低(zone->pages_min)設(shè)置,則必須相應(yīng)地設(shè)置ALLOC_WMARK_MIN或ALLOC_WMARK_LOW
- ALLOC_HARDER通知伙伴系統(tǒng)在急需內(nèi)存時放寬分配規(guī)則
- 在分配高端內(nèi)存域的內(nèi)存時,ALLOC_HIGH進一步放寬限制
- ALLOC_CPUSET告知內(nèi)核,內(nèi)存只能從當前進程允許運行的CPU相關(guān)聯(lián)的內(nèi)存結(jié)點分配,當然該選項只對NUMA系統(tǒng)有意義
zone_watermark_ok
方法,使用了ALLOC_HIGH
和ALLOC_HARDER
標志,其代碼如下:
int zone_watermark_ok(struct zone *z, int order, unsigned long mark,
int classzone_idx, int alloc_flags)
{
/* free_pages可能變?yōu)樨撝?,沒有關(guān)系 */
long min = mark;
long free_pages = zone_page_state(z, NR_FREE_PAGES) -(1 << order) + 1;
int o;
if (alloc_flags & ALLOC_HIGH)
min -= min / 2;
if (alloc_flags & ALLOC_HARDER)
min -= min / 4;
if (free_pages <= min + z->lowmem_reserve[classzone_idx])
return 0;
for(o= 0;o <order;o++){
/* 在下一階,當前階的頁是不可用的 */
free_pages -= z->free_area[o].nr_free << o;
/* 所需高階空閑頁的數(shù)目相對較少 */
min >>= 1;
if (free_pages <= min)
return 0;
}
return 1;
}
注意,zone_watermark_ok
方法中的mark
參數(shù)就是zone中的水印,根據(jù)設(shè)置的ALLOC_WMARK_*
標志的不同,mark選擇對應(yīng)的pages_*
水印,zone_page_state
方法用于訪問內(nèi)存域中的統(tǒng)計量,由于提供了標志NR_FREE_PAGES
,這里獲取的是內(nèi)存域中空閑頁的數(shù)目。
可以看到當flag設(shè)置了ALLOC_HIGH和ALLOC_HARDER后,min的閾值變小了,這也就是所謂的放寬了限制。當前內(nèi)存域需要滿足如下兩個條件才能進行內(nèi)存分配:
- min+lowmem_reserve中指定的緊急分配值 < 內(nèi)存域中的空閑頁數(shù)目
- 對于指定order前的每一個分配階,都要高于當前階的min值(每升高一階,所需空閑頁的最小值折半)
了解了內(nèi)存域的可用性條件后,我們將討論,哪個方法負責從備用列表中選擇合適的內(nèi)存域。該方法為get_page_from_freelist,如果查找到對應(yīng)的內(nèi)存域,將發(fā)起實際的分配操作。
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order,
struct zonelist *zonelist, int alloc_flags)
{
struct zone **z;
struct page *page = NULL;
int classzone_idx = zone_idx(zonelist->zones[0]);
struct zone *zone;
...
/*
* 掃描zonelist,尋找具有足夠空閑空間的內(nèi)存域。
* 請參閱kernel/cpuset.c中cpuset_zone_allowed()的注釋。
*/
z = zonelist->zones;
do {
...
zone = *z;
//cpuset_zone_allowed_softwall是另一個輔助函數(shù),用于檢查給定內(nèi)存域是否屬于該進程允許運行的CPU
if ((alloc_flags & ALLOC_CPUSET) &&!cpuset_zone_allowed_softwall(zone, gfp_mask))
continue;
if (!(alloc_flags & ALLOC_NO_WATERMARKS)) {
unsigned long mark;
if (alloc_flags & ALLOC_WMARK_MIN)
mark = zone->pages_min;
else if (alloc_flags & ALLOC_WMARK_LOW)
mark = zone->pages_low;
else
mark = zone->pages_high;
if (!zone_watermark_ok(zone, order, mark,classzone_idx, alloc_flags))
continue;
}
page = buffered_rmqueue(*z, order, gfp_mask);
if (page) {
zone_statistics(zonelist, *z);
break;
}
} while (*(++z) != NULL);
return page;
}
可以看到do..while循環(huán)遍歷了整個備用列表,通過zone_watermark_ok
方法查找第一個可用的內(nèi)存域,查找到后進行內(nèi)存分配(buffered_rmqueue
方法負責處理分配邏輯)。
__alloc_pages
通過調(diào)用get_page_from_freelist
方法進行實際的分配,但是,分配內(nèi)存的時機是一個很復(fù)雜的問題,在現(xiàn)實生活中,內(nèi)存并不總是充足的,為了充分解決這些情況,__alloc_pages
方法考慮了諸多情況:
-
內(nèi)存充足時,調(diào)用
get_page_from_freelist
方法直接分配:struct page * fastcall __alloc_pages(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist) { const gfp_t wait = gfp_mask & __GFP_WAIT; struct zone **z; struct page *page; struct reclaim_state reclaim_state; struct task_struct *p = current; int do_retry; int alloc_flags; int did_some_progress; might_sleep_if(wait); restart: z = zonelist->zones; /* 適合于gfp_mask的內(nèi)存域列表 */ if (unlikely(*z == NULL)) { /* *如果在沒有內(nèi)存的結(jié)點上使用GFP_THISNODE,導(dǎo)致zonelist為空,就會發(fā)生這種情況 */ return NULL; } page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, order,zonelist, ALLOC_WMARK_LOW|ALLOC_CPUSET); if (page) goto got_pg; ...
可以看到,第一次嘗試分配內(nèi)存時,系統(tǒng)對分配的要求會比較嚴格:
- gft_mask設(shè)置了__GFP_HARDWALL:它限制只在分配到當前進程的各個CPU所關(guān)聯(lián)的結(jié)點分配內(nèi)存。
- flag設(shè)置了ALLOC_WMARK_LOW和ALLOC_CPUSET(這兩個含義代碼注釋里有,這里就不解釋了)
-
首次分配失敗后,內(nèi)核會喚醒負責換出頁的kswapd守護進程,寫回或換出很少使用的頁。在交換守護進程喚醒后,再次嘗試
get_page_from_freelist
:... for (z = zonelist->zones; *z; z++) wakeup_kswapd(*z, order); alloc_flags = ALLOC_WMARK_MIN; if ((unlikely(rt_task(p)) && !in_interrupt()) || !wait) alloc_flags |= ALLOC_HARDER; if (gfp_mask & __GFP_HIGH) alloc_flags |= ALLOC_HIGH; if (wait) alloc_flags |= ALLOC_CPUSET; page = get_page_from_freelist(gfp_mask, order, zonelist, alloc_flags); if (page) goto got_pg; ... }
此處的策略不僅換出了非常用頁,而且放寬了水印的判斷條件:
- alloc_flags成為了ALLOC_WMARK_MIN
- 對實時進程和指定了__GFP_WAIT標志因而不能睡眠的調(diào)用,會設(shè)置ALLOC_HARDER。
-
如果設(shè)置了PF_MEMALLOC或進程設(shè)置了TIF_MEMDIE標志(在這兩種情況下,內(nèi)核不能處于中斷上下文中),內(nèi)核會忽略所有水印,調(diào)用
get_page_from_freelist
方法:rebalance: if (((p->flags & PF_MEMALLOC) || unlikely(test_thread_flag(TIF_MEMDIE)))&& !in_interrupt()) { if (!(gfp_mask & __GFP_NOMEMALLOC)) { nofail_alloc: /* 再一次遍歷zonelist,忽略水印 */ page = get_page_from_freelist(gfp_mask, order,zonelist, ALLOC_NO_WATERMARKS); if (page) goto got_pg; if (gfp_mask & __GFP_NOFAIL) { congestion_wait(WRITE, HZ/50); goto nofail_alloc; } } goto nopage; } ...
通常只有在分配器自身需要更多內(nèi)存時,才會設(shè)置PF_MEMALLOC,而只有在線程剛好被OOM killer機制選中時,才會設(shè)置TIF_MEMDIE
這里的兩個goto語句負責處理此種情況下,內(nèi)存分配失敗的情況:
- 設(shè)置了__GFP_NOMEMALLOC。該標志禁止使用緊急分配鏈表(如果忽略水印,這可能是最佳途徑),因此無法在禁用水印的情況下調(diào)用get_page_from_freelist。跳轉(zhuǎn)到nopage處,通過內(nèi)核消息將失敗報告給用戶,并將NULL指針返回調(diào)用者
- 在忽略水印的情況下,get_page_from_freelist仍然失敗了,這種情況下會放棄搜索,報告錯誤消息。如果設(shè)置了__GFP_NOFAIL,內(nèi)核會進入無限循環(huán)(跳轉(zhuǎn)到第4行的標號nofail_alloc),重復(fù)本段內(nèi)容。
-
如果上述3種情況都沒有成功分配內(nèi)存,內(nèi)核會進行一些耗時的操作。。前提是分配掩碼中設(shè)置了__GFP_WAIT標志,因為隨后的操作可能使進程睡眠(為了使得kswapd取得一些進展)。
if (!wait) goto nopage; cond_schedule(); ...
如果wait標志沒有被設(shè)置,這里會放棄分配。如果設(shè)置了,內(nèi)核通過cond_resched?供了重調(diào)度的時機。這防止了花費過多時間搜索內(nèi)存,以致于使其他進程處于饑餓狀態(tài)。
分頁機制提供了一個目前尚未使用的選項,將很少使用的頁換出到塊介質(zhì),以便在物理內(nèi)存中產(chǎn)生更多空間。但該選項非常耗時,還可能導(dǎo)致進程睡眠狀態(tài)。try_to_free_pages是相應(yīng)的輔助函數(shù),用于查找當前不急需的頁,以便換出。
/* 我們現(xiàn)在進入同步回收狀態(tài) */ p->flags |= PF_MEMALLOC; ... did_some_progress = try_to_free_pages(zonelist->zones, order, gfp_mask); ... p->flags &= ~PF_MEMALLOC; cond_resched(); ...
該調(diào)用被設(shè)置/清除PF_MEMALLOC標志的代碼間隔起來。try_to_free_pages自身可能也需要分配新的內(nèi)存。由于為獲得新內(nèi)存還需要額外分配一點內(nèi)存(相當矛盾的情形),該進程當然應(yīng)該在內(nèi)存管理方面享有最高優(yōu)先級,上述標志的設(shè)置即達到了這一目的。try_to_free_pages會返回增加的空閑頁數(shù)目。
接下來,如果try_to_free_pages釋放了一些頁,那么內(nèi)核再次調(diào)用get_page_from_freelist嘗試分配內(nèi)存:
if (likely(did_some_progress)) { page = get_page_from_freelist(gfp_mask, order,zonelist, alloc_flags); if (page) goto got_pg; } else if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) { ...
如果內(nèi)核可能執(zhí)行影響VFS層的調(diào)用而又沒有設(shè)置GFP_NORETRY,那么調(diào)用OOM killer:
/* OOM killer無助于高階分配,因此失敗 */ if (order > PAGE_ALLOC_COSTLY_ORDER) { clear_zonelist_oom(zonelist); goto nopage; } out_of_memory(zonelist, gfp_mask, order); goto restart; }
out_of_memory函數(shù)函數(shù)選擇一個內(nèi)核認為犯有分配過多內(nèi)存“罪行”的進程,并殺死該進程。這有很大幾率騰出較多的空閑頁,然后跳轉(zhuǎn)到標號restart,重試分配內(nèi)存的操作。但殺死一個進程未必立即出現(xiàn)多于\(2^{PAGE_COSTLY_ORDER}\)頁的連續(xù)內(nèi)存區(qū)(其中PAGE_COSTLY_ORDER_PAGES通常設(shè)置為3),因此如果當前要分配如此大的內(nèi)存區(qū),那么內(nèi)核會饒恕所選擇的進程,不執(zhí)行殺死進程的任務(wù),而是承認失敗并跳轉(zhuǎn)到nopage。
如果設(shè)置了__GFP_NORETRY,或內(nèi)核不允許使用可能影響VFS層的操作,會判斷所需分配的長度,作出不同的決定:文章來源:http://www.zghlxwxcb.cn/news/detail-692665.html
... do_retry = 0; if (!(gfp_mask & __GFP_NORETRY)) { if ((order <= PAGE_ALLOC_COSTLY_ORDER) ||(gfp_mask & __GFP_REPEAT)) do_retry = 1; if (gfp_mask & __GFP_NOFAIL) do_retry = 1; } if (do_retry) { congestion_wait(WRITE, HZ/50); goto rebalance; } nopage: if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit()) { printk(KERN_WARNING "%s: page allocation failure."" order:%d, mode:0x%x\n"p->comm, order, gfp_mask); dump_stack(); show_mem(); } got_pg: return page; }
- 如果分配長度小于\(2^{PAGE_ALLOC_COSTLY_ORDER}\)=8頁,或設(shè)置了__GFP_REPEAT標志,則內(nèi)核進入無限循環(huán)。在這兩種情況下,是不能設(shè)置GFP_NORETRY的。因為如果調(diào)用者不打算重試,那么進入無限循環(huán)重試并沒有意義。內(nèi)核會跳轉(zhuǎn)回rebalance標號,即 的入口,并一直等待,直至找到適當大小的內(nèi)存塊——根據(jù)所要分配的內(nèi)存大小,內(nèi)核可以假定該無限循環(huán)不會持續(xù)太長時間。內(nèi)核在跳轉(zhuǎn)之前會調(diào)用congestion_wait,等待塊設(shè)備層隊列釋放,這樣內(nèi)核就有機會換出頁。
- 在所要求的分配階大于3但設(shè)置了__GFP_NOFAIL標志的情況下,內(nèi)核也會進入上述無限循環(huán),因為該標志無論如何都不允許失敗。
- 如果情況不是這樣,內(nèi)核只能放棄,并向用戶返回NULL指針,并輸出一條內(nèi)存請求無法滿足的警告消息。
總結(jié)
本節(jié)主要總結(jié)了伙伴系統(tǒng)中__alloc_pages
方法的主要流程,由于后續(xù)內(nèi)容過多,這里會分為多個小結(jié)總結(jié)。文章來源地址http://www.zghlxwxcb.cn/news/detail-692665.html
到了這里,關(guān)于深入理解Linux內(nèi)核——內(nèi)存管理(4)——伙伴系統(tǒng)(1)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!