分配頁幀
分配頁幀的具體實現
釋放頁幀
分配頁幀
內核中使用ZONE分配器滿足內存分配請求。該分配器必須具有足夠的空閑頁幀,以便滿足各種內存大小請求。為此,ZONE分配器必須能夠:
它應該保護預留頁幀池;
當內存不足并且允許阻塞當前進程時,能夠觸發頁幀回收機制。一旦某些頁幀被釋放,ZONE分配器重新分配;
盡可能保留小的、珍貴的ZONE_DMA內存區。如果請求正常內存或高端內存,ZONE分配器不太可能分配ZONE_DMA內存區中的頁幀。
對于每次連續頁幀的申請,ZONE頁幀分配器調用alloc_pages()宏實現。該宏其實是__alloc_pages()的封裝,而該函數才是ZONE分配器的核心。它需要三個參數:
gfp_mask
內存分配請求中指定的標志。
order
連續物理頁幀的對數。
zonelist
指向zonelist數據結構,按照優先順序,選擇適合內存分配的內存區。
__alloc_pages()掃描zonelist數據結構中每一個內存區,代碼大概如下所示:
for(i=0;(z=zonelist->zones[i])!=NULL;i++){ if(zone_watermark_ok(z,order,...)){ page=buffered_rmqueue(z,order,gfp_mask); if(page) returnpage; } }
對于每個內存區域,該函數將空閑頁幀的數量與一個閾值進行比較,該閾值取決于內存分配標志、當前進程的類型以及該函數已經檢查該區域的次數。實際上,如果可用內存很少,通常會對每個內存區域掃描幾次,每次都對分配所需的最小可用內存設置較低的閾值。因此,前面的代碼塊在__alloc_pages()函數的主體中被復用了幾次(只有很小的變化)。buffered_rmqueue()函數已經在前面的“CPU頁幀緩存”一節中描述過了:它返回第一個分配的頁幀的頁描述符,如果內存區域不包含一組請求大小的連續頁幀,則返回NULL。
zone_watermark_ok()輔助函數接收幾個參數,這些參數決定內存ZONE中可用頁幀數量的閾值min。特別是,如果滿足以下兩個條件,該函數返回值1,也就是具有足夠的內存:
/* *如果空閑頁幀在閾值之上,則返回1.考慮分配的大小(order密數決定) */ intzone_watermark_ok(structzone*z,intorder,unsignedlongmark, intclasszone_idx,intcan_try_harder,intgfp_high) { /*free_pages可能會變成負值,但是沒有關系*/ longmin=mark,free_pages=z->free_pages-(1<lowmem_reserve[classzone_idx]) return0; /*除了要分配的頁幀, *在`1`到`order`之間的空閑頁幀列表中的每一個`k`, *至少有`min/(2^k)`個空閑頁幀。 *因此,如果`order`大于0,在大小為`2`的內存塊列表中, *至少有`min/2`個空閑頁幀; *如果`order`大于0,在大小為`4`的內存塊列表中, *至少有`min/4`個空閑頁幀;以此類推。 */ for(o=0;ofree_area[o].nr_free<>=1; if(free_pages<=?min) ????????????return?0; ????} ????return?1;
閾值min的值由zone_watermark_ok()確定,如下所示:
可以將pages_min,pages_low和pages_high三個內存ZONE區之一作為基本值作為函數的參數(參見本章前面的“預留頁幀池”一節)。
如果設置了gfp_high標志,則將基值除以2。通常,如果在gfp_mask中設置了__GFP_HIGHMEM標志,也就是說,如果可以從高端內存中分配頁幀的話,則該標志等于1。
如果設置了can_try_harder標志,則閾值將進一步減少四分之一。如果在gfp_mask中設置了__GFP_WAIT標志,或者當前進程是實時進程,并且內存分配是在進程上下文中完成的(在中斷處理程序和可延遲函數之外),則該標志通常等于1。
分配頁幀的具體實現
__alloc_pages()函數主要執行以下步驟:
structpage*fastcall __alloc_pages(unsignedintgfp_mask,unsignedintorder, structzonelist*zonelist) { //...省略 /*如果調用方不能運行直接回收算法, *或者調用方具有實時調度策略, *則調用方可能會更多地使用預留頁幀 */ can_try_harder=(unlikely(rt_task(p))&&!in_interrupt())||!wait; zones=zonelist->zones;/*內存ZONE列表*/ if(unlikely(zones[0]==NULL)){ returnNULL;/*這應該發生嗎?*/ } classzone_idx=zone_idx(zones[0]); restart: /* 1. 執行內存區域的第一次掃描。 *在第一次掃描中,min閾值設置為z->pages_low, *其中z指向正在分析的zone描述符 *(can_try_harder和gfp_high參數設置為零)。 */ for(i=0;(z=zones[i])!=NULL;i++){ if(!zone_watermark_ok(z,order,z->pages_low, classzone_idx,0,0)) continue; page=buffered_rmqueue(z,order,gfp_mask); if(page) gotogot_pg; } /* 2. 如果在前一步中沒有終止,那么剩余的空閑內存就不多了; *應該喚醒kswapd內核線程,開始異步回收頁幀。 */ for(i=0;(z=zones[i])!=NULL;i++) wakeup_kswapd(z,order); /* 3. 對內存區域執行第二次掃描: *將值z->pages_min作為基本閾值傳遞。 *實際閾值還與can_try_harder和gfp_high標志有關。 *(允許內核和實時任務訪問預留頁幀池) *這一步幾乎與步驟1相同,只是函數使用了較低的閾值。 */ 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) gotogot_pg; } /* 4. 執行第三次內存區域掃描: *如果前面沒有分配到內存頁幀,則說明系統內存應該非常低了。 *如果內核代碼不是中斷處理程序或可延遲函數, *且它正在嘗試回收頁幀(設置了PF_MEMALLOC或PF_MEMDIE標志)。 *此時應該進行第3次掃描。 *此時應該忽略低內存閾值,即不調用zone_watermark_ok()。 *這應該是耗盡低內存預留頁幀的唯一情況 *(這些頁幀由zone描述符的lowmem_reserve字段指定)。 *在這種情況下,發送內存請求的內核代碼最終通過嘗試釋放頁幀, *獲得它想要的內存請求。 *如果沒有內存ZONE包含足夠的頁幀, *則函數返回NULL,并通知調用者分配失敗。 */ if(((p->flags&PF_MEMALLOC)|| unlikely(test_thread_flag(TIF_MEMDIE)))&& !in_interrupt()){ /*再一次遍歷zonelist,忽略min*/ for(i=0;(z=zones[i])!=NULL;i++){ page=buffered_rmqueue(z,order,gfp_mask); if(page) gotogot_pg; } gotonopage; } /*5.原子分配-這種情況我們不能做任何均衡處理 *這種情況下,該函數返回NULL以通知內核代碼內存分配失敗: *這種情況下,沒有辦法在不阻塞當前進程的情況下滿足請求。 */ if(!wait) gotonopage; rebalance: /*6.在這里,當前進程可以被阻塞: *調用cond_resched()來檢查其他進程是否需要CPU。 */ cond_resched(); /*7.設置當前的PF_MEMALLOC標志, *表示進程已準備好執行異步內存回收。 */ p->flags|=PF_MEMALLOC; /*8.reclaim_state只包含一個字段reclaimed_slab,初始化為0*/ reclaim_state.reclaimed_slab=0; p->reclaim_state=&reclaim_state; /* 9. 尋找一些要回收的頁幀。 *該函數可能會阻塞當前進程。 *一旦該函數返回,重置當前的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)){ /*10.說明前一步釋放了一些頁幀, *那么該函數將執行與步驟3中相同的另一次內存區域掃描。 *如果內存分配請求不能被滿足, * zone_watermark_ok函數決定是否應該繼續掃描內存區域。 *這兒使用高閾值,僅是為了捕獲并行的oom kill; *(也就是說,如果內存壓力還是很大,則應該失敗) */ 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) gotogot_pg; } } /*11.如果在步驟9中沒有釋放頁幀,那么內核就有大麻煩了, *因為可用內存非常低,無法回收任何頁幀。 *也許是時候做出一個關鍵的決定了: *如果此時設置了__GFP_FS標志,且清零了__GFP_NORETRY標志 *如果內核控制路徑允許執行與文件系統相關的操作來終止進程(gfp_mask中的'__GFP_FS'標志已設置),并且'__GFP_NORETRY'標志已清除,則執行以下子步驟: */ elseif((gfp_mask&__GFP_FS)&&!(gfp_mask&__GFP_NORETRY)){ /* 11.a zone_watermark_ok函數決定是否應該繼續掃描內存區域。 *這兒使用高閾值z->pages_high,僅是為了捕獲并行的oom kill; *(也就是說,如果內存壓力還是很大,則應該失敗) * *因為該步使用的閾值比之前的都高,所以大概率會失敗。 *實際上,只有當內核的其他代碼已經殺死了一個進程并回收內存后 *該步才能成功。但是,這一步避免了殺死兩個進程的情況。 */ 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) gotogot_pg; } /*11.b殺死一些進程,釋放內存*/ out_of_memory(gfp_mask); /*11.c跳轉回第1步*/ gotorestart; } /*如果__GFP_NORETRY標志是清除的,并且內存分配請求跨越最多8頁幀 *也就是說,盡量不要重復分配大于8個頁幀以上的內存。 *或者__GFP_REPEAT和__GFP_NOFAIL標志之一被設置, *函數調用blk_congestion_wait使進程休眠一段時間, *然后它跳回步驟6。 *否則,該函數返回NULL以通知調用者內存分配失敗。 */ 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; ????} ????if?(do_retry)?{ ????????blk_congestion_wait(WRITE,?HZ/50); ????????goto?rebalance; ????} nopage: ????if?(!(gfp_mask?&?__GFP_NOWARN)?&&?printk_ratelimit())?{ ????????//?...省略 ????} ????return?NULL; got_pg: ????zone_statistics(zonelist,?z); ????return?page; }
釋放頁幀
zone分配器還負責釋放頁幀,但要比分配頁幀簡單。
內核中,所有釋放頁幀的宏和函數,都是基于__free_pages()函數實現的。該函數的參數是page,待要釋放的第一個頁幀的頁描述符的地址;order,要釋放的連續頁幀組的對數大小。函數執行以下步驟:
檢查第1個頁幀是否真的屬于動態內存(它的PG_reserved標志被清除);如果不是,則終止。
減少page->_count使用計數器;如果仍然大于等于0,終止。
如果order等于零,該函數調用free_hot_page()將頁幀釋放到相應內存區域的CPU本地熱緩存中。
如果order大于0,它將頁幀添加到本地列表中,并調用free_pages_bulk()函數將它們釋放到適當內存區域的buddy系統中。
-
內核
+關注
關注
3文章
1362瀏覽量
40228 -
cpu
+關注
關注
68文章
10824瀏覽量
211133 -
Linux
+關注
關注
87文章
11225瀏覽量
208911 -
分配器
+關注
關注
0文章
193瀏覽量
25726
原文標題:Linux內核8.6-內存管理之ZONE內存分配器
文章出處:【微信號:嵌入式ARM和Linux,微信公眾號:嵌入式ARM和Linux】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論