linux 頁框管理(四) 管理區分配器

kuraxii發表於2024-04-14

管理區分配器(The Zone Allocator)

相關函式:

  • alloc_pages(gfp_mask, order)
  • struct page * fastcall__alloc_pages(unsigned int gfp_mask, unsigned int order, struct zonelist *zonelist)

管理區分配器(alloc_pages)是核心分配器的前端(前面講到的夥伴系統,每cpu頁幀快取等等不能直接呼叫,透過管理區分配器來呼叫)。該部件必須分配一個足夠大的空閒頁塊以滿足記憶體請求。這個任務並不是看起來都的那麼簡單,因為記憶體管理區分配器必須滿足幾個目標:

  • 它應該保護保留的頁
  • 當記憶體不足且允許阻塞當前程序時,它應當觸發頁框回收演算法;一旦某些頁框被釋放,管理區分配器將再次嘗試分配。
  • 如果可能,他應該儲存小而珍貴的ZOME_DMA記憶體管理區

從前面的章節(分割槽頁框分配器 The Zoned Page Frame Allocator)我們知道,對一組連續頁幀的請求實際上是透過alloc_pages宏來處理的。緊接著,這個宏又呼叫__alloc_pages函式,這是管理區分配的核心。在linux原始碼的註釋裡, This is the 'heart' of the zoned buddy allocator 這是分割槽夥伴分配器的“核心”。它接收三個引數:

型別 名稱 描述
unsigned int gpf_mask 指定了記憶體請求的標誌
unsigned int order 要分配的連續頁框的大小的對數
struct zonelist* zonelist 指向zonelist結構體的指標,結構體存放struct zone指標陣列,按優先順序描述了適於分配的記憶體管理區
struct zonelist {
	struct zone *zones[MAX_NUMNODES * MAX_NR_ZONES + 1]; // NULL delimited
};

__alloc_pages 函式掃描zone_list中的所有記憶體管理區,其程式碼如下:

    for(i = 0; (z = zone_list[i]) != NULL; i++){
        if(zone_watermark_ok(z, order, ...)){
            page = buffered_rmqueue(z, order, gpf_mask);
            if(page)
                return page;
        }
    }

對於每個記憶體管理區,該函式將空閒頁框數與一個閾值比較,這個閾值取決於記憶體申請標誌,當前程序型別以及管理區被函式被檢查的次數。實際上,如果空閒記憶體不足,那麼每個記憶體管理區都會被檢查幾次,每次都會在請求最小空閒頁框的基礎上比上次更小
因此,前面的一段程式碼在__alloc_pages出現了幾次,但是變化都很小

buffered_rmqueue 函式在前面的已經詳細解析過了,它返回頁塊的首個頁框的頁描述符,如果分配失敗,記憶體管理區沒有足夠大小的空閒頁框塊,就返回NULL

zone_watermark_ok輔助函式接收幾個引數,它們決定記憶體管理區的閾值min
特別的,如果滿足下列條件則返回1

  1. 除了被分配的頁框,在管理區中至少還有min個空閒頁框,
  2. 除了被分配的頁框外,這裡在order至少為k的塊中起碼還有min/(2^k)個空閒框,其中,對於每個k,取值在1和分配的order之間。因此對於order > 0,那麼在大小至少為2的塊中的塊起碼還有min/2個空閒頁框;如果order > 1,那麼在大小至少為4的塊中起碼還有min/4個空閒頁框。以此類推

zone_watermark_ok的函式實現在mm/page_alloc.c line664

函式宣告

int zone_watermark_ok(struct zone *z, int order, unsigned long mark,
		      int classzone_idx, int can_try_harder, int gfp_high)
型別 名稱 描述
struct zone* z 記憶體管理區資料結構陣列
unsigned int order 要分配的連續頁框的大小的對數
unsigned long mark 當前閾值,見下面第2點
int classzone_idx 當前的記憶體管理區下標
int can_try_harder 標誌位 見下面第3點
int gfp_high 標誌位,是否位高階地址

min的值在zone_watermark_ok函式中確定:具體如下

  1. 作為min的基本值,在函式傳遞可以是 page_highpage_lowpage_min中的任意一個。
        long min = mark, free_pages = z->free_pages - (1 << order) + 1;
    
  2. 如果作為引數傳遞的gfp_high被置位,那麼min的值/2。通常如果gfp_mask中的__GFP_WAIT標誌被置位,則這個標誌就被置1gfp_high代表更加激進的分配方式,不用考慮頁框的剩餘量
     if (gfp_high)
     	min -= min / 2;
    
  3. 如果作為引數傳遞的can_try_harder被置位,那麼min的值將再減少1/4。如果gfp_mask中的__GFP_WAIT標誌被置位,或者當前程序是個實時程序並且記憶體分配是在程序在上下文中(中斷程式和可延時函式除外)完成的,則can_try_harder被置1
     if (can_try_harder)
     	min -= min / 4;
    

函式的判定策略 暫時留個坑
總之 這個函式主要是用來檢查在特定的記憶體區中是否有足夠的空閒記憶體來保證系統的健康執行和避免記憶體過度分配的風險

    if (free_pages <= min + z->lowmem_reserve[classzone_idx])
		return 0;
	for (o = 0; o < order; o++) {
		/* At the next order, this order's pages become unavailable */
		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;

__alloc_pages的函式實現在mm/page_alloc.c line964
執行步驟如下:

  1. 執行對記憶體管理區的第一次掃描,在第一次掃描中,閾值被設定為pages_low,如果分配到了滿足條件的pages,就跳轉到got_pg 函式返回
        /* Go through the zonelist once, looking for a zone with enough free */
        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)
                goto got_pg;
        }
    /* ... */    
    got_pg:
        // 一個統計分析函式,用於記憶體管理的最佳化,不再深究 
        zone_statistics(zonelist, z);
        return pages;
    
  2. 如果在第一次沒有成功的分配到頁框,則喚醒kswapd核心執行緒,來非同步的回收頁框
    for (i = 0; (z = zones[i]) != NULL; i++)
        wakeup_kswapd(z, order);
    
  3. 執行對管理區的第二次掃描, 這次將can_try_harder1,和引入__GFP_HIGH以更加激進的分配標誌。降低了基礎閾值。
    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;
    }
    /* ... */    
    got_pg:
        // 一個統計分析函式,用於記憶體管理的最佳化,不再深究 
        zone_statistics(zonelist, z);
        return pages;
    
  4. 如果函式在上一步還是沒有終止,那麼表明確實是系統記憶體不足。如果發出記憶體請求的核心控制路徑不在中斷上下文並且正在進行記憶體頁框回收,就進行第三次掃描
    這次掃描忽略了閾值min唯有這種情況下才允許核心控制路徑耗盡為頁框不足預留的頁(由管理區的lowmem_reserve欄位指定)。如果還是沒有分配到頁框,則返回NULL提示呼叫者發生了錯誤,沒有分配到頁框。實際上,這種情況的記憶體申請目的不是為了獲得更多記憶體來使用,而是為了透過某種方式(可能是記憶體壓縮、回收不再使用的頁面等)來釋放佔用的記憶體頁框架。
    /* This allocation should allow future memory freeing. */
    if (((p->flags & PF_MEMALLOC) || unlikely(test_thread_flag(TIF_MEMDIE))) && !in_interrupt()) {
        /* go through the zonelist yet again, ignoring mins */
        for (i = 0; (z = zones[i]) != NULL; i++) {
            page = buffered_rmqueue(z, order, gfp_mask);
            if (page)
                goto got_pg;
        }
        goto nopage;
    }
    /* ... */    
    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();
    }
    
  5. 在這裡,正在呼叫的核心控制路徑並不是為了回收記憶體,如果__GFP_WAIT標誌位沒有被置位,就返回NULL提示該核心控制路徑記憶體分配失敗。這種情況下,如果不阻塞就無法滿足分配需求
    /* Atomic allocations - we can't balance anything */
    if (!wait)
    	goto nopage;
    /* ... */    
    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;
    
  6. 在這裡,當前程序可以被阻塞。呼叫cond_resched檢查其他程序是否需要cpu
        cond_resched();
    
  7. 設定PF_MEMALLOC標誌表示程序已經準備好做記憶體回收
    /* We now go into synchronous reclaim */
    p->flags |= PF_MEMALLOC;
    
  8. reclaim_state中的reclaimed_slab標誌設定為0,這個結構體只有這一個欄位。(在後面slab分配器章節將會看到如何使用它)。之後函式可能可能會阻塞這個程序
    reclaim_state.reclaimed_slab = 0;
    p->reclaim_state = &reclaim_state;
    
  9. 呼叫try_to_free_pages嘗試去尋找一些頁來回收,(在17章,記憶體緊缺回收在分析)。一旦函式返回,就清空之前設定的標誌位,並再次呼叫cond_resched
    did_some_progress = try_to_free_pages(zones, gfp_mask, order);
    p->reclaim_state = NULL;
    p->flags &= ~PF_MEMALLOC;
    
  10. 如果上一步已經回收了一些頁框,那麼接下來還會執行第三步相同的記憶體管理區掃描
    if (likely(did_some_progress)) {
    	/*
    	 * Go through the zonelist yet one more time, keep
    	 * very high watermark here, this is only to catch
    	 * a parallel oom killing, we must fail if we're still
    	 * under heavy pressure.
    	 */
    	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;
    	}
    }
    
  11. 如果在第9步沒有釋放任何頁框,說明核心遇到了很大的麻煩,應為頁框已經少到了危險的底部,並且無法在回收到任何頁框。也許到了做出重大決策的時候了。如果核心控制路徑允許依賴檔案系統的操作來殺死一個程序,並且__GFP_NORETRY標誌被置0,那麼執行以下步驟
    1. 使用z->pages_high的閾值再次掃描記憶體管理區,嘗試最後的分配,如果還是不行就進行下一步
    2. 呼叫out_of_memory殺死一個程序來一些釋放頁框。
    3. 跳回到第一步,再次進行頁框分配操作
      因為第1步的閾值要遠高於之前設定的閾值,所以這個步驟很容易失敗。實際上,只有當另一個核心控制路徑殺死一個程序來回收了它的頁框後,第1步才有可能執行成功。
      因此第1步避免了兩個程序被無辜的殺死。
    else if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) {
    	/*
    	 * Go through the zonelist yet one more time, keep
    	 * very high watermark here, this is only to catch
    	 * a parallel oom killing, we must fail if we're still
    	 * under heavy pressure.
    	 */
    	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;
    }
    
  12. 如果在1011 步,都沒有滿足的條件,並且__GFP_NORETRY被置0,並且__GFP_NOFAIL__GFP_REPEAT被置位)就呼叫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;
    }
    

相關文章