伙伴系統之伙伴系統概述--Linux記憶體管理(十五)

来源:https://www.cnblogs.com/linhaostudy/archive/2018/12/16/10089120.html
-Advertisement-
Play Games

在內核初始化完成之後, 記憶體管理的責任就由伙伴系統來承擔. 伙伴系統基於一種相對簡單然而令人吃驚的強大演算法. Linux內核使用二進位伙伴演算法來管理和分配物理記憶體頁面, 該演算法由Knowlton設計, 後來Knuth又進行了更深刻的描述. 伙伴系統是一個結合了2的方冪個分配器和空閑緩衝區合併計技術的 ...


在內核初始化完成之後, 記憶體管理的責任就由伙伴系統來承擔. 伙伴系統基於一種相對簡單然而令人吃驚的強大演算法.

Linux內核使用二進位伙伴演算法來管理和分配物理記憶體頁面, 該演算法由Knowlton設計, 後來Knuth又進行了更深刻的描述.

伙伴系統是一個結合了2的方冪個分配器和空閑緩衝區合併計技術的記憶體分配方案, 其基本思想很簡單. 記憶體被分成含有很多頁面的大塊, 每一塊都是2個頁面大小的方冪. 如果找不到想要的塊, 一個大塊會被分成兩部分, 這兩部分彼此就成為伙伴. 其中一半被用來分配, 而另一半則空閑. 這些塊在以後分配的過程中會繼續被二分直至產生一個所需大小的塊. 當一個塊被最終釋放時, 其伙伴將被檢測出來, 如果伙伴也空閑則合併兩者.

  • 內核如何記住哪些記憶體塊是空閑的
  • 分配空閑頁面的方法
  • 影響分配器行為的眾多標識位
  • 記憶體碎片的問題和分配器如何處理碎片

2 伙伴系統的結構

2.1 伙伴系統數據結構

系統記憶體中的每個物理記憶體頁(頁幀),都對應於一個struct page實例, 每個記憶體域都關聯了一個struct zone的實例,其中保存了用於管理伙伴數據的主要數數組

//  http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L324
struct zone
{
     /* free areas of different sizes */
    struct free_area        free_area[MAX_ORDER];
};

struct free_area是一個伙伴系統的輔助數據結構, 它定義在include/linux/mmzone.h?v=4.7, line 88

struct free_area {
    struct list_head        free_list[MIGRATE_TYPES];
    unsigned long           nr_free;
};
欄位 描述
free_list 是用於連接空閑頁的鏈表. 頁鏈表包含大小相同的連續記憶體區
nr_free 指定了當前記憶體區中空閑頁塊的數目(對0階記憶體區逐頁計算,對1階記憶體區計算頁對的數目,對2階記憶體區計算4頁集合的數目,依次類推

伙伴系統的分配器維護空閑頁面所組成的塊, 這裡每一塊都是2的方冪個頁面, 方冪的指數稱為階.

階是伙伴系統中一個非常重要的術語. 它描述了記憶體分配的數量單位. 記憶體塊的長度是2^0,order , 其中order的範圍從0到MAX_ORDER

zone->free_area[MAX_ORDER]數組中階作為各個元素的索引, 用於指定對應鏈表中的連續記憶體區包含多少個頁幀.

  • 數組中第0個元素的階為0, 它的free_list鏈表域指向具有包含區為單頁(2^0 = 1)的記憶體頁面鏈表
  • 數組中第1個元素的free_list域管理的記憶體區為兩頁(2^1 = 2)
  • 第3個管理的記憶體區為4頁, 依次類推.
  • 直到 2^MAXORDER-1個頁面大小的塊

2.2 最大階MAX_ORDER與FORCE_MAX_ZONEORDER配置選項

一般來說MAX_ORDER預設定義為11, 這意味著一次分配可以請求的頁數最大是2^11=2048, 參見include/linux/mmzone.h?v=4.7, line 22

/* Free memory management - zoned buddy allocator.  */
#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
#else
#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER
#endif
#define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER - 1))

但如果特定於體繫結構的代碼設置了FORCE_MAX_ZONEORDER配置選項, 該值也可以手工改變

例如,IA-64系統上巨大的地址空間可以處理MAX_ORDER = 18的情形,而ARM或v850系統則使用更小的值(如8或9). 但這不一定是由電腦支持的記憶體數量比較小引起的,也可能是記憶體對齊方式的要求所導致

可以參考一些架構的Kconfig文件如下

arm arm64
arch/arm/Kconfig?v=4.7, line 1696 arch/arm64/Kconfig?v=4.7, line 679

比如arm64體繫結構的Kconfig配置文件的描述

config FORCE_MAX_ZONEORDER
int
default "14" if (ARM64_64K_PAGES && TRANSPARENT_HUGEPAGE)
default "12" if (ARM64_16K_PAGES && TRANSPARENT_HUGEPAGE)
default "11"

2.3 記憶體區是如何連接的

記憶體區中第1頁內的鏈表元素, 可用於將記憶體區維持在鏈表中。因此,也不必引入新的數據結構來管理物理上連續的頁,否則這些頁不可能在同一記憶體區中. 如下圖所示

伙伴不必是彼此連接的. 如果一個記憶體區在分配其間分解為兩半, 內核會自動將未用的一半加入到對應的鏈表中.

如果在未來的某個時刻, 由於記憶體釋放的緣故, 兩個記憶體區都處於空閑狀態, 可通過其地址判斷其是否為伙伴. 管理工作較少, 是伙伴系統的一個主要優點.

基於伙伴系統的記憶體管理專註於某個結點的某個記憶體域, 例如, DMA或高端記憶體域. 但所有記憶體域和結點的伙伴系統都通過備用分配列表連接起來.

下圖說明瞭這種關係.

最後要註意, 有關伙伴系統和當前狀態的信息可以在/proc/buddyinfo中獲取

內核中很多時候要求分配連續頁. 為快速檢測記憶體中的連續區域, 內核採用了一種古老而歷經檢驗的技術: 伙伴系統

系統中的空閑記憶體塊總是兩兩分組, 每組中的兩個記憶體塊稱作伙伴. 伙伴的分配可以是彼此獨立的. 但如果兩個伙伴都是空閑的, 內核會將其合併為一個更大的記憶體塊, 作為下一層次上某個記憶體塊的伙伴.

下圖示範了該系統, 圖中給出了一對伙伴, 初始大小均為8頁. 即系統中所有的頁面都是8頁的.

內核對所有大小相同的伙伴(1、2、4、8、16或其他數目的頁),都放置到同一個列表中管理. 各有8頁的一對伙伴也在相應的列表中.

如果系統現在需要8個頁幀, 則將16個頁幀組成的塊拆分為兩個伙伴. 其中一塊用於滿足應用程式的請求, 而剩餘的8個頁幀則放置到對應8頁大小記憶體塊的列表中.

如果下一個請求只需要2個連續頁幀, 則由8頁組成的塊會分裂成2個伙伴, 每個包含4個頁幀. 其中一塊放置回伙伴列表中,而另一個再次分裂成2個伙伴, 每個包含2頁。其中一個回到伙伴系統,另一個則傳遞給應用程式.

在應用程式釋放記憶體時, 內核可以直接檢查地址, 來判斷是否能夠創建一組伙伴, 併合併為一個更大的記憶體塊放回到伙伴列表中, 這剛好是記憶體塊分裂的逆過程。這提高了較大記憶體塊可用的可能性.

在系統長期運行時,伺服器運行幾個星期乃至幾個月是很正常的,許多桌面系統也趨向於長期開機運行,那麼會發生稱為碎片的記憶體管理問題。頻繁的分配和釋放頁幀可能導致一種情況:系統中有若幹頁幀是空閑的,但卻散佈在物理地址空間的各處。換句話說,系統中缺乏連續頁幀組成的較大的記憶體塊,而從性能上考慮,卻又很需要使用較大的連續記憶體塊。通過伙伴系統可以在某種程度上減少這種效應,但無法完全消除。如果在大塊的連續記憶體中間剛好有一個頁幀分配出去,很顯然這兩塊空閑的記憶體是無法合併的.

在內核版本2.6.24之後, 增加了一些有效措施來防止記憶體碎片.

3 避免碎片

在第1章給出的簡化說明中, 一個雙鏈表即可滿足伙伴系統的所有需求. 在內核版本2.6.23之前, 的確是這樣. 但在內核2.6.24開發期間, 內核開發者對伙伴系統的爭論持續了相當長時間. 這是因為伙伴系統是內核最值得尊敬的一部分,對它的改動不會被大家輕易接受

3.1 記憶體碎片

伙伴系統的基本原理已經在第1章中討論過,其方案在最近幾年間確實工作得非常好。但在Linux記憶體管理方面,有一個長期存在的問題:在系統啟動並長期運行後,物理記憶體會產生很多碎片。該情形如下圖所示

假定記憶體由60頁組成,這顯然不是超級電腦,但用於示例卻足夠了。左側的地址空間中散佈著空閑頁。儘管大約25%的物理記憶體仍然未分配,但最大的連續空閑區只有一頁. 這對用戶空間應用程式沒有問題:其記憶體是通過頁表映射的,無論空閑頁在物理記憶體中的分佈如何,應用程式看到的記憶體 似乎總是連續的。右圖給出的情形中,空閑頁和使用頁的數目與左圖相同,但所有空閑頁都位於一個連續區中。

但對內核來說,碎片是一個問題. 由於(大多數)物理記憶體一致映射到地址空間的內核部分, 那麼在左圖的場景中, 無法映射比一頁更大的記憶體區. 儘管許多時候內核都分配的是比較小的記憶體, 但也有時候需要分配多於一頁的記憶體. 顯而易見, 在分配較大記憶體的情況下, 右圖中所有已分配頁和空閑頁都處於連續記憶體區的情形,是更為可取的.

很有趣的一點是, 在大部分記憶體仍然未分配時, 就也可能發生碎片問題. 考慮圖3-25的情形.

只分配了4頁,但可分配的最大連續區只有8頁,因為伙伴系統所能工作的分配範圍只能是2的冪次.

我提到記憶體碎片只涉及內核,這隻是部分正確的。大多數現代CPU都提供了使用巨型頁的可能性,比普通頁大得多。這對記憶體使用密集的應用程式有好處。在使用更大的頁時,地址轉換後備緩衝器只需處理較少的項,降低了TLB緩存失效的可能性。但分配巨型頁需要連續的空閑物理記憶體!

很長時間以來,物理記憶體的碎片確實是Linux的弱點之一。儘管已經提出了許多方法,但沒有哪個方法能夠既滿足Linux需要處理的各種類型工作負荷提出的苛刻需求,同時又對其他事務影響不大。

3.2 依據可移動性組織頁

在內核2.6.24開發期間,防止碎片的方法最終加入內核。在我討論具體策略之前,有一點需要澄清。

文件系統也有碎片,該領域的碎片問題主要通過碎片合併工具解決。它們分析文件系統,重新排序已分配存儲塊,從而建立較大的連續存儲區. 理論上,該方法對物理記憶體也是可能的,但由於許多物理記憶體頁不能移動到任意位置,阻礙了該方法的實施。因此,內核的方法是反碎片(anti-fragmentation), 即試圖從最初開始儘可能防止碎片.

反碎片的工作原理如何?

為理解該方法,我們必須知道內核將已分配頁劃分為下麵3種不同類型。

頁面類型 描述 舉例
不可移動頁 在記憶體中有固定位置, 不能移動到其他地方. 核心內核分配的大多數記憶體屬於該類別
可移動頁 可以隨意地移動. 屬於用戶空間應用程式的頁屬於該類別. 它們是通過頁表映射的
如果它們複製到新位置,頁表項可以相應地更新,應用程式不會註意到任何事
可回收頁 不能直接移動, 但可以刪除, 其內容可以從某些源重新生成. 例如,映射自文件的數據屬於該類別
kswapd守護進程會根據可回收頁訪問的頻繁程度,周期性釋放此類記憶體. , 頁面回收本身就是一個複雜的過程. 內核會在可回收頁占據了太多記憶體時進行回收, 在記憶體短缺(即分配失敗)時也可以發起頁面回收.

頁的可移動性,依賴該頁屬於3種類別的哪一種. 內核使用的反碎片技術, 即基於將具有相同可移動性的頁分組的思想.

為什麼這種方法有助於減少碎片?

由於頁無法移動, 導致在原本幾乎全空的記憶體區中無法進行連續分配. 根據頁的可移動性, 將其分配到不同的列表中, 即可防止這種情形. 例如, 不可移動的頁不能位於可移動記憶體區的中間, 否則就無法從該記憶體區分配較大的連續記憶體塊.

想一下, 上圖中大多數空閑頁都屬於可回收的類別, 而分配的頁則是不可移動的. 如果這些頁聚集到兩個不同的列表中, 如下圖所示. 在不可移動頁中仍然難以找到較大的連續空閑空間, 但對可回收的頁, 就容易多了.

但要註意, 從最初開始, 記憶體並未劃分為可移動性不同的區. 這些是在運行時形成的. 內核的另一種方法確實將記憶體分區, 分別用於可移動頁和不可移動頁的分配, 我會下文討論其工作原理. 但這種劃分對這裡描述的方法是不必要的

3.3 避免碎片數據結構

3.3.1 遷移類型

儘管內核使用的反碎片技術卓有成效,它對伙伴分配器的代碼和數據結構幾乎沒有影響。內核定義了一些枚舉常量(早期用巨集來實現)來表示不同的遷移類型, 參見include/linux/mmzone.h?v=4.7, line 38

enum {
        MIGRATE_UNMOVABLE,
        MIGRATE_MOVABLE,
        MIGRATE_RECLAIMABLE,
        MIGRATE_PCPTYPES,       /* the number of types on the pcp lists */
        MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
        /*
         * MIGRATE_CMA migration type is designed to mimic the way
         * ZONE_MOVABLE works.  Only movable pages can be allocated
         * from MIGRATE_CMA pageblocks and page allocator never
         * implicitly change migration type of MIGRATE_CMA pageblock.
         *
         * The way to use it is to change migratetype of a range of
         * pageblocks to MIGRATE_CMA which can be done by
         * __free_pageblock_cma() function.  What is important though
         * is that a range of pageblocks must be aligned to
         * MAX_ORDER_NR_PAGES should biggest page be bigger then
         * a single pageblock.
         */
        MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
        MIGRATE_ISOLATE,        /* can't allocate from here */
#endif
        MIGRATE_TYPES
};
巨集 類型
MIGRATE_UNMOVABLE 不可移動頁
MIGRATE_MOVABLE 可移動頁
MIGRATE_RECLAIMABLE 可回收頁
MIGRATE_PCPTYPES 是per_cpu_pageset, 即用來表示每CPU頁框高速緩存的數據結構中的鏈表的遷移類型數目
MIGRATE_HIGHATOMIC 在罕見的情況下,內核需要分配一個高階的頁面塊而不能休眠.如果向具有特定可移動性的列表請求分配記憶體失敗,這種緊急情況下可從MIGRATE_HIGHATOMIC中分配記憶體
MIGRATE_CMA Linux內核最新的連續記憶體分配器(CMA), 用於避免預留大塊記憶體
MIGRATE_ISOLATE 是一個特殊的虛擬區域, 用於跨越NUMA結點移動物理記憶體頁. 在大型系統上, 它有益於將物理記憶體頁移動到接近於使用該頁最頻繁的CPU.
MIGRATE_TYPES 只是表示遷移類型的數目, 也不代表具體的區域

對於MIGRATE_CMA類型, 其中在我們使用ARM等嵌入式Linux系統的時候, 一個頭疼的問題是GPU, Camera, HDMI等都需要預留大量連續記憶體,這部分記憶體平時不用,但是一般的做法又必須先預留著. 目前, Marek Szyprowski和Michal Nazarewicz實現了一套全新的Contiguous Memory Allocator. 通過這套機制, 我們可以做到不預留記憶體,這些記憶體平時是可用的,只有當需要的時候才被分配給Camera,HDMI等設備. 參照宋寶華–Linux內核最新的連續記憶體分配器(CMA)——避免預留大塊記憶體, 內核為此提供了函數is_migrate_cma來檢測當前類型是否為MIGRATE_CMA, 該函數定義在include/linux/mmzone.h?v=4.7, line 69

/* In mm/page_alloc.c; keep in sync also with show_migration_types() there */
extern char * const migratetype_names[MIGRATE_TYPES];

#ifdef CONFIG_CMA
#  define is_migrate_cma(migratetype) unlikely((migratetype) == MIGRATE_CMA)
#else
#  define is_migrate_cma(migratetype) false
#endif

對伙伴系統數據結構的主要調整, 是將空閑列表分解為MIGRATE_TYPE個列表, 可以參見free_area的定義include/linux/mmzone.h?v=4.7, line 88

struct free_area
{
    struct list_head        free_list[MIGRATE_TYPES];
    unsigned long           nr_free;
};
  • nr_free統計了所有列表上空閑頁的數目,而每種遷移類型都對應於一個空閑列表

巨集for_each_migratetype_order(order, type)可用於迭代指定遷移類型的所有分配階

#define for_each_migratetype_order(order, type) \
        for (order = 0; order < MAX_ORDER; order++) \
                for (type = 0; type < MIGRATE_TYPES; type++)

3.3.2 遷移備用列表fallbacks

如果內核無法滿足針對某一給定遷移類型的分配請求, 會怎麼樣?

此前已經出現過一個類似的問題, 即特定的NUMA記憶體域無法滿足分配請求時. 我們需要從其他記憶體域中選擇一個代價最低的記憶體域完成記憶體的分配, 因此內核在記憶體的結點pg_data_t中提供了一個備用記憶體域列表zonelists.

內核在記憶體遷移的過程中處理這種情況下的做法是類似的. 提供了一個備用列表fallbacks, 規定了在指定列表中無法滿足分配請求時. 接下來應使用哪一種遷移類型, 定義在mm/page_alloc.c?v=4.7, line 1799

/*
 * This array describes the order lists are fallen back to when
 * the free lists for the desirable migrate type are depleted
 * 該數組描述了指定遷移類型的空閑列表耗盡時
 * 其他空閑列表在備用列表中的次序
 */
static int fallbacks[MIGRATE_TYPES][4] = {
    //  分配不可移動頁失敗的備用列表
    [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,   MIGRATE_TYPES },
    //  分配可回收頁失敗時的備用列表
    [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,   MIGRATE_TYPES },
    //  分配可移動頁失敗時的備用列表
    [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES },
#ifdef CONFIG_CMA
    [MIGRATE_CMA]     = { MIGRATE_TYPES }, /* Never used */
#endif
#ifdef CONFIG_MEMORY_ISOLATION
    [MIGRATE_ISOLATE]     = { MIGRATE_TYPES }, /* Never used */
#endif
};

該數據結構大體上是自明的 :

每一行對應一個類型的備用搜索域的順序, 在內核想要分配不可移動頁MIGRATE_UNMOVABLE時, 如果對應鏈表為空, 則遍歷fallbacks[MIGRATE_UNMOVABLE], 首先後退到可回收頁鏈表MIGRATE_RECLAIMABLE, 接下來到可移動頁鏈表MIGRATE_MOVABLE, 最後到緊急分配鏈表MIGRATE_TYPES.

3.3.3 pageblock_order變數

全局變數和輔助函數儘管頁可移動性分組特性總是編譯到內核中,但只有在系統中有足夠記憶體可以分配到多個遷移類型對應的鏈表時,才是有意義的。由於每個遷移鏈表都應該有適當數量的記憶體,內核需要定義”適當”的概念. 這是通過兩個全局變數pageblock_order和pageblock_nr_pages提供的. 第一個表示內核認為是”大”的一個分配階, pageblock_nr_pages則表示該分配階對應的頁數。如果體繫結構提供了巨型頁機制, 則pageblock_order通常定義為巨型頁對應的分配階. 定義在include/linux/pageblock-flags.h?v=4.7, line 44

#ifdef CONFIG_HUGETLB_PAGE

    #ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE

        /* Huge page sizes are variable */
        extern unsigned int pageblock_order;

    #else /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */

    /* Huge pages are a constant size */
        #define pageblock_order         HUGETLB_PAGE_ORDER

    #endif /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */

#else /* CONFIG_HUGETLB_PAGE */

    /* If huge pages are not used, group by MAX_ORDER_NR_PAGES */
    #define pageblock_order         (MAX_ORDER-1)

#endif /* CONFIG_HUGETLB_PAGE */

#define pageblock_nr_pages      (1UL << pageblock_order)

在IA-32體繫結構上, 巨型頁長度是4MB, 因此每個巨型頁由1024個普通頁組成, 而HUGETLB_PAGE_ORDER則定義為10. 相比之下, IA-64體繫結構允許設置可變的普通和巨型頁長度, 因此HUGETLB_PAGE_ORDER的值取決於內核配置.

如果體繫結構不支持巨型頁, 則將其定義為第二高的分配階, 即MAX_ORDER - 1

/* If huge pages are not used, group by MAX_ORDER_NR_PAGES */
#define pageblock_order         (MAX_ORDER-1)

如果各遷移類型的鏈表中沒有一塊較大的連續記憶體, 那麼頁面遷移不會提供任何好處, 因此在可用記憶體太少時內核會關閉該特性. 這是在build_all_zonelists函數中檢查的, 該函數用於初始化記憶體域列表. 如果沒有足夠的記憶體可用, 則全局變數page_group_by_mobility_disabled設置為0, 否則設置為1.

內核如何知道給定的分配記憶體屬於何種遷移類型?

我們將在以後講解, 有關各個記憶體分配的細節都通過分配掩碼指定.

內核提供了兩個標誌,分別用於表示分配的記憶體是可移動的(__GFP_MOVABLE)或可回收的(__GFP_RECLAIMABLE).

3.3.4 gfpflags_to_migratetype函數

/* Convert GFP flags to their corresponding migrate type */
static inline int allocflags_to_migratetype(gfp_t gfp_flags)
{
    WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK);

    if (unlikely(page_group_by_mobility_disabled))
        return MIGRATE_UNMOVABLE;

    /* Group based on mobility */
    return (((gfp_flags & __GFP_MOVABLE) != 0) << 1) |
        ((gfp_flags & __GFP_RECLAIMABLE) != 0);
}

如果停用了頁面遷移特性, 則所有的頁都是不可移動的. 否則. 該函數的返回值可以直接用作free_area.free_list的數組索引.

3.3.5 pageblock_flags變數與其函數介面

最後要註意, 每個記憶體域都提供了一個特殊的欄位, 可以跟蹤包含pageblock_nr_pages個頁的記憶體區的屬性. 即zone->pageblock_flags欄位, 當前只有與頁可移動性相關的代碼使用, 參見include/linux/mmzone.h?v=4.7, line 367

struct zone
{
#ifndef CONFIG_SPARSEMEM
    /*
     * Flags for a pageblock_nr_pages block. See pageblock-flags.h.
     * In SPARSEMEM, this map is stored in struct mem_section
     */
    unsigned long       *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */
};

在初始化期間, 內核自動確保對記憶體域中的每個不同的遷移類型分組, 在pageblock_flags中都分配了足夠存儲NR_PAGEBLOCK_BITS個比特位的空間。當前,表示一個連續記憶體區的遷移類型需要3個比特位, 參見include/linux/pageblock-flags.h?v=4.7, line 28

/* Bit indices that affect a whole block of pages */
enum pageblock_bits {
    PB_migrate,
    PB_migrate_end = PB_migrate + 3 - 1,
            /* 3 bits required for migrate types */
    PB_migrate_skip,/* If set the block is skipped by compaction */

    /*
     * Assume the bits will always align on a word. If this assumption
     * changes then get/set pageblock needs updating.
     */
    NR_PAGEBLOCK_BITS
};

內核提供set_pageblock_migratetype負責設置以page為首的一個記憶體區的遷移類型, 該函數定義在mm/page_alloc.c?v=4.7, line 458, 如下所示

void set_pageblock_migratetype(struct page *page, int migratetype)
{
    if (unlikely(page_group_by_mobility_disabled &&
             migratetype < MIGRATE_PCPTYPES))
        migratetype = MIGRATE_UNMOVABLE;

    set_pageblock_flags_group(page, (unsigned long)migratetype,
                    PB_migrate, PB_migrate_end);
}

migratetype參數可以通過上文介紹的gfpflags_to_migratetype輔助函數構建. 請註意很重要的一點, 頁的遷移類型是預先分配好的, 對應的比特位總是可用, 與頁是否由伙伴系統管理無關. 在釋放記憶體時,頁必須返回到正確的遷移鏈表。這之所以可行,是因為能夠從get_pageblock_migratetype獲得所需的信息. 參見include/linux/mmzone.h?v=4.7, line 84

#define get_pageblock_migratetype(page)                                 \
        get_pfnblock_flags_mask(page, page_to_pfn(page),                \
                        PB_migrate_end, MIGRATETYPE_MASK)

最後請註意, 在各個遷移鏈表之間, 當前的頁面分配狀態可以從/proc/pagetypeinfo獲得.

初始化基於可移動性的分組

在記憶體子系統初始化期間, memmap_init_zone負責處理記憶體域的page實例. 該函數定義在mm/page_alloc.c?v=4.7, line 5139, 該函數完成了一些不怎麼有趣的標準初始化工作,但其中有一件是實質性的,即所有的頁最初都標記為可移動的. 參見mm/page_alloc.c?v=4.7, line 5224

/*
 * Initially all pages are reserved - free ones are freed
 * up by free_all_bootmem() once the early boot process is
 * done. Non-atomic initialization, single-pass.
 */
void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
        unsigned long start_pfn, enum memmap_context context)
{
    /*  ......  */

    for (pfn = start_pfn; pfn < end_pfn; pfn++) {
        /*  ......  */
not_early:
        if (!(pfn & (pageblock_nr_pages - 1))) {
            struct page *page = pfn_to_page(pfn);

            __init_single_page(page, pfn, zone, nid);
            set_pageblock_migratetype(page, MIGRATE_MOVABLE);
        } else {
            __init_single_pfn(pfn, zone, nid);
        }
    }
}

在分配記憶體時, 如果必須”盜取”不同於預定遷移類型的記憶體區, 內核在策略上傾向於”盜取”更大的記憶體區. 由於所有頁最初都是可移動的, 那麼在內核分配不可移動的記憶體區時, 則必須”盜取”.

實際上, 在啟動期間分配可移動記憶體區的情況較少, 那麼分配器有很高的幾率分配長度最大的記憶體區, 並將其從可移動列表轉換到不可移動列表. 由於分配的記憶體區長度是最大的, 因此不會向可移動記憶體中引入碎片.

總而言之, 這種做法避免了啟動期間內核分配的記憶體(經常在系統的整個運行時間都不釋放)散佈到物理記憶體各處, 從而使其他類型的記憶體分配免受碎片的干擾,這也是頁可移動性分組框架的最重要的目標之一.

4 分配器API

4.1 分配記憶體的介面

就伙伴系統的介面而言, NUMA或UMA體繫結構是沒有差別的, 二者的調用語法都是相同的.

所有函數的一個共同點是 : 只能分配2的整數冪個頁.

因此,介面中不像C標準庫的malloc函數或bootmem和memblock分配器那樣指定了所需記憶體大小作為參數. 相反, 必須指定的是分配階, 伙伴系統將在記憶體中分配2^0 rder頁. 內核中細粒度的分配只能藉助於slab分配器(或者slub、slob分配器), 後者基於伙伴系統

記憶體分配函數 功能 定義
alloc_pages(mask, order) 分配2^0頁並返回一個struct page的實例,表示分配的記憶體塊的起始頁 NUMA-include/linux/gfp.h, line 466
UMA-include/linux/gfp.h?v=4.7, line 476
alloc_page(mask) 是前者在order = 0情況下的簡化形式,只分配一頁 include/linux/gfp.h?v=4.7, line 483
get_zeroed_page(mask) 分配一頁並返回一個page實例,頁對應的記憶體填充0(所有其他函數,分配之後頁的內容是未定義的) mm/page_alloc.c?v=4.7, line 3900
__get_free_pages(mask, order)
__get_free_page(mask)
工作方式與上述函數相同,但返回分配記憶體塊的虛擬地址,而不是page實例
get_dma_pages(gfp_mask, order) 用來獲得適用於DMA的頁. include/linux/gfp.h?v=4.7, line 503

在空閑記憶體無法滿足請求以至於分配失敗的情況下,所有上述函數都返回空指針(比如alloc_pages和alloc_page)或者0(比如get_zeroed_page、__get_free_pages和__get_free_page).

因此內核在各次分配之後都必須檢查返回的結果. 這種慣例與設計得很好的用戶層應用程式沒什麼不同, 但在內核中忽略檢查會導致嚴重得多的故障

內核除了伙伴系統函數之外, 還提供了其他記憶體管理函數. 它們以伙伴系統為基礎, 但並不屬於伙伴分配器自身. 這些函數包括vmalloc和vmalloc_32, 使用頁表將不連續的記憶體映射到內核地址空間中, 使之看上去是連續的.

還有一組kmalloc類型的函數, 用於分配小於一整頁的記憶體區. 其實現.

釋放函數

有4個函數用於釋放不再使用的頁,與所述函數稍有不同

記憶體釋放函數 描述
free_page(struct page )
free_pages(struct page , order)
用於將一個或2order頁返回給記憶體管理子系統。記憶體區的起始地址由指向該記憶體區的第一個page實例的指針表示
__free_page(addr)
__free_pages(addr, order)
類似於前兩個函數,但在表示需要釋放的記憶體區時,使用了虛擬記憶體地址而不是page實例

4.2 分配掩碼(gfp_mask標誌)

4.2.1 分配掩碼

前述所有函數中強制使用的mask參數,到底是什麼語義?

我們知道Linux將記憶體劃分為記憶體域. 內核提供了所謂的記憶體域修飾符(zone modifier)(在掩碼的最低4個比特位定義), 來指定從哪個記憶體域分配所需的頁.

內核使用巨集的方式定義了這些掩碼, 一個掩碼的定義被劃分為3個部分進行定義, 我們會逐步展開來講解, 參見include/linux/gfp.h?v=4.7, line 12~374, 共計26個掩碼信息, 因此後面__GFP_BITS_SHIFT = 26.

4.2.2 掩碼分類

Linux中這些掩碼標誌gfp_mask分為3種類型 :

類型 描述
區描述都符 內核把物理記憶體分為多個區, 每個區用於不同的目的, 區描述符指明到底從這些區中的哪一區進行分配
行為修飾符 表示內核應該如何分配所需的記憶體. 在某些特定情況下, 只能使用某些特定的方法分配記憶體
類型標誌 組合了行為修飾符和區描述符, 將這些可能用到的組合歸納為不同類型

4.2.3 內核中掩碼的定義

內核中的定義方式

//  http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7

/*  line 12 ~ line 44  第一部分
 *  定義可掩碼所在位的信息, 每個掩碼對應一位為1
 *  定義形式為  #define  ___GFP_XXX      0x01u
 */
/* Plain integer GFP bitmasks. Do not use this directly. */
#define ___GFP_DMA              0x01u
#define ___GFP_HIGHMEM          0x02u
#define ___GFP_DMA32            0x04u
#define ___GFP_MOVABLE          0x08u
/*  ......  */

/*  line 46 ~ line 192  第二部分
 *  定義掩碼和MASK信息, 第二部分的某些巨集可能是第一部分一個或者幾個的組合
 *  定義形式為  #define  __GFP_XXX        ((__force gfp_t)___GFP_XXX)
 */
#define __GFP_DMA       ((__force gfp_t)___GFP_DMA)
#define __GFP_HIGHMEM   ((__force gfp_t)___GFP_HIGHMEM)
#define __GFP_DMA32     ((__force gfp_t)___GFP_DMA32)
#define __GFP_MOVABLE   ((__force gfp_t)___GFP_MOVABLE)  /* ZONE_MOVABLE allowed */
#define GFP_ZONEMASK    (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)

/*  line 194 ~ line 260  第三部分
 *  定義掩碼
 *  定義形式為  #define  GFP_XXX      __GFP_XXX
 */
#define GFP_DMA         __GFP_DMA
#define GFP_DMA32       __GFP_DMA32

其中GFP縮寫的意思為獲取空閑頁(get free page), __GFP_MOVABLE不表示物理記憶體域, 但通知內核應在特殊的虛擬記憶體域ZONE_MOVABLE進行相應的分配.

定義掩碼位

我們首先來看第一部分, 內核源代碼中定義在include/linux/gfp.h?v=4.7, line 18 ~ line 44, 共計26個掩碼信息.

/* Plain integer GFP bitmasks. Do not use this directly. */
//  區域修飾符
#define ___GFP_DMA              0x01u
#define ___GFP_HIGHMEM          0x02u
#define ___GFP_DMA32            0x04u

//  行為修飾符
#define ___GFP_MOVABLE          0x08u       /* 頁是可移動的 */
#define ___GFP_RECLAIMABLE      0x10u       /* 頁是可回收的 */
#define ___GFP_HIGH             0x20u       /* 應該訪問緊急分配池? */
#define ___GFP_IO               0x40u       /* 可以啟動物理IO? */
#define ___GFP_FS               0x80u       /* 可以調用底層文件系統? */
#define ___GFP_COLD             0x100u     /* 需要非緩存的冷頁 */
#define ___GFP_NOWARN           0x200u     /* 禁止分配失敗警告 */
#define ___GFP_REPEAT           0x400u     /* 重試分配,可能失敗 */
#define ___GFP_NOFAIL           0x800u     /* 一直重試,不會失敗 */
#define ___GFP_NORETRY          0x1000u   /* 不重試,可能失敗 */
#define ___GFP_MEMALLOC         0x2000u     /* 使用緊急分配鏈表 */
#define ___GFP_COMP             0x4000u   /* 增加複合頁元數據 */
#define ___GFP_ZERO             0x8000u   /* 成功則返回填充位元組0的頁 */
//  類型修飾符
#define ___GFP_NOMEMALLOC       0x10000u     /* 不使用緊急分配鏈表 */
#define ___GFP_HARDWALL         0x20000u     /* 只允許在進程允許運行的CPU所關聯的結點分配記憶體 */
#define ___GFP_THISNODE         0x40000u     /* 沒有備用結點,沒有策略 */
#define ___GFP_ATOMIC           0x80000u    /* 用於原子分配,在任何情況下都不能中斷  */
#define ___GFP_ACCOUNT          0x100000u
#define ___GFP_NOTRACK          0x200000u
#define ___GFP_DIRECT_RECLAIM   0x400000u
#define ___GFP_OTHER_NODE       0x800000u
#define ___GFP_WRITE            0x1000000u
#define ___GFP_KSWAPD_RECLAIM   0x2000000u

定義掩碼

然後第二部分, 相對而言每一個巨集又被重新定義如下, 參見include/linux/gfp.h?v=4.7, line 46 ~ line 192

/*
* Physical address zone modifiers (see linux/mmzone.h - low four bits)
*
* Do not put any conditional on these. If necessary modify the definitions
* without the underscores and use them consistently. The definitions here may
* be used in bit comparisons.
* 定義區描述符
*/
#define __GFP_DMA       ((__force gfp_t)___GFP_DMA)
#define __GFP_HIGHMEM   ((__force gfp_t)___GFP_HIGHMEM)
#define __GFP_DMA32     ((__force gfp_t)___GFP_DMA32)
#define __GFP_MOVABLE   ((__force gfp_t)___GFP_MOVABLE)  /* ZONE_MOVABLE allowed */
#define GFP_ZONEMASK    (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)

/*
* Page mobility and placement hints
*
* These flags provide hints about how mobile the page is. Pages with similar
* mobility are placed within the same pageblocks to minimise problems due
* to external fragmentation.
*
* __GFP_MOVABLE (also a zone modifier) indicates that the page can be
*   moved by page migration during memory compaction or can be reclaimed.
*
* __GFP_RECLAIMABLE is used for slab allocations that specify
*   SLAB_RECLAIM_ACCOUNT and whose pages can be freed via shrinkers.
*
* __GFP_WRITE indicates the caller intends to dirty the page. Where possible,
*   these pages will be spread between local zones to avoid all the dirty
*   pages being in one zone (fair zone allocation policy).
*
* __GFP_HARDWALL enforces the cpuset memory allocation policy.
*
* __GFP_THISNODE forces the allocation to be satisified from the requested
*   node with no fallbacks or placement policy enforcements.
*
* __GFP_ACCOUNT causes the allocation to be accounted to kmemcg (only relevant
*   to kmem allocations).
*/
#define __GFP_RECLAIMABLE ((__force gfp_t)___GFP_RECLAIMABLE)
#define __GFP_WRITE     ((__force gfp_t)___GFP_WRITE)
#define __GFP_HARDWALL   ((__force gfp_t)___GFP_HARDWALL)
#define __GFP_THISNODE  ((__force gfp_t)___GFP_THISNODE)
#define __GFP_ACCOUNT   ((__force gfp_t)___GFP_ACCOUNT)

/*
* Watermark modifiers -- controls access to emergency reserves
*
* __GFP_HIGH indicates that the caller is high-priority and that granting
*   the request is necessary before the system can make forward progress.
*   For example, creating an IO context to clean pages.
*
* __GFP_ATOMIC indicates that the caller cannot reclaim or sleep and is
*   high priority. Users are typically interrupt handlers. This may be
*   used in conjunction with __GFP_HIGH
 *
 * __GFP_MEMALLOC allows access to all memory. This should only be used when
 *   the caller guarantees the allocation will allow more memory to be freed
 *   very shortly e.g. process exiting or swapping. Users either should
 *   be the MM or co-ordinating closely with the VM (e.g. swap over NFS).
 *
 * __GFP_NOMEMALLOC is used to explicitly forbid access to emergency reserves.
 *   This takes precedence over the __GFP_MEMALLOC flag if both are set.
 */
#define __GFP_ATOMIC    ((__force gfp_t)___GFP_ATOMIC)
#define __GFP_HIGH      ((__force gfp_t)___GFP_HIGH)
#define __GFP_MEMALLOC  ((__force gfp_t)___GFP_MEMALLOC)
#define __GFP_NOMEMALLOC ((__force gfp_t)___GFP_NOMEMALLOC)

/*
 * Reclaim modifiers
 *
 * __GFP_IO can start physical IO.
 *
 * __GFP_FS can call down to the low-level FS. Clearing the flag avoids the
 *   allocator recursing into the filesystem which might already be holding
 *   locks.
 *
 * __GFP_DIRECT_RECLAIM indicates that the caller may enter direct reclaim.
 *   This flag can be cleared to avoid unnecessary delays when a fallback
 *   option is available.
 *
 * __GFP_KSWAPD_RECLAIM indicates that the caller wants to wake kswapd when
 *   the low watermark is reached and have it reclaim pages until the high
 *   watermark is reached. A caller may wish to clear this flag when fallback
 *   options are available and the reclaim is likely to disrupt the system. The
 *   canonical example is THP allocation where a fallback is cheap but
 *   reclaim/compaction may cause indirect stalls.
 *
 * __GFP_RECLAIM is shorthand to allow/forbid both direct and kswapd reclaim.
 *
 * __GFP_REPEAT: Try hard to allocate the memory, but the allocation attempt
 *   _might_ fail.  This depends upon the particular VM implementation.
 *
 * __GFP_NOFAIL: The VM implementation _must_ retry infinitely: the caller
 *   cannot handle allocation failures. New users should be evaluated carefully
 *   (and the flag should be used only when there is no reasonable failure
 *   policy) but it is definitely preferable to use the flag rather than
 *   opencode endless loop around allocator.
 *
 * __GFP_NORETRY: The VM implementation must not retry indefinitely and will
 *   return NULL when direct reclaim and memory compaction have failed to allow
 *   the allocation to succeed.  The OOM killer is not called with the current
 *   implementation.
 */
#define __GFP_IO        ((__force gfp_t)___GFP_IO)
#define __GFP_FS        ((__force gfp_t)___GFP_FS)
#define __GFP_DIRECT_RECLAIM    ((__force gfp_t)___GFP_DIRECT_RECLAIM) /* Caller can reclaim */
#define __GFP_KSWAPD_RECLAIM    ((__force gfp_t)___GFP_KSWAPD_RECLAIM) /* kswapd can wake */
#define __GFP_RECLAIM ((__force gfp_t)(___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM))
#define __GFP_REPEAT    ((__force gfp_t)___GFP_REPEAT)
#define __GFP_NOFAIL    ((__force gfp_t)___GFP_NOFAIL)
#define __GFP_NORETRY   ((__force gfp_t)___GFP_NORETRY)

/*
 * Action modifiers
 *
 * __GFP_COLD indicates that the caller does not expect to be used in the near
 *   future. Where possible, a cache-cold page will be returned.
 *
 * __GFP_NOWARN suppresses allocation failure reports.
 *
 * __GFP_COMP address compound page metadata.
 *
 * __GFP_ZERO returns a zeroed page on success.
 *
 * __GFP_NOTRACK avoids tracking with kmemcheck.
 *
 * __GFP_NOTRACK_FALSE_POSITIVE is an alias of __GFP_NOTRACK. It's a means of
 *   distinguishing in the source between false positives and allocations that
 *   cannot be supported (e.g. page tables).
 *
 * __GFP_OTHER_NODE is for allocations that are on a remote node but that
 *   should not be accounted for as a remote allocation in vmstat. A
 *   typical user would be khugepaged collapsing a huge page on a remote
 *   node.
 */
#define __GFP_COLD      ((__force gfp_t)___GFP_COLD)
#define __GFP_NOWARN    ((__force gfp_t)___GFP_NOWARN)
#define __GFP_COMP      ((__force gfp_t)___GFP_COMP)
#define __GFP_ZERO      ((__force gfp_t)___GFP_ZERO)
#define __GFP_NOTRACK   ((__force gfp_t)___GFP_NOTRACK)
#define __GFP_NOTRACK_FALSE_POSITIVE (__GFP_NOTRACK)
#define __GFP_OTHER_NODE ((__force gfp_t)___GFP_OTHER_NODE)

/* Room for N __GFP_FOO bits */
#define __GFP_BITS_SHIFT 26
#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1))

給出的常數,其中一些很少使用,因此我不會討論。其中最重要的一些常數語義如下所示

其中在開始的位置定義了對應的區修飾符, 定義在include/linux/gfp.h?v=4.7, line 46 ~ line 57

區修飾符標誌 描述
__GFP_DMA 從ZONE_DMA中分配記憶體
__GFP_HIGHMEM 從ZONE_HIGHMEM活ZONE_NORMAL中分配記憶體
__GFP_DMA32 從ZONE_DMA32中分配記憶體
__GFP_MOVABLE 從__GFP_MOVABLE中分配記憶體

其次還定義了我們程式和函數中所需要的掩碼MASK的信息, 由於其中__GFP_DMA, __GFP_DMA32, __GFP_HIGHMEM, __GFP_MOVABLE是在記憶體中分別有對應的記憶體域信息, 因此我們定義了記憶體域的掩碼GFP_ZONEMASK, 參見include/linux/gfp.h?v=4.7, line 57

#define GFP_ZONEMASK    (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)

接著內核定義了行為修飾符

/* __GFP_WAIT表示分配記憶體的請求可以中斷。也就是說,調度器在該請求期間可隨意選擇另一個過程執行,或者該請求可以被另一個更重要的事件中斷. 分配器還可以在返回記憶體之前, 在隊列上等待一個事件(相關進程會進入睡眠狀態).

雖然名字相似,但__GFP_HIGH與__GFP_HIGHMEM毫無關係,請不要弄混這兩者
行為修飾符 描述
__GFP_RECLAIMABLE
__GFP_MOVABLE
是頁遷移機制所需的標誌. 顧名思義,它們分別將分配的記憶體標記為可回收的或可移動的。這影響從空閑列表的哪個子表獲取記憶體
__GFP_WRITE
__GFP_HARDWALL 只在NUMA系統上有意義. 它限制只在分配到當前進程的各個CPU所關聯的結點分配記憶體。如果進程允許在所有CPU上運行(預設情況),該標誌是無意義的。只有進程可以運行的CPU受限時,該標誌才有效果
__GFP_THISNODE 也只在NUMA系統上有意義。如果設置該比特位,則記憶體分配失敗的情況下不允許使用其他結點作為備用,需要保證在當前結點或者明確指定的結點上成功分配記憶體
__GFP_ACCOUNT
__GFP_ATOMIC
__GFP_HIGH 如果請求非常重要, 則設置__GFP_HIGH,即內核急切地需要記憶體時。在分配記憶體失敗可能給內核帶來嚴重後果時(比如威脅到系統穩定性或系統崩潰), 總是會使用該標誌
__GFP_MEMALLOC
__GFP_NOMEMALLOC
__GFP_IO 說明在查找空閑記憶體期間內核可以進行I/O操作. 實際上, 這意味著如果內核在記憶體分配期間換出頁, 那麼僅當設置該標誌時, 才能將選擇的頁寫入硬碟
__GFP_FS 允許內核執行VFS操作. 在與VFS層有聯繫的內核子系統中必須禁用, 因為這可能引起迴圈遞歸調用.
__GFP_DIRECT_RECLAIM
__GFP_KSWAPD_RECLAIM
__GFP_RECLAIM
__GFP_REPEAT 在分配失敗後自動重試,但在嘗試若幹次之後會停止
__GFP_NOFAIL 在分配失敗後一直重試,直至成功
__GFP_NORETRY 在分配失敗後不重試,因此可能分配失敗
__GFP_COLD 如果需要分配不在CPU高速緩存中的“冷”頁時,則設置__GFP_COLD
__GFP_NOWARN 在分配失敗時禁止內核故障警告。在極少數場合該標誌有用
__GFP_COMP 添加混合頁元素, 在hugetlb的代碼內部使用
__GFP_ZERO 在分配成功時,將返回填充位元組0的頁
__GFP_NOTRACK
__GFP_NOTRACK_FALSE_POSITIVE
__GFP_NOTRACK
__GFP_OTHER_NODE

那自然還有__GFP_BITS_SHIFT來表示我們所有的掩碼位, 由於我們共計26個掩碼位

/* Room for N __GFP_FOO bits */
#define __GFP_BITS_SHIFT 26
#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1))

可以同時指定這些分配標誌, 例如

ptr = kmalloc(size, __GFP_IO | __GFP_FS);

說明頁分配器(最終會調用alloc_page)在分配時可以執行I/O, 在必要時還可以執行文件系統操作. 這就讓內核有很大的自由度, 以便它儘可能找到空閑的記憶體來滿足分配請求. 大多數分配器都會執行這些修飾符, 但一般不是這樣直接指定, 而是將這些行為描述符標誌進行分組, 即類型標誌

掩碼分組

最後來看第三部分, 由於這些標誌幾乎總是組合使用,內核作了一些分組,包含了用於各種標準情形的適當的標誌. 稱之為類型標誌, 定義在include/linux/gfp.h?v=4.7, lien 194 ~ line 258

類型標誌指定所需的行為和區描述符以安城特殊類型的處理, 正因為這一點, 內核總是趨於使用正確的類型標誌, 而不是一味地指定它可能用到的多種描述符. 這麼做既簡單又不容易出錯誤.

如果有可能的話, 在記憶體管理子系統之外, 總是把下列分組之一用於記憶體分配. 在內核源代碼中, 雙下劃線通常用於內部數據和定義. 而這些預定義的分組名沒有雙下劃線首碼, 點從側面驗證了上述說法.

#define GFP_ATOMIC      (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
#define GFP_KERNEL      (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT)
#define GFP_NOWAIT      (__GFP_KSWAPD_RECLAIM)
#define GFP_NOIO        (__GFP_RECLAIM)
#define GFP_NOFS        (__GFP_RECLAIM | __GFP_IO)
#define GFP_TEMPORARY   (__GFP_RECLAIM | __GFP_IO | __GFP_FS | \
                         __GFP_RECLAIMABLE)
#define GFP_USER        (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA         __GFP_DMA
#define GFP_DMA32       __GFP_DMA32
#define GFP_HIGHUSER    (GFP_USER | __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE    (GFP_HIGHUSER | __GFP_MOVABLE)
#define GFP_TRANSHUGE   ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
                         __GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN) & \
                         ~__GFP_RECLAIM)

/* Convert GFP flags to their corresponding migrate type */
#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)
#define GFP_MOVABLE_SHIFT 3
掩碼組 描述
GFP_ATOMIC 用於原子分配,在任何情況下都不能中斷, 可能使用緊急分配鏈表中的記憶體, 這個標誌用在中斷處理程式, 下半部, 持有自旋鎖以及其他不能睡眠的地方
GFP_KERNEL 這是一種常規的分配方式, 可能會阻塞. 這個標誌在睡眠安全時用在進程的長下文代碼中. 為了獲取調用者所需的記憶體, 內核會儘力而為. 這個標誌應該是首選標誌
GFP_KERNEL_ACCOUNT
GFP_NOWAIT 與GFP_ATOMIC類似, 不同之處在於, 調用不會退給緊急記憶體池, 這就增加了記憶體分配失敗的可能性
GFP_NOIO 這種分配可以阻塞, 但不會啟動磁碟I/O, 這個標誌在不能引發更多的磁碟I/O時阻塞I/O代碼, 這可能導致令人不愉快的遞歸
GFP_NOFS 這種分配在必要時可以阻塞, 但是也可能啟動磁碟, 但是不會啟動文件系統操作, 這個標誌在你不鞥在啟動另一個文件系統操作時, 用在文件系統部分的代碼中
GFP_TEMPORARY
GFP_USER 這是一種常規的分配方式, 可能會阻塞. 這個標誌用於為用戶空間進程分配記憶體時使用
GFP_DMA
GFP_DMA32 用於分配適用於DMA的記憶體, 當前是__GFP_DMA的同義詞, GFP_DMA32也是__GFP_GMA32的同義詞
GFP_HIGHUSER 是GFP_USER的一個擴展, 也用於用戶空間. 它允許分配無法直接映射的高端記憶體. 使用高端記憶體頁是沒有壞處的,因為用戶過程的地址空間總是通過非線性頁表組織的
GFP_HIGHUSER_MOVABLE 用途類似於GFP_HIGHUSER,但分配將從虛擬記憶體域ZONE_MOVABLE進行
GFP_TRANSHUGE
  • 其中GFP_NOIO和GFP_NOFS, 分別明確禁止I/O操作和訪問VFS層, 但同時設置了__GFP_RECLAIM,因此可以被回收
  • 而GFP_KERNEL和GFP_USER. 分別是內核和用戶分配的預設設置。二者的失敗不會立即威脅系統穩定性, GFP_KERNEL絕對是內核源代碼中最常使用的標誌 |

最後內核設置了碎片管理的可移動依據組織頁的MASK信息GFP_MOVABLE_MASK, 參見include/linux/gfp.h?v=4.7, line 262

/* Convert GFP flags to their corresponding migrate type */
#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)
#define GFP_MOVABLE_SHIFT 3

在你編寫的絕大多數代碼中, 用麽用到的是GFP_KERNEL, 要麼是GFP_ATOMIC, 當然各個類型標誌也均有其應用場景

情形 相應標誌
進程上下文, 可以睡眠 使用GFP_KERNEL
進程上下文, 不可以睡眠 使用GFP_KERNEL, 在你睡眠之前或之後以GFP_KERNEL執行記憶體分配
中斷處理程式 使用GFP_ATMOIC
軟中斷 使用GFP_ATMOIC
tasklet 使用GFP_ATMOIC
需要用於DMA的記憶體, 可以睡眠 使用(GFP_DMA GFP_KERNEL)
需要用於DMA的記憶體, 不可以睡眠 使用(GFP_DMA GFP_ATOMIC), 或在你睡眠之前執行記憶體分配

4.2.5 總結

我們從註釋中找到這樣的信息, 可以作為參考

bit       result
=================
0x0    => NORMAL
0x1    => DMA or NORMAL
0x2    => HIGHMEM or NORMAL
0x3    => BAD (DMA+HIGHMEM)
0x4    => DMA32 or DMA or NORMAL
0x5    => BAD (DMA+DMA32)
0x6    => BAD (HIGHMEM+DMA32)
0x7    => BAD (HIGHMEM+DMA32+DMA)
0x8    => NORMAL (MOVABLE+0)
0x9    => DMA or NORMAL (MOVABLE+DMA)
0xa    => MOVABLE (Movable is valid only if HIGHMEM is set too)
0xb    => BAD (MOVABLE+HIGHMEM+DMA)
0xc    => DMA32 (MOVABLE+DMA32)
0xd    => BAD (MOVABLE+DMA32+DMA)
0xe    => BAD (MOVABLE+DMA32+HIGHMEM)
0xf    => BAD (MOVABLE+DMA32+HIGHMEM+DMA)

GFP_ZONES_SHIFT must be <= 2 on 32 bit platforms.

很有趣的一點是,沒有__GFP_NORMAL常數,而記憶體分配的主要負擔卻落到ZONE_NORMAL記憶體域

內核考慮到這一點, 提供了一個函數gfp_zone來計算與給定分配標誌相容的最高記憶體域. 那麼記憶體分配可以從該記憶體域或更低的記憶體域進行, 該函數定義在include/linux/gfp.h?v=4.7, line 394

#if defined(CONFIG_ZONE_DEVICE) && (MAX_NR_ZONES-1) <= 4
/* ZONE_DEVICE is not a valid GFP zone specifier */
#define GFP_ZONES_SHIFT 2
#else
#define GFP_ZONES_SHIFT ZONES_SHIFT
#endif

#if 16 * GFP_ZONES_SHIFT > BITS_PER_LONG
#error GFP_ZONES_SHIFT too large to create GFP_ZONE_TABLE integer
#endif

由於記憶體域修飾符的解釋方式不是那麼直觀, 表3-7給出了該函數結果的一個例子, 其中DMA和DMA32記憶體域相同. 假定在下文中沒有設置__GFP_MOVABLE修飾符.

修飾符 掃描的記憶體域
ZONE_NORMAL、ZONE_DMA
__GFP_DMA ZONE_DMA
__GFP_DMA & __GFP_HIGHMEM ZONE_DMA
__GFP_HIGHMEM ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA
  • 如果__GFP_DMA和__GFP_HIGHMEM都沒有設置, 則首先掃描ZONE_NORMAL, 後面是ZONE_DMA
  • 如果設置了__GFP_HIGHMEM沒有設置__GFP_DMA,則結果是從ZONE_HIGHMEM開始掃描所有3個記憶體域。
  • 如果設置了__GFP_DMA,那麼__GFP_HIGHMEM設置與否沒有關係. 只有ZONE_DMA用於3種情形. 這是合理的, 因為同時使用__GFP_HIGHMEM和__GFP_DMA沒有意義. 高端記憶體從來都不適用於DMA

設置__GFP_MOVABLE不會影響內核的決策,除非它與__GFP_HIGHMEM同時指定. 在這種情況下, 會使用特殊的虛擬記憶體域ZONE_MOVABLE滿足記憶體分配請求. 對前文描述的內核的反碎片策略而言, 這種行為是必要的.

除了記憶體域修飾符之外, 掩碼中還可以設置一些標誌.

下圖中給出了掩碼的佈局,以及與各個比特位置關聯的常數. __GFP_DMA32出現了幾次,因為它可能位於不同的地方.

與記憶體域修飾符相反, 這些額外的標誌並不限制從哪個物理記憶體段分配記憶體, 但確實可以改變分配器的行為. 例如, 它們可以修改查找空閑記憶體時的積極程度.

4.3 分配頁

4.3.1 記憶體分配統一到alloc_pages介面

通過使用標誌、記憶體域修飾符和各個分配函數,內核提供了一種非常靈活的記憶體分配體系.儘管如此, 所有介面函數都可以追溯到一個簡單的基本函數(alloc_pages_node)

分配單頁的函數alloc_page和__get_free_page, 還有__get_dma_pages是藉助於巨集定義的.

//  http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L483
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)

//  http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L500
#define __get_free_page(gfp_mask) \
    __get_free_pages((gfp_mask), 0)`

//  http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L503
#define __get_dma_pages(gfp_mask, order) \
    __get_free_pages((gfp_mask) | GFP_DMA, (order))

get_zeroed_page的實現也沒什麼困難, 對__get_free_pages使用__GFP_ZERO標誌,即可分配填充位元組0的頁. 再返回與頁關聯的記憶體區地址即可.

//  http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3900
unsigned long get_zeroed_page(gfp_t gfp_mask)
{
        return __get_free_pages(gfp_mask | __GFP_ZERO, 0);
}
EXPORT_SYMBOL(get_zeroed_page);

__get_free_pages調用alloc_pages完成記憶體分配, 而alloc_pages又藉助於alloc_pages_node

__get_free_pages函數的定義在mm/page_alloc.c?v=4.7, line 3883

//  http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
    struct page *page;

    /*
     * __get_free_pages() returns a 32-bit address, which cannot represent
     * a highmem page
     */
    VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);

    page = alloc_pages(gfp_mask, order);
    if (!page)
        return 0;
    return (unsigned long) page_address(page);
}
EXPORT_SYMBOL(__get_free_pages);

在這種情況下, 使用了一個普通函數而不是巨集, 因為alloc_pages返回的page實例需要使用輔助

函數page_address轉換為記憶體地址. 在這裡,只要知道該函數可根據page實例計算相關頁的線性記憶體地址即可. 對高端記憶體頁這是有問題的

這樣, 就完成了所有分配記憶體的API函數到公共的基礎函數alloc_pages的統一

所有體繫結構都必須實現的標準函數clear_page, 可幫助alloc_pages對頁填充位元組0, 實現如下表所示

x86 arm
arch/x86/include/asm/page_32.h?v=4.7, line 24 arch/arm/include/asm/page.h?v=4.7#L14
arch/arm/include/asm/page-nommu.h

4.3.2 alloc_pages函數分配頁

既然所有的記憶體分配API函數都可以追溯掉alloc_page函數, 從某種意義上說,該函數是伙伴系統主要實現的”發射台”.

alloc_pages函數的定義是依賴於NUMA或者UMA架構的, 定義如下

#ifdef CONFIG_NUMA

//  http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L465
static inline struct page *
alloc_pages(gfp_t gfp_mask, unsigned int order)
{
        return alloc_pages_current(gfp_mask, order);

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 周末閑暇,閑來無事,想起以前在校做的小項目,於是想打包成exe安裝包,今天和各位碼農分享一下 。 首先打開自己項目,在工具欄中找到"工具"選擇"擴展和更新" 然後選擇”聯機“在右側搜索框輸入”Visual studio Installer“點擊安裝 Visual studio Installer安裝 ...
  • 在 MasterTableView 加上 GroupsDefaultExpanded = " true " 即可 自動展開分組下麵的子項 ...
  • 在 forms 裡面,目前使用比較多的彈出組件是 Acr.UserDialogs ,但是這個組件有些小問題,比如 loading .hide 會同時把 toast 給一起關掉,android 下的 toast 希望是 安卓原生的toast 樣子,而不是 底部彈出一個橫條(其實是 android 的 ...
  • 一、命令介紹 ls命令用於顯示目錄中的信息。 二、實例 我們首先使用ls命令不加任何參數,不帶參數運行ls會只列出文件或者目錄。看不到其他信息輸出。 所處的工作目錄不同,當前工作目錄下的文件肯定也不同。 使用 ls 命令的“-a”參數看到全部文件(包括隱藏文件),使用“-l”參數可以查看文件的屬性、 ...
  • 安裝swat swat是一個圖形化的samba管理軟體,可以幫助不熟悉的人去靈活的配置samba服務, 1、安裝swat [root@localhost wj]#yum install -y samba-swat Dependency Updated: libsmbclient.i686 0:3.6 ...
  • { 個人心得: 嵌入式底層重要的是在CPU(各種架構)或SOC基礎上,利用u-boot初始化系統,並啟動OS,建立實時多任務環境、文件系統等,再根據功能要求設計上層程式;而對硬體的需有足夠掌握。 } 1 cmd命令 1.1 常用命令 pwd、ls、cd、mkdir(文件操作:touch、cp、mv、 ...
  • inotify簡介: inotify是一種強大的、細粒度的、非同步的文件系統事件監控機制,linux內核從2.6.13起,加入了inotify支持,通過inotify可以監控文件系統添加、刪除、修改、移動等各種事件,利用這個內核介面,第三方軟體就可以監控文件系統下文件的各種變化情況,而inotify- ...
  • 大家都知道數據非常重要的,需要經常備份,如果備份了,但無法恢復還原,那就證明你備份的很失敗,所有當我們備份了數據需要檢查是否備份完整,是否可用可恢復。以下為一個企業案例: 某公司里有一臺Web伺服器,裡面的數據很重要,但是如果硬碟壞了,數據就會丟失,現在領導要求你把數據在其他機器上做一個周期性定時備 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...