linux內存源碼分析 - 伙伴系統(初始化和申請頁框)


本文為原創,轉載請注明:http://www.cnblogs.com/tolimit/

 

 

  之前的文章已經介紹了伙伴系統,這篇我們主要看看源碼中是如何初始化伙伴系統、從伙伴系統中分配頁框,返回頁框於伙伴系統中的。

  我們知道,每個管理區都有自己的伙伴系統管理屬於這個管理區的頁框,這也說明了,在伙伴系統初始化時,管理區必須要已經存在(初始化完成)了。在管理區描述符(struct zone)中,struct free_area就專門用於描述伙伴系統的。在一個管理區中,伙伴系統一共維護着包含1,2,4,8,16,...,512,1024個連續頁框的鏈表,當我需要從此管理區分配8個頁框時,伙伴系統會從包含8個連續頁框的鏈表取出一個結點(一個結點就是一個連續的8個頁框),然后分配給我。這時如果8個連續頁框的鏈表為空,則會從16個連續頁框的鏈表取出一個結點將其分為兩個8個連續頁框的結點,並把它們放入8頁框的鏈表,然后再分配其中一個結點給我。而當回收頁框時,會嘗試與前后連續頁框組成更大的頁框塊,加入到更高級的頁框頁表中,比如,我釋放了8個頁框,伙伴系統會嘗試將我釋放這連續的8個頁框與前8個頁框或者后8個頁框合並,一直嘗試合並到不能合並為止,並加入相應的鏈表中。

  而注意的是,在系統中,需要向伙伴系統申請頁框時,頁框的數量都是以2的次方計算的,也就是申請頁框的數量一定是1,2,4,8,16,32,64......1024這些數中的一個。釋放時也是這樣的道理,不過一般而言,會從伙伴系統中申請頁框操作最頻繁的模塊可能就是slab/slub和建立進程的線性區了。

/* 內存管理區描述符 */
struct zone {

    ........

    /* 實現每CPU頁框高速緩存,里面包含每個CPU的單頁框的鏈表 */
    struct per_cpu_pageset __percpu *pageset;

    ........

    /* 對應於伙伴系統中MIGRATE_RESEVE鏈的頁塊的數量 */
    int            nr_migrate_reserve_block;

    ........

    /* 標識出管理區中的空閑頁框塊,用於伙伴系統 */
    /* MAX_ORDER為11,分別代表包含大小為1,2,4,8,16,32,64,128,256,512,1024個連續頁框的鏈表,具體見下面 */
    struct free_area    free_area[MAX_ORDER];

    ......

}



/* 伙伴系統的一個塊,描述1,2,4,8,16,32,64,128,256,512或1024個連續頁框的塊 */
struct free_area {
    /* 指向這個塊中所有空閑小塊的第一個頁描述符,這些小塊會按照MIGRATE_TYPES類型存放在不同指針里 */
    struct list_head    free_list[MIGRATE_TYPES];
    /* 空閑小塊的個數 */
    unsigned long        nr_free;
};

/* 每CPU高速緩存描述符 */ struct per_cpu_pageset { /* 核心結構,高速緩存頁框結構 */ struct per_cpu_pages pcp; #ifdef CONFIG_NUMA s8 expire; #endif #ifdef CONFIG_SMP s8 stat_threshold; s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS]; #endif }; struct per_cpu_pages { /* 當前CPU高速緩存中頁框個數 */ int count; /* number of pages in the list */ /* 上界,當此CPU高速緩存中頁框個數大於high,則會將batch個頁框放回伙伴系統 */ int high; /* high watermark, emptying needed */ /* 在高速緩存中將要添加或被刪去的頁框個數,當鏈表中頁框數量多個上界時會將batch個頁框放回伙伴系統,當鏈表中頁框數量為0時則從伙伴系統中獲取batch個頁框 */ int batch; /* chunk size for buddy add/remove */ /* Lists of pages, one per migrate type stored on the pcp-lists */ /* 頁框的鏈表,如果需要冷高速緩存,從鏈表尾開始獲取頁框,如果需要熱高速緩存,從鏈表頭開始獲取頁框 */ struct list_head lists[MIGRATE_PCPTYPES]; };

  在各個頁框塊鏈表中又有幾個以MIGRATE_TYPES區分的小鏈表,MIGRATE_TYPES類型如下:

  • MIGRATE_UNMOVABLE:頁框內容不可移動,在內存中位置必須固定,無法移動到其他地方,核心內核分配的大部分頁面都屬於這一類。
  • MIGRATE_RECLAIMABLE:頁框內容可回收,不能直接移動,但是可以回收,因為還可以從某些源重建頁面,比如映射文件的數據屬於這種類別,kswapd會按照一定的規則,周期性的回收這類頁面。
  • MIGRATE_MOVABLE:頁框內容可移動,屬於用戶空間應用程序的頁屬於此類頁面,它們是通過頁表映射的,因此我們只需要更新頁表項,並把數據復制到新位置就可以了,當然要注意,一個頁面可能被多個進程共享,對應着多個頁表項。
  • MIGRATE_PCPTYPES:用來表示每CPU頁框高速緩存的數據結構中的鏈表的遷移類型數目。
  • MIGRATE_CMA: 預留一段的內存給驅動使用,但當驅動不用的時候,伙伴系統可以分配給用戶進程用作匿名內存或者頁緩存。而當驅動需要使用時,就將進程占用的內存通過回收或者遷移的方式將之前占用的預留內存騰出來,供驅動使用。
  • MIGRATE_ISOLATE:不能從這個鏈表分配頁框,因為這個鏈表專門用於NUMA結點移動物理內存頁,將物理內存頁內容移動到使用這個頁最頻繁的CPU。
/* 這幾個鏈表主要用於反內存碎片 */
enum {
    MIGRATE_UNMOVABLE,         /* 頁框內容不可移動,在內存中位置必須固定,無法移動到其他地方,核心內核分配的大部分頁面都屬於這一類。 */
    MIGRATE_RECLAIMABLE,         /* 頁框內容可回收,不能直接移動,但是可以回收,因為還可以從某些源重建頁面,比如映射文件的數據屬於這種類別,kswapd會按照一定的規則,周期性的回收這類頁面。 */
    MIGRATE_MOVABLE,             /* 頁框內容可移動,屬於用戶空間應用程序的頁屬於此類頁面,它們是通過頁表映射的,因此我們只需要更新頁表項,並把數據復制到新位置就可以了
                                 * 當然要注意,一個頁面可能被多個進程共享,對應着多個頁表項。 
                                 */
    MIGRATE_PCPTYPES,             /* 用來表示每CPU頁框高速緩存的數據結構中的鏈表的遷移類型數目 */
    MIGRATE_RESERVE = MIGRATE_PCPTYPES,     
#ifdef CONFIG_CMA
    MIGRATE_CMA,                   /* 預留一段的內存給驅動使用,但當驅動不用的時候,伙伴系統可以分配給用戶進程用作匿名內存或者頁緩存。而當驅動需要使用時,就將進程占用的內存通過回收或者遷移的方式將之前占用的預留內存騰出來,供驅動使用。 */
#endif   
#ifdef CONFIG_MEMORY_ISOLATION
    MIGRATE_ISOLATE,            /* 不能從這個鏈表分配頁框,因為這個鏈表專門用於NUMA結點移動物理內存頁,將物理內存頁內容移動到使用這個頁最頻繁的CPU */
#endif
    MIGRATE_TYPES
};

  很簡單地看出,相應類型的頁要從相應的鏈表中獲取,而它們之間也是有一定的優先級順序的:

static int fallbacks[MIGRATE_TYPES][4] = {
    [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,     MIGRATE_RESERVE },
    [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,     MIGRATE_RESERVE },
#ifdef CONFIG_CMA
    [MIGRATE_MOVABLE]     = { MIGRATE_CMA,         MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
    [MIGRATE_CMA]         = { MIGRATE_RESERVE }, /* Never used */
#else
    [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE,   MIGRATE_RESERVE },
#endif
    [MIGRATE_RESERVE]     = { MIGRATE_RESERVE }, /* Never used */
#ifdef CONFIG_MEMORY_ISOLATION
    [MIGRATE_ISOLATE]     = { MIGRATE_RESERVE }, /* Never used */
#endif
};

  以MIGRATE_RECLAIMABLE為例,如果我需要申請這種頁框,當然會優先從這類頁框的鏈表中獲取,如果沒有,我會依次嘗試從MIGRATE_UNMOVABLE -> MIGRATE_MOVABLE -> MIGRATE_RESERVE鏈進行分配。

  之前的文章也有提到頁描述符,它用於描述一個物理頁框,所有的頁描述符保存在mem_map數組中,通過頁框號可以直接獲取它對應的頁描述符(以頁框號做mem_map數組下標獲取的就是對應的頁描述符),通過頁描述符我們可以確定物理頁框所在位置,所以在伙伴系統中,保存在鏈表中的結點,就是這組連續頁框的首個頁框的頁描述符。

 

 

初始化伙伴系統

  在初始化伙伴系統之前,所有的node和zone的描述符都已經初始化完畢,同時物理內存中所有的頁描述符頁相應的初始化為了MIGRATE_MOVABLE類型的頁。

  初始化過程中首先將所有管理區的伙伴系統鏈表置空,這些工作處於start_kernel() -> setup_arch() -> x86_init.paging.pagetable_init()中進行:

//以下代碼處於free_area_init_core()函數中


/* 將管理區ZONE的伙伴系統置空 */
static void __meminit zone_init_free_lists(struct zone *zone)
{
    unsigned int order, t;

    for (order = 0; order < MAX_ORDER; order++) 
        for (type = 0; type < MIGRATE_TYPES; type++) {
                INIT_LIST_HEAD(&zone->free_area[order].free_list[t]);
                zone->free_area[order].nr_free = 0;
            }
}



void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
        unsigned long start_pfn, enum memmap_context context)
{
        ........
/* 該區所有頁都設置為MIGRATE_MOVABLE */ if ((z->zone_start_pfn <= pfn) && (pfn < zone_end_pfn(z)) && !(pfn & (pageblock_nr_pages - 1))) set_pageblock_migratetype(page, MIGRATE_MOVABLE); ........ }

 

  

初始化高端內存區

  在伙伴系統的初始化過程中,X86下高端內存和低端內存的伙伴系統初始化是分開來進行的,順序是從高端內存區到低端內存區,在初始化伙伴系統之前,系統中的node、zone、page都已經初始化完成了,基本上伙伴系統的初始化就是將所有不用的頁框釋放回伙伴系統中。我們先看看高端內存區的伙伴系統初始化,主要在set_highmem_pages_init(void)函數中:

/* 所有高端內存管理區初始化,將所有node的所有zone的managed_pages置為0,並將他們的頁框回收到頁框分配器中 */
void __init set_highmem_pages_init(void)
{
    struct zone *zone;
    int nid;

    /* 將所有node的所有zone的managed_pages置為0,即將所有管理區的所管理頁數量設置為0 */
    reset_all_zones_managed_pages();
    
    /* 遍歷所有管理區,這里只初始化高端內存區 */
    for_each_zone(zone) {
        unsigned long zone_start_pfn, zone_end_pfn;

        /* 如果不是高端內存區,則下一個 */
        /* 判斷方法: 當前zone描述符地址 - 所屬node的zone描述符數組基地址 == 高端內存區偏移量 */
        if (!is_highmem(zone))
            continue;

        /* 該管理區開始頁框號 */
        zone_start_pfn = zone->zone_start_pfn;
        /* 該管理區結束頁框號 */
        zone_end_pfn = zone_start_pfn + zone->spanned_pages;

        /* 該管理區所屬的node結點號 */
        nid = zone_to_nid(zone);
        printk(KERN_INFO "Initializing %s for node %d (%08lx:%08lx)\n",
                zone->name, nid, zone_start_pfn, zone_end_pfn);
        
        /* 將start_pfn到end_pfn中所有頁框回收,並放入頁框分配器 */
        add_highpages_with_active_regions(nid, zone_start_pfn,
                 zone_end_pfn);
    }
}

  我們具體看add_highpages_with_active_regions函數:

/* 將start_pfn到end_pfn中所有頁框回收,並放入頁框分配器 */
void __init add_highpages_with_active_regions(int nid,
             unsigned long start_pfn, unsigned long end_pfn)
{
    phys_addr_t start, end;
    u64 i;

    /* 遍歷所有memblock.memory,此結構有e820傳遞的數據初始化成,每個node會是其中一個這種結構 */
    for_each_free_mem_range(i, nid, &start, &end, NULL) {
        /* 修正后第一個頁框號,因為有可能頁框號不在node上 */
        unsigned long pfn = clamp_t(unsigned long, PFN_UP(start),
                        start_pfn, end_pfn);
        /* 修正后最后一個頁框號 */
        unsigned long e_pfn = clamp_t(unsigned long, PFN_DOWN(end),
                          start_pfn, end_pfn);
        for ( ; pfn < e_pfn; pfn++)
            if (pfn_valid(pfn))
                /* 這里會將頁框回收到頁框分配器中 */
                free_highmem_page(pfn_to_page(pfn));
    }
}

  繼續

void free_highmem_page(struct page *page)
{
    
    __free_reserved_page(page);
    /* 系統中總頁數量++ */
    totalram_pages++;
    /* 頁所屬的管理區的managed_pages++ */
    page_zone(page)->managed_pages++;
    /* 高端內存頁數量++ */
    totalhigh_pages++;
}


static inline void __free_reserved_page(struct page *page)
{
    ClearPageReserved(page);
    /* page->_count = 1 */
    init_page_count(page);
    /* 釋放到伙伴系統中,之后給出詳細代碼 */
    __free_page(page);
}

  到這里整個高端內存區的伙伴系統就初始化完成了,關於__free_page()函數我們之后會說明,這個函數是伙伴系統的是否單個頁框的API。

 

初始化低端內存區(ZONE_DMA、ZONE_NORMAL)

  在系統初始化階段會先啟用一個bootmem分配器,此分配器是專門用於啟動階段的,一個bootmem分配器管理着一個node結點的所有內存,也就是在numa架構中多個node有多個bootmem,他們被鏈入bdata_list鏈表中保存。而伙伴系統的初始化就是將bootmem管理的所有物理頁框釋放到伙伴系統中去。

  我們先看看bootmem的struct bootmem_data結構:

/* bootmem分配器結點(管理着一整塊連續內存,可以管理一個node中所有的物理內存),啟動時使用 */
typedef struct bootmem_data {
    /* 此塊內存開始頁框號 */
    unsigned long node_min_pfn;
    /* 此塊內存結束頁框號,如果是32位系統下此保存的是 ZONE_NORMAL最后一個頁框號 */
    unsigned long node_low_pfn;
    /* 指向位圖內存區,node中所有ZONE_HIGHMEM之前的頁框都在這里面有一個位,每次需要分配內存時就會掃描找出一個空閑頁框,空洞的內存也會占用位,不過空洞的內存應該設置為已分配 */
    void *node_bootmem_map;
    /* 上次分配距離末尾的偏移量 */
    unsigned long last_end_off;
    unsigned long hint_idx;
    /* 鏈入bdata_list結構鏈表 */
    struct list_head list;
} bootmem_data_t;

  bootmem分配器核心就是node_bootmem_map這個位圖,每一位代表這個node的一個頁,當需要分配時就會掃描這個位圖,然后獲取一段物理頁框進行分配,一般都會從開始處向后進行分配,並沒有什么特殊的算法在其中。而伙伴系統初始化時頁會根據這個位圖,將位圖中空閑的頁釋放回到伙伴系統中,而已經分配出去的頁則不會在初始化階段釋放回伙伴系統,不過有可能會在系統運行過程中釋放回伙伴系統中,我們具體看看是如何實現初始化的:

/* 釋放所有啟動后不需要的內存到頁框分配器 */
unsigned long __init free_all_bootmem(void)
{
    unsigned long total_pages = 0;
    /* 系統會為每個node分配一個這種結構,這個管理着node中所有頁框,可以叫做bootmem分配器 */
    bootmem_data_t *bdata;

    /* 設置所有node的所有zone的managed_pages = 0,該函數在啟動時只會調用一次,如果初始化高端內存的伙伴系統時調用過,這里就不會再次調用了 */
    reset_all_zones_managed_pages();

    /* 遍歷所有需要釋放的啟動內存數據塊 */
    list_for_each_entry(bdata, &bdata_list, list)
        /* 釋放bdata啟動內存塊中所有頁框到頁框分配器 */
        total_pages += free_all_bootmem_core(bdata);

    /* 所有內存頁數量 */
    totalram_pages += total_pages;

    /* 返回總共釋放的頁數量 */
    return total_pages;
}

  繼續,主要看free_all_bootmem_core()函數:

/* 釋放bdata啟動內存塊中所有頁框到頁框分配器 */
static unsigned long __init free_all_bootmem_core(bootmem_data_t *bdata)
{
    struct page *page;
    unsigned long *map, start, end, pages, count = 0;

    /* 此bootmem沒有位圖,也就是沒有管理內存 */
    if (!bdata->node_bootmem_map)
        return 0;

    /* 此bootmem的位圖 */
    map = bdata->node_bootmem_map;
    /* 此bootmem包含的開始頁框 */
    start = bdata->node_min_pfn;
    /* 此bootmem包含的結束頁框 */
    end = bdata->node_low_pfn;

    bdebug("nid=%td start=%lx end=%lx\n",
        bdata - bootmem_node_data, start, end);

    /* 釋放 bdata->node_min_pfn 到 bdata->node_low_pfn 之間空閑的頁框到伙伴系統 */
    while (start < end) {
        unsigned long idx, vec;
        unsigned shift;

        /* 一次循環檢查long所占位數長度的頁框數量(32或64) */
        idx = start - bdata->node_min_pfn;
        shift = idx & (BITS_PER_LONG - 1);

        /* 做個整理,因為有可能start並不是按long位數對其的,有可能出現在了vec的中間位數 */
        vec = ~map[idx / BITS_PER_LONG];

        if (shift) {
            vec >>= shift;
            if (end - start >= BITS_PER_LONG)
                vec |= ~map[idx / BITS_PER_LONG + 1] <<
                    (BITS_PER_LONG - shift);
        }

        /* 如果檢查的這一塊內存塊全是空的,則一次性釋放 */
        if (IS_ALIGNED(start, BITS_PER_LONG) && vec == ~0UL) {
            /* 這一塊長度的內存塊都為空閑的,計算這塊內存的order,如果這塊內存塊長度是8個頁框,那order就是3(2的3次方) */
            int order = ilog2(BITS_PER_LONG);

            /* 從start開始,釋放2的order次方的頁框到伙伴系統 */
            __free_pages_bootmem(pfn_to_page(start), order);
            /* count用來記錄總共釋放的頁框 */
            count += BITS_PER_LONG;
            /* 開始位置向后移動 */
            start += BITS_PER_LONG;
        } else {
            /* 內存塊中有部分是頁框是空的,一頁一頁釋放 */
            unsigned long cur = start;

            start = ALIGN(start + 1, BITS_PER_LONG);
            while (vec && cur != start) {
                if (vec & 1) {
                    /* 獲取頁框描述符,頁框號實際上就是頁描述符在mem_map的偏移量 */
                    page = pfn_to_page(cur);
                    /* 將此頁釋放到伙伴系統 */
                    __free_pages_bootmem(page, 0);
                    count++;
                }
                vec >>= 1;
                ++cur;
            }
        }
    }

  在__free_pages_bootmem()中也是調用了__free_pages()將頁釋放到伙伴系統中:

void __init __free_pages_bootmem(struct page *page, unsigned int order)
{
    /* 需要釋放的頁數量 */
    unsigned int nr_pages = 1 << order;
    struct page *p = page;
    unsigned int loop;

    /* 預取指令,該指令用於把將要使用到的數據從內存提前裝入緩存中,以減少訪問主存的指令執行時的延遲 */
    prefetchw(p);
    for (loop = 0; loop < (nr_pages - 1); loop++, p++) {
        /* 預取下一個頁描述符 */
        prefetchw(p + 1);
        __ClearPageReserved(p);
        /* 設置page->_count = 0 */
        set_page_count(p, 0);
    }
    __ClearPageReserved(p);
    set_page_count(p, 0);

    /* 管理區的managed_pages加上這些頁數量 */
    page_zone(page)->managed_pages += nr_pages;
    /* 將首頁框的_count設置為1,代表被使用,因為被使用的頁框才能夠釋放 */
    set_page_refcounted(page);
    /* 釋放到管理區的伙伴系統 */
    __free_pages(page, order);
}

  到這里,高端內存和低端內存的初始化就已經完成了。所以未使用的頁框都已經放入伙伴系統中供伙伴系統進行管理。之后我們會詳細說明申請頁框和釋放頁框的相關的操作。

 

 

申請頁框

  對於申請單個頁框而言,系統會從每CPU高速緩存維護的單頁框鏈表中進行分配,而對於申請多個頁框,系統則從伙伴系統中進行分配,可以說每CPU高速緩存算是伙伴系統中的一小部分,專門用於分配單個頁框,因為系統希望盡量讓那些剛釋放掉的單頁框分配出去,這樣可以有效地提高緩存命中率,因為釋放掉的頁框可能還處於緩存中,而剛分配的頁框一般都會馬上使用,系統就不用對這些頁框換進換出緩存了。因為每個CPU都有自己的高速緩存,所以這個結構就叫每CPU高速緩存。

  伙伴系統提供了多個接口供其他模塊申請頁框使用,如下:

  • struct page * alloc_pages (gfp_mask, order):向伙伴系統請求連續的2的order次方個頁框,返回第一個頁描述符。
  • struct page * alloc_page (gfp_mask):相當於struct page * alloc_pages(gfp_mask, 0)。
  • void * __get_free_pages (gfp_mask, order):該函數類似於alloc_pages(),但返回第一個所分配頁的線性地址。
  • void * __get_free_page (gfp_mask):相當於void * __get_free_pages (gfp_mask, 0)。

  對於gfp_mask掩碼,有如下這些:

/* 允許內核對等待空閑頁框的當前進程進行阻塞 */
#define __GFP_WAIT    ((__force gfp_t)___GFP_WAIT)   
/* 允許內核訪問保留的頁框池 */
#define __GFP_HIGH    ((__force gfp_t)___GFP_HIGH)  
/* 允許內核在低端內存頁上執行IO傳輸 */
#define __GFP_IO    ((__force gfp_t)___GFP_IO)
/* 如果清0,則不允許內核執行依賴於文件系統的操作 */
#define __GFP_FS    ((__force gfp_t)___GFP_FS)  
/* 所請求的頁框可能是"冷"的 */
#define __GFP_COLD    ((__force gfp_t)___GFP_COLD)  
/* 一次內存分配失敗將不會產生警告 */
#define __GFP_NOWARN    ((__force gfp_t)___GFP_NOWARN)  
#define __GFP_REPEAT ((__force gfp_t)___GFP_REPEAT) /* 同上 */ #define __GFP_NOFAIL ((__force gfp_t)___GFP_NOFAIL) /* 一次分配失敗后不再重試 */ #define __GFP_NORETRY ((__force gfp_t)___GFP_NORETRY) #define __GFP_MEMALLOC ((__force gfp_t)___GFP_MEMALLOC) /* 屬於擴展頁的頁框 */ #define __GFP_COMP ((__force gfp_t)___GFP_COMP) /* 任何返回的頁框必須被填滿0 */ #define __GFP_ZERO ((__force gfp_t)___GFP_ZERO)

  而這些類型進行一些組合,會產生如下的一些掩碼:

/* 原子分配,分配期間不會進行阻塞 */
#define GFP_ATOMIC    (__GFP_HIGH)
#define GFP_NOIO    (__GFP_WAIT)
#define GFP_NOFS    (__GFP_WAIT | __GFP_IO)
#define GFP_KERNEL    (__GFP_WAIT | __GFP_IO | __GFP_FS)
#define GFP_TEMPORARY    (__GFP_WAIT | __GFP_IO | __GFP_FS | \
             __GFP_RECLAIMABLE)
#define GFP_USER    (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_HIGHUSER    (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | \
             __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE    (__GFP_WAIT | __GFP_IO | __GFP_FS | \
                 __GFP_HARDWALL | __GFP_HIGHMEM | \
                 __GFP_MOVABLE)
#define GFP_IOFS    (__GFP_IO | __GFP_FS)
#define GFP_TRANSHUGE    (GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
             __GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN | \
             __GFP_NO_KSWAPD)

  好了,我們主要看申請頁框的接口,這些接口最后都會調用到__alloc_pages_nodemask()函數,這里我只用alloc_pages()函數進行講解,我們先看從alloc_pages()如何到__alloc_pages_nodemask()函數的:

/* 分配頁框
 * gfp_mask: 標志
 * order: 需求2的次方個數頁框
 */
#define alloc_pages(gfp_mask, order) \
        alloc_pages_node(numa_node_id(), gfp_mask, order)



static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,
                        unsigned int order)
{
    /* Unknown node is current node */
    if (nid < 0)
        nid = numa_node_id();

    /* 根據node號獲取此node相應的zonelist,因為如果此node上沒法分配出多余的內存,會從zonelist的其他node的zone中分配 */
    return __alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask));
}



static inline struct page *
__alloc_pages(gfp_t gfp_mask, unsigned int order,
        struct zonelist *zonelist)
{
        /* 最后調用到的函數 */
    return __alloc_pages_nodemask(gfp_mask, order, zonelist, NULL);
}

  在node中會有多個zonelist,這個zonelist的作用是將所有的zone按相對於此node的優先級進行排序,鏈表頭4個就是此node的ZONE_MOVABLE、ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA,之后是排在此node之后的node的這4個管理區。然后再是排在此node之前的node的這4個管理區。在分配時就按照這個順序,直到分配出相應數量的頁框為止。

  在同一個node中的管理區中分配也有一定的順序,當我需要從高端內存區申請內存時,系統會按照 ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA 順序為我嘗試分配,而當我需要從ZONE_NORMAL區申請內存時,系統會按照ZONE_NORMAL -> ZONE_DMA 順序為我嘗試分配,當我需要從ZONE_DMA區申請內存時,系統只會從ZONE_DMA區為我分配。在NUMA架構里也是一樣,只會在其他node上的相應的管理區中分配內存。

  接下來我們要着重說明__alloc_pages_nodemask()函數。

 

__alloc_pages_nodemask()

  此函數可以說是頁框分配的心臟,里面組織了整個分配過程中遇到各種問題時的處理情況。

  伙伴系統的頁框分配方式主要有兩種:快速分配和慢速分配。

  • 快速分配:會根據zonelist鏈表的優先級順序,以zone的low閥值從相應zone的伙伴系統中分配連續頁框。
  • 慢速分配:在快速分配失敗之后執行,頁會根據zonelist鏈表的優先級順序,以zone的min閥值從相遇zone的伙伴系統中分配連續頁框,如果失敗,會喚醒kswapd內核線程,進行頁框的回收、頁框的異步壓縮和輕同步壓縮,以及oom等操作來使系統獲得更多的空閑頁框,並且在這些操作的過程中會調用快速分配嘗試獲取內存。

  伙伴系統分配可用頁框給申請者時,首先會根據zonelist對每個可用的zone進行快速分配,成功則返回第一個頁框的頁描述符,如果所有zone的快速分配都不成功,則會zonelist中的zone進行慢速分配,慢速分配中會進行內存回收、內存壓縮和喚醒kswapd線程也同時進行內存的回收工作,之后再嘗試繼續分配。我們先看看__alloc_pages_nodemask()源碼:

/* gfp_mask: 上層要求分配內存時使用的標志
 * order: 需要的連續頁框的order值,如果是1個頁框,則為0
 * zonelist: 合適的zone列表
 * nodemask: node結點掩碼,用於判斷允許從哪些node上分配
 */
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
            struct zonelist *zonelist, nodemask_t *nodemask)
{
    enum zone_type high_zoneidx = gfp_zone(gfp_mask);
    /* preferred_zone指向第一個合適的管理區 */    
    struct zone *preferred_zone;
    struct zoneref *preferred_zoneref;
    struct page *page = NULL;
    /* 從gfp_mask中獲取選定的頁框類型,當中只會檢查__GFP_MOVABLE和__GFP_RECLAIMABLE */
    int migratetype = gfpflags_to_migratetype(gfp_mask);
    unsigned int cpuset_mems_cookie;
    /* 這個需要注意一下,之后分配是會根據這個flags進行一定的操作,默認是使用zone的低閥值判斷是否需要進行內存回收 */
    int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET|ALLOC_FAIR;
    int classzone_idx;

    gfp_mask &= gfp_allowed_mask;

    lockdep_trace_alloc(gfp_mask);

    /* 如果設置了__GFP_WAIT,就檢查當前進程是否需要調度,如果要則會進行調度
     * 大多數情況的分配都會有__GFP_WAIT標志
     */
    might_sleep_if(gfp_mask & __GFP_WAIT);

    /* 檢查gfp_mask和order是否符合要求,就是跟fail_page_alloc里面每一項對比檢查 */
    if (should_fail_alloc_page(gfp_mask, order))
        return NULL;

    /* 檢查結點的管理區鏈表是否為空 */
    if (unlikely(!zonelist->_zonerefs->zone))
        return NULL;

    /* 如果使能了CMA,選定的頁框類型是可遷移的頁框,就在標志上加上ALLOC_CMA */
    if (IS_ENABLED(CONFIG_CMA) && migratetype == MIGRATE_MOVABLE)
        alloc_flags |= ALLOC_CMA;

retry_cpuset:
    /* 這里只是表明在這個順序鎖中是個讀者 */
    cpuset_mems_cookie = read_mems_allowed_begin();

    /* The preferred zone is used for statistics later */
    /* 獲取鏈表中第一個管理區,每一次retry_cpuset就是在一個管理區中進行分配 
     * preferred_zone指向第一個合適的管理區
     */
    preferred_zoneref = first_zones_zonelist(zonelist, high_zoneidx,
                nodemask ? : &cpuset_current_mems_allowed,
                &preferred_zone);
    if (!preferred_zone)
        goto out;
    
    /* 管理區的類型的偏移量,0是 ZONE_DMA , 1是 ZONE_NORMAL , 2是 ZONE_HIGHMEM , 3是 ZONE_MOVABLE */
    classzone_idx = zonelist_zone_idx(preferred_zoneref);

    /* 第一次嘗試分配頁框,這里是快速分配
     * 快速分配時以low閥值為標准
     * 遍歷zonelist,嘗試獲取2的order次方個連續的頁框 
     * 在遍歷zone時,如果此zone當前空閑內存減去需要申請的內存之后,空閑內存是低於low閥值,那么此zone會進行快速內存回收
     */
    page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, nodemask, order,
            zonelist, high_zoneidx, alloc_flags,
            preferred_zone, classzone_idx, migratetype);

    
    if (unlikely(!page)) {
        /* 如果沒有分配到所需連續的頁框,這里會嘗試第二次分配,這次是慢速分配,並且同樣分配時不允許進行IO操作 
         * 如果當前進程current->flags標志了PF_MEMALLOC_NOIO標志,表示進程獲取內存時禁止IO操作,則返回清除了__GFP_IO和__GFP_FS的gfp_mask
         * 而gfp_mask絕大多數情況都是允許__GFP_IO和__GFP_FS標志的
         */
        gfp_mask = memalloc_noio_flags(gfp_mask);
        /* 如果之前沒有分配成功,這里嘗試進入慢速分配,在這個函數中,會嘗試喚醒頁框回收線程,然后再進行分配 
         * 慢速分配首先會喚醒kswapd線程進行內存回收
         * 然后如果標記了忽略閥值,則從保留的內存里回收
         * 然后進行內存壓縮
         * 最后再嘗試直接內存回收
         */
        page = __alloc_pages_slowpath(gfp_mask, order,
                zonelist, high_zoneidx, nodemask,
                preferred_zone, classzone_idx, migratetype);
    }

    trace_mm_page_alloc(page, order, gfp_mask, migratetype);

out:
    /* 如果都沒有分配成功,這里會不停嘗試重新分配,獲取下一個zonelist的zone */
    if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie)))
        goto retry_cpuset;
    /* 返回第一個頁描述符 */
    return page;
}

   快速分配的函數是get_page_from_freelist(),可以說這個函數是最基本的獲取頁框函數,只要需要獲取頁框,就一定要調用這個函數,即使在慢速分配過程中想要獲取頁框,也需要調用這個函數進行獲取。我們看看:

/* 從管理區鏈表中遍歷所有管理區,獲取指定連續的頁框數 
 * 在遍歷管理區時,如果此zone當前空閑內存減去需要申請的內存之后,空閑內存是低於low閥值,那么此zone會進行快速內存回收
 * 第一輪循環會嘗試只從preferred_zone這個zone中獲取連續頁框,如果無法獲取,會進入第二輪循環
 * 第二輪循環會遍歷整個zonelist中的zone,從里面獲取連續頁框
 */
static struct page *
get_page_from_freelist(gfp_t gfp_mask, nodemask_t *nodemask, unsigned int order,
        struct zonelist *zonelist, int high_zoneidx, int alloc_flags,
        struct zone *preferred_zone, int classzone_idx, int migratetype)
{
    struct zoneref *z;
    struct page *page = NULL;
    struct zone *zone;
    nodemask_t *allowednodes = NULL;/* zonelist_cache approximation */
    int zlc_active = 0;        /* set if using zonelist_cache */
    int did_zlc_setup = 0;        /* just call zlc_setup() one time */
    /* 是否考慮臟頁過多的判斷值,如果臟頁過多,則不在此zone進行分配 */
    bool consider_zone_dirty = (alloc_flags & ALLOC_WMARK_LOW) &&
                (gfp_mask & __GFP_WRITE);
    int nr_fair_skipped = 0;
    bool zonelist_rescan;

zonelist_scan:
    zonelist_rescan = false;

    /* 遍歷結點中的管理區,如果要求從高端內存分配,則順序為ZONE_HighMen -> ZONE_NORMAL -> ZONE_DMA
     * 如果沒要求從高端內存分配,則順序為ZONE_NORMAL -> ZONE_DMA
     */
    for_each_zone_zonelist_nodemask(zone, z, zonelist,
                        high_zoneidx, nodemask) {
        unsigned long mark;

        if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
            !zlc_zone_worth_trying(zonelist, z, allowednodes))
                continue;

        /* 檢查此管理區是否屬於該CPU所允許分配的管理區 */
        if (cpusets_enabled() &&
            (alloc_flags & ALLOC_CPUSET) &&
            !cpuset_zone_allowed_softwall(zone, gfp_mask))
                continue;

        /* 公平分配,標記了ALLOC_FAIR的情況,會只從preferred_zone這個zone所在node
         * 所以第一輪循環,會只嘗試從preferred_zone所在node進行分配
         * 而第二輪循環,會遍歷整個zonelist里的包含的其他node的zone
         */
        if (alloc_flags & ALLOC_FAIR) {
            /* 判斷zone和preferred_zone是否屬於同一個node,不屬於則跳出循環,因為后面的頁不會屬於此node了 */
            if (!zone_local(preferred_zone, zone))
                break;
            /* 此zone屬於此node,看看ZONE_FAIR_DEPLETED標記有沒有置位,置位了說明此zone可用於其他zone的頁框數量已經用盡 */
            if (test_bit(ZONE_FAIR_DEPLETED, &zone->flags)) {
                nr_fair_skipped++;
                continue;
            }
        }

        /* 如果gfp_mask中允許進行臟頁回寫,那么如果此zone在內存中有過多的臟頁,則跳過此zone,不對此zone進行處理
         * 這里大概意思是臟頁過多,kswapd會將這些臟頁進行回寫,這里就不將這些臟頁進行回寫了,會增加整個zone的壓力
         */
        if (consider_zone_dirty && !zone_dirty_ok(zone))
            continue;

        /* 選擇閥值,閥值保存在管理區的watermark中,分別有alloc_min alloc_low alloc_high三種,選擇任何一種都會要求分配后空閑頁框數量不能少於閥值,默認是alloc_low */
        mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
        
        /* 根據閥值查看管理區中是否有足夠的空閑頁框,空閑內存數量保存在 zone->vm_stat[NR_FREE_PAGES],這里的檢查算法是: 分配后剩余的頁框數量是否大於閥值加上此zone保留的內存數量,高於則返回true
         * 三個閥值的大小關系是min < low < high
         * high一般用於判斷zone是否平衡
         * 快速分配時,用的閥值是low
         * 慢速分配中,用的閥值是min
         * 在准備oom進程時,用的閥值是high
         * 分配后剩余的頁框數量低於或等於閥值加上此zone保留的內存數量,那么進行快速內存回收
         */
        if (!zone_watermark_ok(zone, order, mark,
                       classzone_idx, alloc_flags)) {
            /* 沒有足夠的空閑頁框 */
            int ret;

            BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
            /* 如果分配標志中有 ALLOC_NO_WATERMARKS標志,代表無視閥值,直接分配 */
            if (alloc_flags & ALLOC_NO_WATERMARKS)
                goto try_this_zone;

            if (IS_ENABLED(CONFIG_NUMA) &&
                    !did_zlc_setup && nr_online_nodes > 1) {
                /* NUMA系統中如果使用了zlc(zonelist_cache),則取出此zonelist允許的node列表 */
                allowednodes = zlc_setup(zonelist, alloc_flags);
                zlc_active = 1;
                did_zlc_setup = 1;
            }

            /* 
             * 判斷是否對此zone進行內存回收,如果開啟了內存回收,則會對此zone進行內存回收,否則,通過距離判斷是否進行內存回收
             * zone_allows_reclaim()函數實際上就是判斷zone所在node是否與preferred_zone所在node的距離 < RECLAIM_DISTANCE(30或10)
             * 當內存回收未開啟的情況下,只會對距離比較近的zone進行回收
             */
            if (zone_reclaim_mode == 0 ||
                !zone_allows_reclaim(preferred_zone, zone))
                goto this_zone_full;

            if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
                !zlc_zone_worth_trying(zonelist, z, allowednodes))
                continue;

            /*
             * 這里就叫做快速回收,因為這里會選擇盡量快的回收方式
             * 回收到了2^order數量的頁框時,才會返回真,即使回收了,沒達到這個數量,也返回假
             */
            ret = zone_reclaim(zone, gfp_mask, order);
            switch (ret) {
            case ZONE_RECLAIM_NOSCAN:      /* 沒有進行回收 */
                /* did not scan */
                continue;
            case ZONE_RECLAIM_FULL:         /* 沒有找到可回收的頁面,也就是回收到的頁框數量為0 */
                /* scanned but unreclaimable */
                continue;
            default:
                /* 回收到了一些或者回收到了2^order個頁框,都會執行到這 */
                
                /* did we reclaim enough */
                /* 回收到了一些頁,這里繼續檢查閥值是否足夠分配連續頁框,足夠則跳到 try_this_zone 嘗試在此zone中分配 */
                if (zone_watermark_ok(zone, order, mark,
                        classzone_idx, alloc_flags))
                    goto try_this_zone;

                /* 如果是按照min閥值進行分配的(在慢速分配中會嘗試),或者從此zone回收到一些頁框了(但不足以分配),則跳到this_zone_full,標記此zone */
                if (((alloc_flags & ALLOC_WMARK_MASK) == ALLOC_WMARK_MIN) ||
                    ret == ZONE_RECLAIM_SOME)
                    goto this_zone_full;

                continue;
            }
        }

try_this_zone:
        /* 嘗試從這個zone獲取連續頁框
         * 只有當此zone中空閑頁框數量 - 本次需要分配的數量 > 此zone的low閥值,這樣才能執行到這
         * 如果本意從preferred_zone分配內存,但是preferred_zone沒有足夠內存,到到了此zone進行分配,那么分配的頁數量會統計到此zone的NR_ALLOC_BATCH
         */
        page = buffered_rmqueue(preferred_zone, zone, order,
                        gfp_mask, migratetype);
        /* 分配到了連續頁框,跳出循環 */
        if (page)
            break;
this_zone_full:
        if (IS_ENABLED(CONFIG_NUMA) && zlc_active)
            /* 在zonelist的zonelist_cache中標記此node為滿狀態 */
            zlc_mark_zone_full(zonelist, z);
    }

    /* 分配到了連續頁框 */
    if (page) {
        /* 如果分配時有 ALLOC_NO_WATERMARKS 標記則記錄到頁描述符中 */
        page->pfmemalloc = !!(alloc_flags & ALLOC_NO_WATERMARKS);
        /* 將連續頁框中第一個頁的頁描述符返回 */
        return page;
    }

    /* 這里是失敗時才會到這 */
    /* 如果第一次ALLOC_FAIR分配沒有能夠分配到內存,第二次嘗試非ALLOC_FAIR分配 
     * 第二次會遍歷zonelist中其他node上的zone
     * 而第一次公平分配不會
     */
    if (alloc_flags & ALLOC_FAIR) {
        alloc_flags &= ~ALLOC_FAIR;
        /* nr_fair_skipped不為0,說明此node有些zone的batch頁已經用盡,這里要增加一些給它 */
        if (nr_fair_skipped) {
            zonelist_rescan = true;
            /* 重新設置除目標zone之外,node中在此目標zone前面的zone的batch頁數量大小 
             * 設置為: batch頁數量 = high閥值 - 低閥值 - 當前batch數量
             */
            reset_alloc_batches(preferred_zone);
        }
        if (nr_online_nodes > 1)
            zonelist_rescan = true;
    }

    if (unlikely(IS_ENABLED(CONFIG_NUMA) && zlc_active)) {
        /* 禁止zonelist_cache,zonelist_cache用於快速掃描的,它標記着哪個node有空閑內存哪個node沒有,掃描時就跳過某些node */
        zlc_active = 0;
        zonelist_rescan = true;
    }

    /* 跳回去,嘗試再次掃描一遍zonelist,這里最多只會進行一次再次掃描,因為第二次就不會把 zonelist_rescan 設置為true了 */
    if (zonelist_rescan)
        goto zonelist_scan;

    return NULL;
}

   在快速分配中,如果條件允許會以low閥值遍歷兩次zonelist中的zone,整個快速分配的流程是:從zonelist中取出一個zone,檢查此zone標志判斷是否可通過此zone分配內存,如果 zone的空閑內存 - 需要申請的內存 < 閥值 ,伙伴系統則會將zone的一些快速內存回收,然后再次判斷閥值和空閑內存與申請內存大小直接的關系,如果 zone的空閑內存 - 需要申請的內存 > 閥值,則調用buffered_rmqueue()函數從此zone中的分配內存,否則,選取下一個zone繼續執行這段操作。當zonelist中的所有zone都遍歷完成后,還是沒有分配到內存,如果條件允許會再次遍歷一遍。由於在慢速過程中也會調用此函數進行快速內存分配,所以閥值是由調用者傳進來,因為不同情況使用的閥值是不同的,比如第一次快速分配過程中,會使用zone的low閥值進行分配,而進入慢速分配過程中,會使用min閥值進行分配。

  對於zone_reclaim函數這里就不詳細進行分析了,我們記得在伙伴系統中有一個每CPU高速緩存,里面保存着以migratetype分類的單頁框的雙向鏈表,當申請內存者只需要一個頁框時,內核會從每CPU高速緩存中相應類型的單頁框鏈表中獲取一個頁框交給申請者,這樣的好處是,但釋放單個頁框時會放入每CPU高速緩存鏈表,如果這時有需要申請單個頁框,就把這個剛剛釋放的頁框交付出去,因為這個頁框可能還存在於cache中,處理時就可直接處理cache而不用把這個頁框再放入cache中,提高了cache的命中率,這樣的頁框就稱為“熱”頁。每CPU高速緩存維護的這些所有類型的單頁框雙向鏈表時,把剛釋放的頁框從鏈表頭插入,申請“熱”頁時就從鏈表頭拿出頁框,申請“冷”頁時則從鏈表位拿出。我們看看buffered_rmqueue():

static inline
struct page *buffered_rmqueue(struct zone *preferred_zone,
            struct zone *zone, unsigned int order,
            gfp_t gfp_flags, int migratetype)
{
    unsigned long flags;
    struct page *page;
    bool cold = ((gfp_flags & __GFP_COLD) != 0);

again:
    if (likely(order == 0)) {
        /* 這里是只需要分配一個頁框,會從每CPU高速緩存中分配 */
        struct per_cpu_pages *pcp;
        struct list_head *list;

        local_irq_save(flags);
        /* 獲取此zone的每CPU高速緩存 */
        pcp = &this_cpu_ptr(zone->pageset)->pcp;
        /* 獲取需要的類型的頁框的高速緩存鏈表,高速緩存中也區分migratetype類型的鏈表,鏈表中保存的頁框對應的頁描述符 */
        list = &pcp->lists[migratetype];
        if (list_empty(list)) {
            /* 如果當前migratetype的每CPU高速緩存鏈表中沒有空閑的頁框,從伙伴系統中獲取batch個頁框加入到這個鏈表中,batch保存在每CPU高速緩存描述符中,在rmqueue_bulk中是每次要1個頁框,要batch次,也就是這些頁框是離散的 */
            pcp->count += rmqueue_bulk(zone, 0,
                    pcp->batch, list,
                    migratetype, cold);
            if (unlikely(list_empty(list)))
                goto failed;
        }

        if (cold)
            /* 需要冷的高速緩存,則從每CPU高速緩存的雙向鏈表的后面開始分配 */
            page = list_entry(list->prev, struct page, lru);
        else
            /* 需要熱的高速緩存,則從每CPU高速緩存的雙向鏈表的前面開始分配,因為釋放時會從鏈表頭插入,所以鏈表頭是熱的高速緩存 */
            page = list_entry(list->next, struct page, lru);
        /* 從每CPU高速緩存鏈表中拿出來 */
        list_del(&page->lru);
        pcp->count--;
    } else {
        /* 需要多個頁框,從伙伴系統中分配,但是申請多個頁框時是有可能會發生失敗的情況的,而分配時又表明__GFP_NOFAIL不允許發生失敗,所以這里給出一個警告 */
        if (unlikely(gfp_flags & __GFP_NOFAIL)) {
            WARN_ON_ONCE(order > 1);
        }
        spin_lock_irqsave(&zone->lock, flags);
        /* 從伙伴系統中獲取連續頁框,返回第一個頁的頁描述符 */
        page = __rmqueue(zone, order, migratetype);
        spin_unlock(&zone->lock);
        if (!page)
            goto failed;
        /* 統計,減少zone的free_pages數量統計,因為里面使用加法,所以這里傳進負數 */
        __mod_zone_freepage_state(zone, -(1 << order),
                      get_freepage_migratetype(page));
    }

    __mod_zone_page_state(zone, NR_ALLOC_BATCH, -(1 << order));
    if (atomic_long_read(&zone->vm_stat[NR_ALLOC_BATCH]) <= 0 &&
        !test_bit(ZONE_FAIR_DEPLETED, &zone->flags))
        set_bit(ZONE_FAIR_DEPLETED, &zone->flags);

    __count_zone_vm_events(PGALLOC, zone, 1 << order);
    /* 統計 */
    zone_statistics(preferred_zone, zone, gfp_flags);
    local_irq_restore(flags);

    VM_BUG_ON_PAGE(bad_range(zone, page), page);
    /* 檢查所有分配的連續頁框是否為空閑頁 */
    if (prep_new_page(page, order, gfp_flags))
        goto again;
    /* 返回第一個頁描述符 */
    return page;

failed:
    /* 分配失敗 */
    local_irq_restore(flags);
    return NULL;
}

  對於每CPU高速緩存,有可能出現的情況就是鏈表為空(太多人申請1個頁框),這時候每CPU高速緩存會向伙伴系統連續申請batch次單個頁框放入自己的鏈表中,這里發生在rmqueue_bulk()中。而如果一次申請多個頁框,則直接從伙伴系統中獲取,也就是__rmqueue()函數。而對於rmqueue_bulk()來說,其核心函數也是__rmqueue(),因為rmqueue_bulk()中也是調用了batch次__rmqueue()獲取batch個單頁框。

/* 從伙伴系統中獲取2的order次方個頁框,返回第一個頁框的描述符
 * zone: 管理區描述符
 * order: 需要頁面的2的次方數
 * migratetype: 從此類型中獲取,這時傳入的時需求的頁框類型
 */
static struct page *__rmqueue(struct zone *zone, unsigned int order,
                        int migratetype)
{
    struct page *page;

retry_reserve:
    /* 直接從migratetype類型的鏈表中獲取了2的order次方個頁框 */
    page = __rmqueue_smallest(zone, order, migratetype);

    /* 如果page為空,沒有在需要的migratetype類型中分配獲得頁框,說明當前需求類型(migratetype)的頁框沒有空閑,會根據fallback數組中定義好的優先級從其他類型的頁框中獲取頁框,一次移動一個pageblock */
    if (unlikely(!page) && migratetype != MIGRATE_RESERVE) {
        /* 根據fallbacks數組從其他migratetype類型的鏈表中獲取內存 */
        page = __rmqueue_fallback(zone, order, migratetype);

         /* 從其他類型的空閑頁框鏈表中也沒有獲得頁框,設定為默認類型的頁框,重試一次 */
        if (!page) {
            /* 定義從頁框屬性為MIGRATE_RESERVE的空閑鏈表中查找 */
            migratetype = MIGRATE_RESERVE;
            /* 重試嘗試從MIGRATE_RESERVE類型的鏈表中找出空閑內存 */
            goto retry_reserve;
        }
    }

    trace_mm_page_alloc_zone_locked(page, order, migratetype);
    return page;
}

  在__rmqueue中,可能會進行兩次分配,首先會調用__rmqueue_smallest()進行分配,如果分配失敗,則會調用__rmqueue_fallback()進行分配,為什么要這樣,就是與前面說的migratetype類型有關,首先調用__rmqueue_smallest(),此函數只會從傳入的migratetype類型的鏈表中進行分配,並不會到其他migratetype類型的鏈表中獲取頁框進行分配,而__rmqueue_fallback()則是嘗試從將一個其他migratetype類型的pageblock轉為我們需要的類型的pageblock,然后嘗試進行分配,而最后,我們看當__rmqueue_fallback()函數都無法進行分配時,會將migratetype設置為MIGRATE_RESERVE類型,從此類型中嘗試分配,首先先看__rm_queue_smallest(),其實現如下:

static inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
                        int migratetype)
{
    unsigned int current_order;
    struct free_area *area;
    struct page *page;

    /* 循環遍歷這層之后的空閑鏈表 */
    for (current_order = order; current_order < MAX_ORDER; ++current_order) {
        area = &(zone->free_area[current_order]);
        /* 如果當前空閑鏈表為空,則從更高一級的鏈表中獲取空閑頁框 */
        if (list_empty(&area->free_list[migratetype]))
            continue;
        /* 獲取空閑鏈表中第一個結點所代表的連續頁框 */
        page = list_entry(area->free_list[migratetype].next,
                            struct page, lru);
        /* 將頁框從空閑鏈表中刪除 */
        list_del(&page->lru);
        /* 將首頁框的private設置為0 */
        rmv_page_order(page);
        area->nr_free--;
        /* 如果從更高級的頁框的鏈表中分配,這里會將多余的頁框放回伙伴系統的鏈表中,比如我們只需要2個頁框,但是這里是從8個連續頁框的鏈表分配給我們的,那其他6個就要拆分為2和4個分別放入鏈表中 */
        expand(zone, page, order, current_order, area, migratetype);
        /* 設置頁框的類型與migratetype一致 */
        set_freepage_migratetype(page, migratetype);
        return page;
    }

    return NULL;
}

  __rmqueue_smallest()函數中只會對migratetype類型的鏈表進行操作,並且會從需要的order值開始向上遍歷,直到成功分配連續頁框或者無法分配連續頁框為止,比如order為8,也就是需要連續的256個頁框,那會嘗試從order為8的空閑頁框塊鏈表中申請內存,如果失敗,order就會變為9,從連續512個頁框的空閑頁框塊鏈表中嘗試分配,如果還是失敗,order就會達到最大的10,然后從連續1024個頁框的空閑頁框塊鏈表中嘗試分配。這里需要注意,如果分配到內存的order與最初的order不相等,比如最初傳入的是8,而能夠成功分配的是10,那么就會對連續頁框進行拆分,這時候會拆為256,256,512這三塊連續頁框,把512放入order為9的free_list,把一個256放入order為8的free_list,剩余了一個256用於分配。

  而當__rm_queue_smallest()無法分配到頁框時,說明zone的mirgratetype類型的連續頁框不足以分配本次1 << order個連續頁框,那么就會調用__rmqueue_fallback()進行分配,在__rmqueue_fallback()函數中,主要根據fallbacks表,嘗試將其他migratetype類型的pageblock中的空閑頁移動到目標類型的mirgratetype類型的空閑頁框塊鏈表中,看代碼:

/* Remove an element from the buddy allocator from the fallback list */
/* 根據fallbacks數組中定義的優先級,從其他migratetype類型的鏈表中獲取連續頁框,返回第一個頁框的頁描述符 
 * start_migratetype是申請頁框時需要但是又缺少的類型
 */
static inline struct page *
__rmqueue_fallback(struct zone *zone, unsigned int order, int start_migratetype)
{
    struct free_area *area;
    unsigned int current_order;
    struct page *page;
    int migratetype, new_type, i;

    /* Find the largest possible block of pages in the other list */
    /* 遍歷不同order的鏈表,如果需要分配2個連續頁框,則會遍歷1024,512,256,128,64,32,16,8,4,2,1這幾個鏈表,注意這里是倒着遍歷的 */
    for (current_order = MAX_ORDER-1;
                current_order >= order && current_order <= MAX_ORDER-1;
                --current_order) {     
        for (i = 0;; i++) {      /* 遍歷order鏈表中對應fallbacks優先級的類型鏈表 */

            /* 根據fallbacks和i獲取migratetype,start_migratetype是申請頁框時需要的類型 */
            /*static int fallbacks[MIGRATE_TYPES][4] = {
             *    [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,     MIGRATE_RESERVE },
             *    [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,     MIGRATE_RESERVE },
             *#ifdef CONFIG_CMA
             *    [MIGRATE_MOVABLE]     = { MIGRATE_CMA,         MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
             *    [MIGRATE_CMA]         = { MIGRATE_RESERVE }, 
             *#else
             *    [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE,   MIGRATE_RESERVE },
             *#endif
             *    [MIGRATE_RESERVE]     = { MIGRATE_RESERVE }, 
             *#ifdef CONFIG_MEMORY_ISOLATION
             *    [MIGRATE_ISOLATE]     = { MIGRATE_RESERVE }, 
             *#endif
             *};
             */
            migratetype = fallbacks[start_migratetype][i];

            /* MIGRATE_RESERVE handled later if necessary */
            /* 這里不能分配MIGRATE_RESERVE類型的內存,這部分內存是保留使用,最后其他的migratetype都沒有內存可分配才會分配MIGRATE_RESERVE類型的內存 */
            if (migratetype == MIGRATE_RESERVE)
                break;

            /* 當前order的鏈表,current_order從10 ~ order */
            area = &(zone->free_area[current_order]);
            /* 鏈表為空,說明這個鏈表頁沒有內存 */
            if (list_empty(&area->free_list[migratetype]))
                continue;

            /* 有空余的內存,即將分配 */
            /* 從鏈表中獲取第一個節點,但是注意,這里分配的內存可能大於我們需要的數量(從其他order鏈表中獲取的連續頁框),之后會調用expand把多余的放回去 */
            page = list_entry(area->free_list[migratetype].next,
                    struct page, lru);
            area->nr_free--;

            /* 在當前start_migratetype中沒有足夠的頁進行分配時,則會將獲取到的migratetype類型的pageblock中的所有空閑頁框移動到start_migratetype中,返回獲取的頁框本來所屬的類型  
             * 只有系統禁止了page_group_by_mobility_disabled或者order > pageblock_order / 2,才會這樣做
             * 在調用前,page一定是migratetype類型的
             * 里面的具體做法是:
             * page是屬於migratetype類型的pageblock中的一個頁,然后函數中會根據page獲取其所在的pageblock
             * 從pageblock開始的第一頁遍歷到此pageblock的最后一頁
             * 然后根據page->_mapcount是否等於-1,如果等於-1,說明此頁在伙伴系統中,不等於-1則下一頁
             * 對page->_mapcount == -1的頁獲取order值,order值保存在page->private中,然后將這一段連續空閑頁框移動到start_type類型的free_list中
             * 對這段連續空閑頁框首頁設置為start_type類型,這樣就能表示此段連續空閑頁框都是此類型了,通過page->index = start_type設置
             * 繼續遍歷,直到整個pageblock遍歷結束,這樣整個pageblock中的空閑頁框都被移動到start_type類型中了
             */
            new_type = try_to_steal_freepages(zone, page,
                              start_migratetype,
                              migratetype);

            /* 從伙伴系統中拿出來,因為在try_to_steal_freepages已經將新的頁框放到了需要的start_mirgatetype的鏈表中
             * 並且此order並不一定是所需要order的上級,因為order是倒着遍歷了,比如我們需要32個MIGRATE_UNMOVABLE頁框,但是移動的是1024個MIGRATE_MOVABLE頁框到MIGRATE_UNMOVABLE的order=10的鏈表中。
             */
            list_del(&page->lru);
            /* 設置page->_mapcount = -1 並且 page->private = 0 */
            rmv_page_order(page);

            /* 如果有多余的頁框,則把多余的頁框放回伙伴系統中 */
            expand(zone, page, order, current_order, area,
                   new_type);

            /* 設置獲取的頁框的類型為新的類型,因為在try_to_steal_freepages()中cma類型是直接返回的,而其他類型都會在里面被設置,page->index = new_type
             * 到這里,page已經是一個2^oder連續頁框的內存段,之后就把它返回到申請者就好
             */
            set_freepage_migratetype(page, new_type);

            trace_mm_page_alloc_extfrag(page, order, current_order,
                start_migratetype, migratetype, new_type);

            return page;
        }
    }

    return NULL;
}

  此函數中主要就是從其他migratetype類型的空閑塊鏈表中獲取一個空閑塊,然后將此空閑塊所在的pageblock中的所有空閑頁都移動到當前migratetype類型的空閑塊鏈表中,不過只有當系統的page_group_by_mobility_disable被禁止時,或者order > pageblock_order / 2時,才允許這樣做(pageblock_order為最大頁的大小,或者1024)。

  整個快速分配大概就是這樣,如果申請1個頁框則會從每CPU高速緩存分配,如果申請的是多個頁框則從伙伴系統中分配。如果都不成功,那就進入慢速分配了。

  對於慢速分配,我也只做一個拋磚引玉,里面涉及的流程既長又復雜,涉及到內存壓縮(同步和異步)、直接內存回收和kswapd線程喚醒。這里就不做詳細說明了,如果要需要的朋友,可以跟我說,我看看要不要單獨拿一篇出來進行說明。

/* 慢速分配頁框 */
static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
    struct zonelist *zonelist, enum zone_type high_zoneidx,
    nodemask_t *nodemask, struct zone *preferred_zone,
    int classzone_idx, int migratetype)
{
    const gfp_t wait = gfp_mask & __GFP_WAIT;
    struct page *page = NULL;
    int alloc_flags;
    unsigned long pages_reclaimed = 0;
    unsigned long did_some_progress;
    enum migrate_mode migration_mode = MIGRATE_ASYNC;
    bool deferred_compaction = false;
    int contended_compaction = COMPACT_CONTENDED_NONE;

    /* order不能大於10或11 */
    if (order >= MAX_ORDER) {
        WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN));
        return NULL;
    }

    /* 調用者指定了GFP_THISNODE標志,表示不能進行內存回收
     * 上層調用者應當在指定了GFP_THISNODE失敗后,使用其他標志進行分配
     */
    if (IS_ENABLED(CONFIG_NUMA) &&
        (gfp_mask & GFP_THISNODE) == GFP_THISNODE)
        goto nopage;

restart:
    /* 如果調用者的標志沒有禁止kswapd線程標志,則會喚醒這個線程用於頁框回收,這里會喚醒所有node結點中的kswap線程,每個node都有一個自己的kswap線程
     * 里面會遍歷zonelist鏈表的zone區域,只有zone區域的空閑頁框數量低於高警戒值才會喚醒zone對應的node的kswapd線程
     */
    if (!(gfp_mask & __GFP_NO_KSWAPD))
        wake_all_kswapds(order, zonelist, high_zoneidx,
                preferred_zone, nodemask);

    /* 根據傳入標志確定其他的一些標志 
     * 這里會默認使用min閥值進行內存分配
     * 如果gfp_mask是GFP_ATOMIC,那么這個alloc_flags應該是__GFP_HIGH | __GFP_HARDER
     *
     * 如果標記有__GFP_MEMALLOC,或者
     * 處於軟中斷中,並且當前進程被設為允許使用保留內存,或者
     * 不在中斷中,並且當前進程 被設置為允許使用保留內存 或者是正在被oom的進程
     * 那么就會標記ALLOC_NO_WATERMARKS,表示忽略閥值進行分配
     */
    alloc_flags = gfp_to_alloc_flags(gfp_mask);

    /* 如果不受CPUSET的限制,則找出優先用於分配的管理區 */
    if (!(alloc_flags & ALLOC_CPUSET) && !nodemask) {
        struct zoneref *preferred_zoneref;
        preferred_zoneref = first_zones_zonelist(zonelist, high_zoneidx,
                NULL, &preferred_zone);
        classzone_idx = zonelist_zone_idx(preferred_zoneref);
    }

rebalance:
    /* This is the last chance, in general, before the goto nopage. */
    /* 這里會用min閥值再次嘗試獲取頁框,如果這次嘗試還是沒申請到頁框,就要走漫長的步驟了 */
    page = get_page_from_freelist(gfp_mask, nodemask, order, zonelist,
            high_zoneidx, alloc_flags & ~ALLOC_NO_WATERMARKS,
            preferred_zone, classzone_idx, migratetype);
    
    if (page)
        goto got_pg;

    /* Allocate without watermarks if the context allows */
    /* 如果標記了不關注閥值進行分配,這樣會有可能使用預留的內存進行分配 
     * 如果標記有__GFP_MEMALLOC,或者
     * 處於軟中斷中,並且當前進程被設為允許使用保留內存,或者
     * 不在中斷中,並且當前進程 被設置為允許使用保留內存 或者是正在被oom的進程
     * 那么就會進行這種分配
     * 而平常可能用到的GFP_ATOMIC,則不是這種分配,GFP_ATOMIC會在zone_watermark_ok()中通過降低閥值進行判斷,它不會用到預留的內存
     */
    if (alloc_flags & ALLOC_NO_WATERMARKS) {
        /* 這里就是還是沒有獲取到,嘗試忽略閥值再次進行獲取頁框 */
        zonelist = node_zonelist(numa_node_id(), gfp_mask);

        /* 嘗試獲取頁框,這里不調用zone_watermark_ok(),也就是忽略了閥值,使用管理區預留的頁框 
         * 當沒有獲取到時,如果標記有__GFP_NOFAIL,則進行循環不停地分配,直到獲取到頁框
         */
        page = __alloc_pages_high_priority(gfp_mask, order,
                zonelist, high_zoneidx, nodemask,
                preferred_zone, classzone_idx, migratetype);
        if (page) {
            goto got_pg;
        }
    }

    /* Atomic allocations - we can't balance anything */
    /* 還是沒有分配到,如果調用者不希望等待獲取內存,就返回退出 */
    if (!wait) {
        WARN_ON_ONCE(gfp_mask & __GFP_NOFAIL);
        goto nopage;
    }

    /* Avoid recursion of direct reclaim */
    /* 調用者本身就是內存回收進程,不能執行后面的內存回收流程,是為了防止死鎖 */
    if (current->flags & PF_MEMALLOC)
        goto nopage;

    /* Avoid allocations with no watermarks from looping endlessly */
    if (test_thread_flag(TIF_MEMDIE) && !(gfp_mask & __GFP_NOFAIL))
        goto nopage;

    /* 通過壓縮看能否有多余的頁框,通過頁面遷移實現,這里的內存壓縮是異步模式,要進入這里,有個前提就是分配內存的標志中必須允許阻塞(__GFP_WAIT),大多數情況分配都允許阻塞
     * 只會對MIRGATE_MOVABLE和MIGRATE_CMA類型的頁進行移動,並且不允許阻塞
     * 對zonelist的每個zone進行一次異步內存壓縮
     */
    page = __alloc_pages_direct_compact(gfp_mask, order, zonelist,
                    high_zoneidx, nodemask, alloc_flags,
                    preferred_zone,
                    classzone_idx, migratetype,
                    migration_mode, &contended_compaction,
                    &deferred_compaction);
    if (page)
        goto got_pg;

    /* Checks for THP-specific high-order allocations */
    if ((gfp_mask & GFP_TRANSHUGE) == GFP_TRANSHUGE) {
        if (deferred_compaction)
            goto nopage;

        if (contended_compaction == COMPACT_CONTENDED_LOCK)
            goto nopage;

        if (contended_compaction == COMPACT_CONTENDED_SCHED
            && !(current->flags & PF_KTHREAD))
            goto nopage;
    }

    /* 設置第二次內存壓縮為輕同步模式,當第一次內存壓縮后還是沒有分配到足夠頁框時會使用
     * 輕同步內存壓縮兩有一種情況會發生
     * 在申請內存時內存不足,通過第一次異步內存壓縮后,還是不足以分配連續頁框后
     * 1.明確禁止處理透明大頁的時候,可以進行輕同步內存壓縮
     * 2.如果是內核線程,可以進行輕同步內存壓縮(即使沒有禁止處理透明大頁的情況)
     */
    if ((gfp_mask & GFP_TRANSHUGE) != GFP_TRANSHUGE ||
                        (current->flags & PF_KTHREAD))
        migration_mode = MIGRATE_SYNC_LIGHT;

    /* Try direct reclaim and then allocating */
    /* 進行直接內存回收 */
    page = __alloc_pages_direct_reclaim(gfp_mask, order,
                    zonelist, high_zoneidx,
                    nodemask,
                    alloc_flags, preferred_zone,
                    classzone_idx, migratetype,
                    &did_some_progress);
    if (page)
        goto got_pg;

    /* 還是沒有分配到內存 */
    if (!did_some_progress) {
        /* 如果是文件系統操作,並且允許重試,就是這次一定要分配到內存 */
        if (oom_gfp_allowed(gfp_mask)) {
            if (oom_killer_disabled)
                goto nopage;
            /* Coredumps can quickly deplete all memory reserves */
            if ((current->flags & PF_DUMPCORE) &&
                !(gfp_mask & __GFP_NOFAIL))
                goto nopage;
            /* 殺死其他進程后再嘗試,里面會使用high閥值進行嘗試分配
             * 是希望通過殺死進程獲取比較多的內存?
             */
            page = __alloc_pages_may_oom(gfp_mask, order,
                    zonelist, high_zoneidx,
                    nodemask, preferred_zone,
                    classzone_idx, migratetype);
            if (page)
                goto got_pg;

            /* 分配禁止失敗 */
            if (!(gfp_mask & __GFP_NOFAIL)) {
                /* 要求的數量太多,沒辦法 */
                if (order > PAGE_ALLOC_COSTLY_ORDER)
                    goto nopage;
                /* 是從DMA區域要內存,實在沒太多內存 */
                if (high_zoneidx < ZONE_NORMAL)
                    goto nopage;
            }

            goto restart;
        }
    }

    /* 回收到了一部分,這里檢查是否繼續嘗試回收 */
    pages_reclaimed += did_some_progress;
    if (should_alloc_retry(gfp_mask, order, did_some_progress,
                        pages_reclaimed)) {
        /* 需要,這里會阻塞一段時間,然后重試 */
        /* Wait for some write requests to complete then retry */
        wait_iff_congested(preferred_zone, BLK_RW_ASYNC, HZ/50);
        goto rebalance;
    } else {
        /* 如果是在內核線程中,或者分配的不是透明大頁的情況下,會對zonelist中的每個zone進行輕同步內存壓縮,否則還是異步模式
         * 此模式下允許進行大多數操作的阻塞,但不會對隔離出來需要移動的臟頁進行回寫操作,也不會等待正在回寫的臟頁回寫完成,會阻塞去獲取鎖
         * 回收的數量保存在did_some_progress中,有可能回收到了頁框,但是並不夠分配
         */
        page = __alloc_pages_direct_compact(gfp_mask, order, zonelist,
                    high_zoneidx, nodemask, alloc_flags,
                    preferred_zone,
                    classzone_idx, migratetype,
                    migration_mode, &contended_compaction,
                    &deferred_compaction);
        if (page)
            goto got_pg;
    }

nopage:
    /* 沒有分配到內存 */
    warn_alloc_failed(gfp_mask, order, NULL);
    return page;
got_pg:
    /* 分配到了 */
    if (kmemcheck_enabled)
        kmemcheck_pagealloc_alloc(page, order, gfp_mask);

    return page;
}

  不過需要注意的是,慢速內存分配中使用的是min閥值, 而在慢速內存分配過程中,還有一個oom分配,這里面會使用zone的high閥值進行內存分配,實際上也就是殺死一些進程,讓zone的空閑內存達到high閥值。

 

最后整理一下,如果一次分配,從開始到最后都沒有成功,所走的路徑是:

  1. 遍歷zonelist,從zonelist中獲取一個zone
  2. 檢查zone如果分配后,空閑頁框是否會低於allow_low
  3. 對此zone回收一些文件映射頁和slab使用的頁
  4. 再次檢查zone如果分配后,空閑頁框是否會低於allow_low
  5. 嘗試從此zone分配頁框(1個頁優先從每CPU高速緩存分配,連續頁框優先從需要的類型(migratetype)分配,如果不行再從其他migratetype分配)
  6. free_order小於11的情況, free_order++, 再次嘗試第5步.如果free_order大於等於11, 則走第7步
  7. 跳到第1步,遍歷zonelist結束則到下一步
  8. 再重新遍歷zonelist一次,如果重新遍歷過則到下一步
  9. 進入慢速分配
  10. 喚醒所有kswapd內核線程
  11. 再次嘗試一次1~7步驟進行分配
  12. 如果有ALLOC_NO_WATERMARKS,則嘗試分配預留的內存
  13. 進行異步內存壓縮,然后嘗試分配內存
  14. 嘗試調用__alloc_pages__direct_reclaim()進行內存回收,然后嘗試分配內存
  15. 使用oom殺掉oom_score較大的進程,每個進程都有一個oom_score(在/proc/PID/oom_score)
  16. 嘗試輕同步內存壓縮,然后嘗試分配內存
  17. 壓縮后再次嘗試1~7步驟進行分配

 

補充

pageblock

  在內核中,會被內存分為一小塊一小塊,這每個一小塊叫做一個pageblock,在pageblock中所有頁框都是空閑的情況下,它們都是屬於同一種類型的頁框。在內核里頁框被分為以下幾種類型:

/* 這幾個鏈表主要用於內存壓縮時做判斷 */
enum {
    MIGRATE_UNMOVABLE,         /* 頁框內容不可移動,在內存中位置必須固定,無法移動到其他地方,核心內核分配的大部分頁面都屬於這一類。 */
    MIGRATE_RECLAIMABLE,         /* 頁框內容可回收,不能直接移動,但是可以回收,因為還可以從某些源重建頁面,比如映射文件的數據屬於這種類別,kswapd會按照一定的規則回收這類頁面。 */
    MIGRATE_MOVABLE,             /* 頁框內容可移動,屬於用戶空間應用程序的頁屬於此類頁面,它們是通過頁表映射的,因此我們只需要更新頁表項,並把數據復制到新位置就可以了
                                 * 當然要注意,一個頁面可能被多個進程共享,對應着多個頁表項。 
                                 */
    MIGRATE_PCPTYPES,             /* 用來表示每CPU頁框高速緩存的數據結構中的鏈表的遷移類型數目 */
    MIGRATE_RESERVE = MIGRATE_PCPTYPES,     
#ifdef CONFIG_CMA
    MIGRATE_CMA,                   /* 預留一段的內存給驅動使用,但當驅動不用的時候,伙伴系統可以分配給用戶進程用作匿名內存或者頁緩存。而當驅動需要使用時,就將進程占用的內存通過回收或者遷移的方式將之前占用的預留內存騰出來,供驅動使用。 */
#endif   
#ifdef CONFIG_MEMORY_ISOLATION
    MIGRATE_ISOLATE,            /* 不能從這個鏈表分配頁框,因為這個鏈表專門用於NUMA結點移動物理內存頁,將物理內存頁內容移動到使用這個頁最頻繁的CPU */
#endif
    MIGRATE_TYPES
};

  在每個內存管理區的伙伴系統中,都維護着以這些類型區分開的空閑頁框段鏈表,如下:

/* 內存管理區描述符 */
struct zone {

  ....../* 標識出管理區中的空閑頁框塊,用於伙伴系統 */
    /* MAX_ORDER為11,分別代表包含大小為1,2,4,8,16,32,64,128,256,512,1024個連續頁框的鏈表,具體見下面 */
    struct free_area    free_area[MAX_ORDER];

    ......

}



/* 伙伴系統的一個塊,描述1,2,4,8,16,32,64,128,256,512或1024個連續頁框的塊 */
struct free_area {
    /* 指向這個塊中所有空閑小塊的第一個頁描述符,這些小塊會按照MIGRATE_TYPES類型存放在不同指針里 */
    struct list_head    free_list[MIGRATE_TYPES];
    /* 空閑小塊的個數 */
    unsigned long        nr_free;
};

  在每個管理區的伙伴系統初始化過程中,會把所有空閑頁框都設置為MIGRATE_MOVABLE類型,並放入到對應管理區的free_list[MIGRATE_MOVABLE]這個鏈表中,而根據釋放頁框函數(見linux內存源碼分析 - 伙伴系統(釋放頁框))可以看出,在初始化過程中,釋放的所有頁框幾乎都放到了free_area[10].free_list[MIGRATE_MOVABLE]這個鏈表中來,其他鏈表幾乎為空。在這里思考一下,在初始化伙伴系統完成后,除了內核正在使用的頁框(這些頁框類型為MIGRATE_UNMOVABLE),其他所有空閑頁框都被設置為可移動頁框(MIGRATE_MOVABLE),其他類型的頁框幾乎都為空。再去看看內核分配頁框函數,里面提到了一個fallbacks:

static int fallbacks[MIGRATE_TYPES][4] = {
    [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,     MIGRATE_RESERVE },
    [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,     MIGRATE_RESERVE },
#ifdef CONFIG_CMA
    [MIGRATE_MOVABLE]     = { MIGRATE_CMA,         MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
    [MIGRATE_CMA]         = { MIGRATE_RESERVE }, /* Never used */
#else
    [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE,   MIGRATE_RESERVE },
#endif
    [MIGRATE_RESERVE]     = { MIGRATE_RESERVE }, /* Never used */
#ifdef CONFIG_MEMORY_ISOLATION
    [MIGRATE_ISOLATE]     = { MIGRATE_RESERVE }, /* Never used */
#endif
};

   在這個表中,表明了不同類型的頁框空缺時,會從其他那些類型的頁框中獲取。舉個例子,在伙伴系統初始化完成后,幾乎所有空閑頁框都是MIGRATE_MOVABLE。注意這一行

[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,     MIGRATE_RESERVE },

  而當我們映射一個文件時,申請頁框時會表明我們需要申請MIGRATE_RECLAIMABLE類型的頁框,但是這種類型的頁框空缺的,伙伴系統會先去MIGRATE_UNMOVABLE類型的鏈表中獲取一段連續空閑頁框,如果MIGRATE_UNMOVABLE類型也為空缺的,會再往下,從MIGRATE_MOVABLE類型獲取一段連續頁框,這種類型在初始化之后是最多的,在獲取這段連續頁框成功后,會把這段頁框設置為我們需要的MIGRATE_RECLAIMABLE類型。這整個過程對整個內存壓縮有着至關重要的關系,說到這個從其他類型的free_list中獲取頁框,就必須要說到pageblock,pageblock中對內存壓縮來說最重要的就兩個變量:

#ifdef CONFIG_HUGETLB_PAGE

#ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE

/* Huge page sizes are variable */
extern 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)

  pageblock_nr_pages和pageblock_order,pageblock_order有兩種情況:2的pageblock_order次方等於大頁大小,或者pageblock_order=MAX_ORDER。也就是一個pageblock的大小要么等於大頁的大小,要么等於一個free_list[10]鏈表中一個1024個連續頁框的大小。在默認不是用大頁的情況下,一個pageblock就是1024個連續頁框大小,內核會將內存簡單地分為一個一個pageblock,每個pageblock為1024個連續頁框,0~1023為一個pageblock,1024~2047為一個pageblock。當從其他類型頁框的free_list中拿去一段連續頁框時,是以一個pageblock為單位的,也就是說,這個pageblock中的所有頁框都會被設置為我們需要的頁框類型,如下圖:

  這里只用了order為10的連續空閑頁框鏈表說例子。而如果在一個pageblock中有部分頁已經被使用的情況,這種情況是有可能發生的,因為在檢索目標類型的free_are[ORDER]時,ORDER是從大到小進行搜索的,如果ORDER=10的時候沒有空閑的連續頁框供於使用,則會到ORDER=9的free_list[目標類型]進行查找,這樣的話就有可能導致一個pageblokc中可能有512個頁框是處於使用中的,而另外512個是空閑的。在這種情況下, 如果此塊pageblock被移動到新類型的頁框數量大於等於此塊pageblock的頁框總數量的一半,則會把pageblock設置為新的類型,否則不設置。這樣就會有一種情況,就是當一些頁正在使用時,這些頁所屬的pageblock被移動到了其他類型的伙伴系統中,會導致pageblock的類型與正在使用的頁的類型不一致。內核為了解決這種情況,會在頁釋放的時候檢查釋放的頁是否與所屬pageblock的類型一致,不一致則將釋放頁的類型更改為與pageblock一致的類型。

  從以上總結出來,當從不同類型的伙伴系統中獲取連續頁框時,是一pageblock為單位,而每個pageblock標記了這塊pageblock都屬於同一種類型,即使有些正在使用的頁不能在pageblock移動時立即更改類型,但這些頁也會在被釋放時檢查是否與所屬pageblock一致,並進行更改。這里我們具體看看代碼,我們從__rmqueue函數進行分析:

/* 從伙伴系統中獲取2的order次方個頁框,返回第一個頁框的描述符
 * zone: 管理區描述符
 * order: 需要頁面的2的次方數
 * migratetype: 從此類型中獲取,這時傳入的時需求的頁框類型
 */
static struct page *__rmqueue(struct zone *zone, unsigned int order,
                        int migratetype)
{
    struct page *page;

retry_reserve:
    /* 直接從migratetype類型的鏈表中獲取了2的order次方個頁框 */
    page = __rmqueue_smallest(zone, order, migratetype);

    /* 如果page為空,沒有在需要的migratetype類型中分配獲得頁框,說明當前需求類型(migratetype)的頁框沒有空閑,會根據fallback數組中定義好的優先級從其他類型的頁框中獲取頁框 */
    if (unlikely(!page) && migratetype != MIGRATE_RESERVE) {
        /* 根據fallbacks數組從其他migratetype類型的鏈表中獲取內存 */
        page = __rmqueue_fallback(zone, order, migratetype);

         /* 從其他類型的空閑頁框鏈表中也沒有獲得頁框,設定為MIGRATE_RESERVE類型,從保留頁框里嘗試獲取 */
        if (!page) {
            /* 定義從頁框屬性為MIGRATE_RESERVE的空閑鏈表中查找 */
            migratetype = MIGRATE_RESERVE;
            /* 重試嘗試從MIGRATE_RESERVE類型的鏈表中找出空閑內存 */
            goto retry_reserve;
        }
    }

    trace_mm_page_alloc_zone_locked(page, order, migratetype);
    return page;
}

  可以看到在_rmqueue中,當無法從目標類型的鏈表中獲取連續頁框時,就會調用__rmqueue_fallback(),嘗試從其他類型的pageblock中將空閑頁框移動到目錄類型的空閑頁框塊鏈表中,進入到__rmqueue_fallback()中看看:

/* 根據fallbacks數組中定義的優先級,從其他migratetype類型的鏈表中獲取連續頁框,返回第一個頁框的頁描述符 
 * start_migratetype是申請頁框時需要但是又缺少的類型
 */
static inline struct page *
__rmqueue_fallback(struct zone *zone, unsigned int order, int start_migratetype)
{
    struct free_area *area;
    unsigned int current_order;
    struct page *page;
    int migratetype, new_type, i;

    /* Find the largest possible block of pages in the other list */
    for (current_order = MAX_ORDER-1;
                current_order >= order && current_order <= MAX_ORDER-1;
                --current_order) {     /* 遍歷不同order的鏈表,如果需要分配2個連續頁框,則會遍歷1024,512,256,128,64,32,16,8,4,2,1這幾個鏈表,注意這里是倒着遍歷的 */
        for (i = 0;; i++) {      /* 遍歷order鏈表中對應fallbacks優先級的類型鏈表 */

            /* 根據fallbacks和i獲取migratetype,start_migratetype是申請頁框時需要的類型 */
            /*static int fallbacks[MIGRATE_TYPES][4] = {
             *    [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,     MIGRATE_RESERVE },
             *    [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,     MIGRATE_RESERVE },
             *#ifdef CONFIG_CMA
             *    [MIGRATE_MOVABLE]     = { MIGRATE_CMA,         MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
             *    [MIGRATE_CMA]         = { MIGRATE_RESERVE }, 
             *#else
             *    [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE,   MIGRATE_RESERVE },
             *#endif
             *    [MIGRATE_RESERVE]     = { MIGRATE_RESERVE }, 
             *#ifdef CONFIG_MEMORY_ISOLATION
             *    [MIGRATE_ISOLATE]     = { MIGRATE_RESERVE }, 
             *#endif
             *};
             */
            migratetype = fallbacks[start_migratetype][i];

            /* MIGRATE_RESERVE handled later if necessary */
            /* 這里不能分配MIGRATE_RESERVE類型的內存,這部分內存是保留使用,最后其他的migratetype都沒有內存可分配才會分配MIGRATE_RESERVE類型的內存 */
            if (migratetype == MIGRATE_RESERVE)
                break;

            /* 當前order的鏈表,current_order從10 ~ order */
            area = &(zone->free_area[current_order]);
            /* 鏈表為空,說明這個鏈表頁沒有內存 */
            if (list_empty(&area->free_list[migratetype]))
                continue;

            /* 有空余的內存,即將分配 */
            /* 從鏈表中獲取第一個節點,但是注意,這里分配的內存可能大於我們需要的數量(從其他order鏈表中獲取的連續頁框),之后會調用expand把多余的放回去 */
            page = list_entry(area->free_list[migratetype].next,
                    struct page, lru);
            area->nr_free--;

            /* 在當前start_migratetype中沒有足夠的頁進行分配時,則會從migratetype獲取order或者pageblock相等數量的頁框放到start_migratetype中的order鏈表中,返回獲取的頁框本來所屬的類型  
             * 代碼中不建議把過低order數量的頁框進行移動,最小移動單位是一個pageblock,它的大小是1024個頁框或者一個大頁的大小。如果order過小則可能不會移動
             */
            new_type = try_to_steal_freepages(zone, page,
                              start_migratetype,
                              migratetype);

            /* 從伙伴系統中拿出來,因為在try_to_steal_freepages已經將新的頁框放到了需要的start_mirgatetype的鏈表中
             * 並且此order並不一定是所需要order的上級,因為order是倒着遍歷了,比如我們需要32個MIGRATE_UNMOVABLE頁框,但是移動的是1024個MIGRATE_MOVABLE頁框到MIGRATE_UNMOVABLE的order=10的鏈表中。
             */
            list_del(&page->lru);
            /* 設置page->_mapcount = -1 並且 page->private = 0 */
            rmv_page_order(page);

            /* 如果有多余的頁框,則把多余的頁框放回伙伴系統中 */
            expand(zone, page, order, current_order, area,
                   new_type);

            /* 設置獲取的頁框的類型為新的類型,因為在try_to_steal_freepages()中cma類型是直接返回的,而其他類型都會在里面被設置,page->index = new_type
             * 到這里,page已經是一個2^oder連續頁框的內存段,之后就把它返回到申請者就好
             */
            set_freepage_migratetype(page, new_type);

            trace_mm_page_alloc_extfrag(page, order, current_order,
                start_migratetype, migratetype, new_type);

            return page;
        }
    }

    return NULL;
}

  在__rmqueue_fallback(),根據fallbacks表,遍歷后面類型的空閑頁框塊鏈表,從找到一塊合適的,然后獲取對應的pageblock,將pageblock中所有空閑頁框都移動到當前類型的空閑頁框鏈表中,主要通過調用try_to_steal_freepages()函數實現將其他類型的pageblock中所有空閑頁框移動到當前類型的空閑頁框鏈表中:

/* 在當前start_migratetype中沒有足夠的頁進行分配時,則會將獲取到的migratetype類型的pageblock中的所有空閑頁框移動到start_migratetype中,返回獲取的頁框本來所屬的類型  
 * 在調用前,page一定是migratetype類型的
 * 里面的具體做法是:
 * page是屬於migratetype類型的pageblock中的一個頁,然后函數中會根據page獲取其所在的pageblock
 * 從pageblock開始的第一頁遍歷到此pageblock的最后一頁
 * 然后根據page->_mapcount是否等於-1,如果等於-1,說明此頁在伙伴系統中,不等於-1則下一頁
 * 對page->_mapcount == -1的頁獲取order值,order值保存在page->private中,然后將這一段連續空閑頁框移動到start_type類型的free_list中
 * 對這段連續空閑頁框首頁設置為start_type類型,這樣就能表示此段連續空閑頁框都是此類型了,通過page->index = start_type設置
 * 繼續遍歷,直到整個pageblock遍歷結束,這樣整個pageblock中的空閑頁框都被移動到start_type類型中了
 */
static int try_to_steal_freepages(struct zone *zone, struct page *page,
                  int start_type, int fallback_type)
{
    /* page是當前遍歷到的migratetype當中order頁的首頁描述符,並不是我們需要的migratetype中的頁
     * order是當前遍歷到的migratetype當中order,並不是當前需要分配的order
     */
    int current_order = page_order(page);

    /* 如果是CMA類型則不做處理 */
    if (is_migrate_cma(fallback_type))
        return fallback_type;

    /* Take ownership for orders >= pageblock_order */
    /* 如果當前需要的order值大於默認一個內存塊的order值(這個值為MAX_ORDER-1或者大頁的大小),就算出需要多少塊pageblock才能達到order,然后把這些pageblock都設置為start_type 
     * 這種情況發生在pageblock_order等於大頁的大小,而內核配置了CONFIG_FORCE_ORDER,導致order >= pageblock_order
     */
    if (current_order >= pageblock_order) {
        /* 計算出需要的pageblock的塊數,然后將每一塊都設置為需要的類型,這種情況下並沒有把它們從舊類型的伙伴系統移到需要類型的伙伴系統中,在外面函數會將其移出來 */
        change_pageblock_range(page, current_order, start_type);
        return start_type;
    }

    /* 如果order大於pageblock_order的一半,或者類型是MIGRATE_RECLAIMABLE,或者內核關閉了頁可遷移的特性,則從此頁所屬的mirgatetype和order鏈表中獲取頁框放到start_type中 
     * 如果oder小於pageblock_order / 2並且start_type != MIGRATE_RECLAIMABLE並且page_group_by_mobility_disabled == false,就不會移動頁框。
     */
    if (current_order >= pageblock_order / 2 ||
        start_type == MIGRATE_RECLAIMABLE ||
        page_group_by_mobility_disabled) {
        int pages;

        /* 這個page所在的pageblock必定屬於fallback_type類型
         * 將這個page所在的pageblock中所有空閑頁框移動到start_type類型的free_list鏈表中,order不變,返回移動的頁數量,但是已經在使用的頁會被跳過,並且這些已經被使用的頁不會被更改為新的類型
         * 具體做法:
         * 從pageblock開始的第一頁遍歷到此pageblock的最后一頁
         * 然后根據page->_mapcount是否等於-1,如果等於-1,說明此頁在伙伴系統中,不等於-1則下一頁
         * 對page->_mapcount == -1的頁獲取order值,order值保存在page->private中,然后將這一段連續空閑頁框移動到start_type類型的free_list中
         * 對這段連續空閑頁框首頁設置為start_type類型,這樣就能表示此段連續空閑頁框都是此類型了,通過page->index = start_type設置
         * 繼續遍歷,直到整個pageblock遍歷結束,這樣整個pageblock中的空閑頁框都被移動到start_type類型中了
         */
        pages = move_freepages_block(zone, page, start_type);

        /* Claim the whole block if over half of it is free */
        /* 如果這塊pageblock中的頁數量大於pageblock的頁數量的一半,則設置這塊pageblock為新的migratetype類型,如果小於,則不會把此pageblock設置為新的類型
         * 如果不將pageblock設置為新的類型,會導致一種情況: 空閑頁的migratetype類型與pageblock的migratetype類型不一致
         * 對於這種情況,在這些正在使用的塊被釋放時,會被檢查是否與所屬pageblock的類型一致,不一致則會設置為一致
         * 一個zone的每個pageblock的狀態占4位,保存在zone->pageblock_flags指向的一個位圖中
         */
        if (pages >= (1 << (pageblock_order-1)) ||
                page_group_by_mobility_disabled) {
            set_pageblock_migratetype(page, start_type);
            return start_type;
        }
    }
    /* 返回是從哪個migratetype中移動的頁框 */
    return fallback_type;

  move_freepages_block():

/* 將page所在的pageblock中所有空閑頁框移動到新的類型鏈表中
 * 比如一段連續頁框塊,order為8,那么就會移動zone->free_area[8].free_list[新的類型]這個空閑頁框塊中
 */
int move_freepages_block(struct zone *zone, struct page *page,
                int migratetype)
{
    unsigned long start_pfn, end_pfn;
    struct page *start_page, *end_page;

    /* 根據page
     * 將start_pfn設置為page所在pageblock的起始頁框
     * 將end_pfn設置為page所在pageblock的結束頁框
     * start_page指向start_pfn對應的頁描述符
     * end_page指向end_page對應的頁描述符
     */
    start_pfn = page_to_pfn(page);
    start_pfn = start_pfn & ~(pageblock_nr_pages-1);
    start_page = pfn_to_page(start_pfn);
    end_page = start_page + pageblock_nr_pages - 1;
    end_pfn = start_pfn + pageblock_nr_pages - 1;

    /* Do not cross zone boundaries */
    /* 檢查開始頁框是否屬於zone中,如果不屬於,則用page作為開始頁框 
     * 因為有可能pageblock中一半在上一個zone中,一半在本zone中
     */
    if (!zone_spans_pfn(zone, start_pfn))
        start_page = page;
    /* 同上如果結束頁框不屬於zone,不過這里直接返回0 */
    if (!zone_spans_pfn(zone, end_pfn))
        return 0;

    /* 將此pageblock中的空閑頁框全部移動到新的migratetype類型的伙伴系統鏈表中 */
    return move_freepages(zone, start_page, end_page, migratetype);
}

  move_freepages():

/* 將此段頁框中的空閑頁框移動到新的migratetype類型的伙伴系統鏈表中 */
int move_freepages(struct zone *zone,
              struct page *start_page, struct page *end_page,
              int migratetype)
{
    struct page *page;
    unsigned long order;
    int pages_moved = 0;

#ifndef CONFIG_HOLES_IN_ZONE
    VM_BUG_ON(page_zone(start_page) != page_zone(end_page));
#endif
    /* 遍歷這組頁框 */
    for (page = start_page; page <= end_page;) {
        /* Make sure we are not inadvertently changing nodes */
        VM_BUG_ON_PAGE(page_to_nid(page) != zone_to_nid(zone), page);

        /* 檢查頁框和頁框號是否屬於內存,如果不正確則跳過 */
        if (!pfn_valid_within(page_to_pfn(page))) {
            page++;
            continue;
        }

        /* 如果頁框不在伙伴系統中則跳到下一頁,通過判斷page->_mapcount是否等於-128 */
        if (!PageBuddy(page)) {
            page++;
            continue;
        }

        /* 獲取此頁框的order號,保存在page->private中 */
        order = page_order(page);
        /* 從伙伴系統中拿出來,並放到新的migratetype類型中的order鏈表中 */
        list_move(&page->lru,
              &zone->free_area[order].free_list[migratetype]);
        /* 將這段空閑頁框的首頁設置為新的類型page->index = migratetype */
        set_freepage_migratetype(page, migratetype);
        /* 跳過此order個頁框數量 */
        page += 1 << order;
        /* 記錄拿出來了多少個頁框 */
        pages_moved += 1 << order;
    }
    /* 返回一共拿出來的頁框 */
    return pages_moved;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM