Linux核心-記憶體-分割槽頁框分配器
阿新 • • 發佈:2019-02-07
簡介
分割槽頁框分配器處理對連續頁框組的記憶體分配請求,主要組成如下圖所示:
管理區分配器接受動態記憶體分配與釋放的請求,它首先從每CPU頁框快取記憶體中請求頁框,若無法滿足才從夥伴系統中請求分配。
原始碼分析(Linux2.6/mm/page_alloc.c)
buffered_rmqueue()
此函式從指定記憶體管理區中分配頁框,對應上圖中的分配機制。
static struct page *buffered_rmqueue(struct zone *zone, int order, int gfp_flags) { unsigned long
__alloc_pages()
__alloc_pages()函式是管理區分配器的核心,請求頁框的函式,比如alloc_page()是對__alloc_pages()的封裝,該函式比較長,但是不算難理解,耐心點看完吧。該函式接收3個引數:
- gfp_mask:在記憶體分配請求中指定的標誌
- order: 連續分配的頁框數量的對數(實際分配的是2^order個連續的頁框)
- zonelist: zonelist資料結構的指標。該結構按優先次序描述了適於記憶體分配的記憶體管理區。它實際上是指向zone陣列,存放本節點和其他節點的同類型管理區
程式碼如下:
struct page * fastcall __alloc_pages(unsigned int gfp_mask, unsigned int order, struct zonelist *zonelist) { const int wait = gfp_mask & __GFP_WAIT; struct zone **zones, *z; struct page *page; struct reclaim_state reclaim_state; struct task_struct *p = current; int i; int classzone_idx; int do_retry; int can_try_harder; int did_some_progress; might_sleep_if(wait); /** * if語句在彙編中的實現:如果條件滿足就執行下一條指令,如果不滿足則跳轉,unlikely是為了使程式順序執行而避免跳轉 * rt_task判斷是否為實時程序,如果是實時程序且不可中斷或者不可等待 * 那麼can_try_harder被置位,閾值將會再次減少(見下面zone_watermark_ok()函式) */ can_try_harder = (unlikely(rt_task(p)) && !in_interrupt()) || !wait; zones = zonelist->zones; if (unlikely(zones[0] == NULL)) { return NULL; } classzone_idx = zone_idx(zones[0]); restart: /** * 掃描包含在zonelist資料結構中的每個記憶體管理區 */ for (i = 0; (z = zones[i]) != NULL; i++) { /** *對於每個記憶體管理區,該函式將空閒頁框的個數與一個閥值進行比較 *該值取決於記憶體分配標誌、當前程序的型別及管理區被函式檢查的次數。 *實際上,如果空閒記憶體不足,那麼每個記憶體管理區一般會被檢查幾次。 *每一次在所請求的空閒記憶體最低量的基礎上使用更低的值進行掃描。 * 因此,這段迴圈程式碼會被複制幾次,而變化很小。 */ /** *zone_watermark_ok輔助函式接收幾個引數,它們決定記憶體管理區中空閒頁框個數的閥值min。 *這是對記憶體管理區的第一次掃描,在第一次掃描中,閥值設定為z->pages_low */ if (!zone_watermark_ok(z, order, z->pages_low, classzone_idx, 0, 0)) continue; page = buffered_rmqueue(z, order, gfp_mask); if (page) goto got_pg; } /** * 一般來說,應當在上一次掃描時得到記憶體。 * 執行到此,表示記憶體已經緊張了(沒有連續的頁框可供分配了) * 就喚醒kswapd核心執行緒來非同步的開始回收頁框。 */ for (i = 0; (z = zones[i]) != NULL; i++) wakeup_kswapd(z, order); /** * 執行對記憶體管理區的第二次掃描,將值z->pages_min作為閥值傳入。這個值已經在上一步的基礎上降低了。 * 當然,實際的min值還是要由can_try_harder和gfp_high確定。z->pages_min僅僅是一個參考值而已。 */ for (i = 0; (z = zones[i]) != NULL; i++) { if (!zone_watermark_ok(z, order, z->pages_min, classzone_idx, can_try_harder, gfp_mask & __GFP_HIGH)) continue; page = buffered_rmqueue(z, order, gfp_mask); if (page) goto got_pg; } /** * 上一步都還沒有獲得記憶體,系統記憶體肯定是不足了。 */ /** * 如果產生記憶體分配的核心控制路徑不是一箇中斷處理程式或者可延遲函式, * 並且它試圖回收頁框(PF_MEMALLOC,TIF_MEMDIE標誌被置位),那麼才對記憶體管理區進行第三次掃描。 */ if (((p->flags & PF_MEMALLOC) || unlikely(test_thread_flag(TIF_MEMDIE))) && !in_interrupt()) { for (i = 0; (z = zones[i]) != NULL; i++) { /** *本次掃描就不呼叫zone_watermark_ok,它忽略閥值,這樣才能從預留的頁中分配頁。 *允許這樣做,因為是這個程序想要歸還頁框,那就暫借一點給它吧(捨不得孩子套不到狼)。 */ page = buffered_rmqueue(z, order, gfp_mask); if (page) goto got_pg; } /** * 執行到這裡來,實在是沒有記憶體了。 *不論是高階記憶體區還是普通記憶體區、還是DMA記憶體區,甚至這些管理區中保留的記憶體都沒有了。 * 意味著我們的家底都完了。 */ goto nopage; } /** * 如果gfp_mask的__GFP_WAIT標誌沒有被置位,函式就返回NULL。 */ if (!wait) goto nopage; rebalance: /** * 如果當前程序能夠被阻塞,呼叫cond_resched檢查是否有其他程序需要CPU */ cond_resched(); /* * 設定PF_MEMALLOC標誌來表示程序已經準備好執行記憶體回收。 */ p->flags |= PF_MEMALLOC; reclaim_state.reclaimed_slab = 0; /** * 將reclaim_state資料結構指標存入reclaim_state。這個結構只包含一個欄位reclaimed_slab,初始值為0 */ p->reclaim_state = &reclaim_state; /** * 呼叫try_to_free_pages尋找一些頁框來回收。 * 這個函式可能會阻塞當前程序。一旦返回,就重設PF_MEMALLOC,並再次呼叫cond_resched */ did_some_progress = try_to_free_pages(zones, gfp_mask, order); p->reclaim_state = NULL; p->flags &= ~PF_MEMALLOC; cond_resched(); /** * 如果已經回收了一些頁框,那麼執行第二遍掃描類似的操作。 */ if (likely(did_some_progress)) { for (i = 0; (z = zones[i]) != NULL; i++) { if (!zone_watermark_ok(z, order, z->pages_min, classzone_idx, can_try_harder, gfp_mask & __GFP_HIGH)) continue; page = buffered_rmqueue(z, order, gfp_mask); if (page) goto got_pg; } } else if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) { /** *沒有釋放任何頁框,說明核心遇到很大麻煩了。因為記憶體少又不能釋放頁框。 *如果允許殺死程序:__GFP_FS被置位並且__GFP_NORETRY標誌為0。 * 那就開始準備殺死程序吧。 */ /** * 再掃描一次記憶體管理區。 *這樣做有點莫名其妙,既然申請少一點的記憶體都不行,為什麼還要傳入z->pages_high??它看起來更不會成功。 *其實這樣做還是有道理的:實際上,只有另一個核心控制路徑已經殺死一個程序來回收它的記憶體後,這步才會成功。 * 因此,這步避免了兩個(而不是一個)無辜的程序被殺死。 */ for (i = 0; (z = zones[i]) != NULL; i++) { if (!zone_watermark_ok(z, order, z->pages_high, classzone_idx, 0, 0)) continue; page = buffered_rmqueue(z, order, gfp_mask); if (page) goto got_pg; } /** * 還是不行,就殺死一些程序再試吧。 */ out_of_memory(gfp_mask); goto restart; } /** * 如果記憶體分配請求不能被滿足,那麼函式決定是否應當繼續掃描記憶體管理區。 * 如果__GFP_NORETRY被清除,並且記憶體分配請求跨越了多達8個頁框或者__GFP_REPEAT被置位,或者__GFP_NOFAIL被置位。 */ do_retry = 0; if (!(gfp_mask & __GFP_NORETRY)) { if ((order <= 3) || (gfp_mask & __GFP_REPEAT)) do_retry = 1; if (gfp_mask & __GFP_NOFAIL) do_retry = 1; } /** * 要重試,就呼叫blk_congestion_wait使程序休眠一會。再跳到rebalance重試。 */ if (do_retry) { blk_congestion_wait(WRITE, HZ/50); goto rebalance; } /** * 既然不用重試,那就執行到nopage返回NULL了。 */ 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(); } return NULL; got_pg: zone_statistics(zonelist, z); return page; }
zone_watermark_ok()函式如下:
int zone_watermark_ok(struct zone *z, int order, unsigned long mark,int classzone_idx, int can_try_harder, int gfp_high) { long min = mark, free_pages = z->free_pages - (1 << order) + 1; int o; /** * 如果gfp_high標誌被置位。則base除2。 * 注意這裡不是:min /= 2(min為奇偶數的差別) * 一般來說,如果gfp_mask的__GFP_WAIT標誌被置位,那麼這個標誌就會為1 * 換句話說,就是指從高階記憶體中分配。 */ if (gfp_high) min -= min / 2; /** * 如果作為引數傳遞的can_try_harder標誌被置位,這個值再減少1/4 * can_try_harder=1一般是當:gfp_mask中的__GFP_WAIT標誌被置位,或者當前程序是一個實時程序並且在程序上下文中已經完成了記憶體分配。 */ if (can_try_harder) min -= min / 4; if (free_pages <= min + z->lowmem_reserve[classzone_idx]) return 0; /* * for函式作用:除了被分配的頁框外,在order至少為k的塊中起碼還有min/2^k個空閒頁框 */ for (o = 0; o < order; o++) { free_pages -= z->free_area[o].nr_free << o; /* Require fewer higher order pages to be free */ min >>= 1; if (free_pages <= min) return 0; } return 1; }