關於內存實驗的總結

關於統計內存信息實驗的總結


實驗要求

linux內核在管理內存時,是一個struct page 對應一個物理頁框,struct page *mem_map管理着系統中的所有page,可以看作一個page的數組。現在的要求就是獲得你的系統中有多少個struct page ,看看是否和你的物理內存相對應;其中處於空閒的有多少個;處於PG_reserved狀態的有多少個;處於PG_swapcache狀態的有多少個;被共享的有多少個。

linux內核使用 struct page 結構來保存物理頁框的各種信息。而對 struct page 的組織以及物理頁框的組織都是和內存模型密切相關的。對linux進行配置時有三種內存模型可供選擇。

  • flat memory
  • discontigous memory
  • sparse memory

所以,這個實驗要結合具體的內存模型去討論。下面分別介紹這三種內存模型,並對 sparse memory 做詳細討論。


1.flat memory

平坦內存模型即只有一個內存節點,並且內存結點內部的物理地址是連續的,即不存在空洞。對於這種內存模型,內核是通過一個全局的 struct page *mem_map 數組來存放所有page,從而達到管理物理頁的目的。在這裏我們不做詳細討論。


2.discontigous memory

非連續內存是指有多個內存節點,內存結點之間的物理地址可能不連續,但節點內部的物理地址是連續的。對於這種內存模型,每個結點對應的pg_data_t結構體中的node_mem_map成員來指向page數組,在此我們也不做詳細討論。因爲相對於sparse memory內存模型,前兩種內存模型結構比較簡單,我們重點分析最後一種內存模型。


3.sparse memory

引入稀疏內存模型的原因和熱插拔有關,將理論上最大支持的物理內存以section爲單位進行分段,每個section對應的物理內存實際上可能存在也可能不存在,每進行一次熱插拔,都會對相應的section進行調整,如設置一些標誌,分配或回收section_memmap等。


3.0 關於section的基本介紹

文件
/arch/x86/include/asm/sparsemem.h中定義了MAX_PHYSMEM_BITS,SECTION_SIZE_BITS等宏,即定義了該內存模型支持的最大物理內存以及一個section的大小。

#define SECTION_SIZE_BITS      27 /* matt - 128 is convenient right now */
#define MAX_PHYSADDR_BITS      44
#define MAX_PHYSMEM_BITS       46

可以通過SECTION_SIZE_BITS簡單計算得出一個section大小爲2^27B即128MB

通過PAGE_SHIFT
可知一個section包含了2^15也即0x8000個頁。

#define PAGE_SHIFT              12

使用以下兩個宏定義了section的總數:

mmzone.h

#define NR_MEM_SECTIONS     (1UL << SECTIONS_SHIFT)

page-flags-layout.h

#ifdef CONFIG_SPARSEMEM
#include <asm/sparsemem.h>

/* SECTION_SHIFT    #bits space required to store a section # */
#define SECTIONS_SHIFT  (MAX_PHYSMEM_BITS - SECTION_SIZE_BITS)

#endif /* CONFIG_SPARSEMEM */

即在編譯時,就已經將section的總數按照最大可支持的物理內存設置好,但實際的物理內存一般情況下都會遠小於可支持的最大物理內存,這時只需將超出實際物理內存的section作爲一般的空洞來處理即可。

例如下圖2號section對應的物理內存不存在或不可用(以後統稱爲不可用),則對其標記爲不存在(具體如何標記在3.1中section結構和3.2初始化中會講到)。對於n+1號及以後的各個section,爲超出實際物理內存最大編號的部分,做同樣處理,因此可以理解n+1號section以後的部分是一個巨大空洞。

image

內核同樣提供了用於section號和pfn之間相互轉換的宏:

#define pfn_to_section_nr(pfn) ((pfn) >> PFN_SECTION_SHIFT)
#define section_nr_to_pfn(sec) ((sec) << PFN_SECTION_SHIFT)

3.1 用到的數據結構&變量

3.1.1 mem_section結構

include/linux/mmzone.h

struct mem_section {
    unsigned long section_mem_map;
    unsigned long *pageblock_flags;
#ifdef CONFIG_PAGE_EXTENSION
    struct page_ext *page_ext;
    unsigned long pad;
#endif
};

這裏我們主要關心section_mem_map成員,它不僅包含了section對應的mem_map信息,還包含一些其他的信息:

/*
 * We use the lower bits of the mem_map pointer to store
 * a little bit of information.  There should be at least
 * 3 bits here due to 32-bit alignment.
 */
#define SECTION_MARKED_PRESENT  (1UL<<0)
#define SECTION_HAS_MEM_MAP (1UL<<1)
#define SECTION_MAP_LAST_BIT    (1UL<<2)
#define SECTION_MAP_MASK    (~(SECTION_MAP_LAST_BIT-1))

section_mem_map的bit_0表示該section對應的物理內存當前是否可用,bit_1表示該section是否有對應的mem_map。
SECTION_HAS_MEM_MAP在判斷一個pfnsection是否可用時會用到:

static inline int valid_section(struct mem_section *section)
{
    return (section && (section->section_mem_map & SECTION_HAS_MEM_MAP));
}

static inline int valid_section_nr(unsigned long nr)
{
    return valid_section(__nr_to_section(nr));
}

static inline int pfn_valid(unsigned long pfn)
{
        /*先由pfn得到其所在的section號,在通過判斷section是否可用來得出pfn是否可用*/
    if (pfn_to_section_nr(pfn) >= NR_MEM_SECTIONS)
        return 0;
    return valid_section(__nr_to_section(pfn_to_section_nr(pfn)));
}

可見,最終都會通過判斷section_nr對應的section結構體是否存在以及section結構體section_mem_map成員中的SECTION_HAS_MEMMAP標誌是否置位來判斷。

這裏涉及到一個問題:__nr_to_section()的實現

mmzone.h

static inline struct mem_section *__nr_to_section(unsigned long nr)
{
    if (!mem_section[SECTION_NR_TO_ROOT(nr)])
        return NULL;
    return &mem_section[SECTION_NR_TO_ROOT(nr)][nr & SECTION_ROOT_MASK];
}

這就引出了用來維護section結構體的mem_section型全局二維數組mem_section


3.1.2 mem_section全局數組

根據是否設置了CONFIG_SPARSEMEM_EXTREME選項,mem_section的定義略有不同,代碼如下:

mmzone.h

#ifdef CONFIG_SPARSEMEM_EXTREME
#define SECTIONS_PER_ROOT       (PAGE_SIZE / sizeof (struct mem_section))
#else
#define SECTIONS_PER_ROOT   1
#endif

#define SECTION_NR_TO_ROOT(sec) ((sec) / SECTIONS_PER_ROOT)
#define NR_SECTION_ROOTS    DIV_ROUND_UP(NR_MEM_SECTIONS, SECTIONS_PER_ROOT)
#define SECTION_ROOT_MASK   (SECTIONS_PER_ROOT - 1)

#ifdef CONFIG_SPARSEMEM_EXTREME
extern struct mem_section *mem_section[NR_SECTION_ROOTS];
#else
extern struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT];
#endif

對於一般的稀疏內存,直接定義了一個二維數組,同時由於SECTIONS_PER_ROOT == 1,實際上和一維數組沒什麼差別,重點在於所有的section結構體一起申請。

對於超稀疏內存,和一般稀疏內存的區別在於,可能絕大多數的section對應的物理內存是不可用的(PC機就是如此,實際物理內存和理論支持的相差太多),如果直接定義一個全局數組顯然會造成很大的內存浪費。在這種情況下,內核採取的做法是,只定義一個mem_section*型的全局數組,在進行初始化的時候再根據實際情況去決定是否分配用於存放mem_struction結構的數組。這裏我們在3.2.1中詳細討論。

總而言之,就是將所有的section劃分爲若干個ROOT,通過section_nr / SECTIONS_PER_ROOT即可得到section_nr的ROOT。再通過section_nr & SECTION_ROOT_MASK得到section_nr在ROOT中的下標,即可定位對應的mem_section結構體。


3.2 初始化內存模型

直接從paging_init看起,此前內存的探測已經完成,已經確定有哪些可用的內存段(regions),以start_pfn~end_pfn來表示一個region。

arch/x86/mm/init_64.c

void __init paging_init(void)
{
    sparse_memory_present_with_active_regions(MAX_NUMNODES);
    sparse_init();
    //...
}

mem_section的初始化及mem_map的建立通過sparse_memory_present_with_active_regions(MAX_NUMNODES);和sparse_init();兩個函數完成。


3.2.1 sparse_memory_present_with_active_regions()

該函數主要是完成兩個任務:

1.完成mem_section數組的申請(只有超稀疏內存纔有此步驟)。

2.設置mem_section結構的SECTION_MARKED_PRESENT標誌,後面幾乎所有的初始化操作都要以這個標誌爲依據。

mm/page_alloc.c

void __init sparse_memory_present_with_active_regions(int nid)
{
    unsigned long start_pfn, end_pfn;
    int i, this_nid;

        /*對內存探測得出的每個region調用memory_present()*/
    for_each_mem_pfn_range(i, nid, &start_pfn, &end_pfn, &this_nid)
        memory_present(this_nid, start_pfn, end_pfn);
}

mm/sparse.c

/* Record a memory area against a node. */
void __init memory_present(int nid, unsigned long start, unsigned long end)
{
    unsigned long pfn;

    start &= PAGE_SECTION_MASK;
    mminit_validate_memmodel_limits(&start, &end); /*確定可用內存的最大最小pfn*/
    for (pfn = start; pfn < end; pfn += PAGES_PER_SECTION) { //以section爲單位
        unsigned long section = pfn_to_section_nr(pfn); //通過pfn計算section號
        struct mem_section *ms;

                /*對於一般的稀疏內存,sparse_index_init()爲空。
                對於超稀疏內存:
                已經定義了一個全局的mem_section *mem_section[NR_SECTION_ROOTS]的數組,
                這裏爲可用物理內存對應ROOT的mem_section數組申請空間
                */
        sparse_index_init(section, nid);
        /*建立section和node的對應關係*/
        set_section_nid(section, nid);

                /*通過section_nr得到對應的mem_section結構體,看了後邊mem_section初始化,
                再結合__nr_to_section()的實現就會明白如何轉換的,感興趣的自己看看*/
        ms = __nr_to_section(section);
        /*設置section的標識SECTION_MARKED_PRESENT,表示該section是存在的*/
        if (!ms->section_mem_map)
            ms->section_mem_map = sparse_encode_early_nid(nid) |
                            SECTION_MARKED_PRESENT;
    }
}

sparse_index_init(section, nid)完成主要工作:

mm/sparse.c

#ifdef CONFIG_SPARSEMEM_EXTREME
/*
具體的分配細節,有興趣的可以看看
*/
static struct mem_section noinline __init_refok *sparse_index_alloc(int nid)
{
    struct mem_section *section = NULL;
    unsigned long array_size = SECTIONS_PER_ROOT *
                   sizeof(struct mem_section);

    if (slab_is_available()) {
        if (node_state(nid, N_HIGH_MEMORY))
            section = kzalloc_node(array_size, GFP_KERNEL, nid);
        else
            section = kzalloc(array_size, GFP_KERNEL);
    } else {
        section = memblock_virt_alloc_node(array_size, nid);
    }

    return section;
}

/*主要看看這個就行了*/
static int __meminit sparse_index_init(unsigned long section_nr, int nid)
{
    unsigned long root = SECTION_NR_TO_ROOT(section_nr);//確定section_nr所在的root
    struct mem_section *section;
        /*如果root對應的mem_section數組已存在,則返回“已存在”*/
    if (mem_section[root])
        return -EEXIST;

    // 調用sparse_index_alloc()申請分配內存塊,用於存放root的mem_section數組.
    section = sparse_index_alloc(nid);

    if (!section)
        return -ENOMEM;

    /*將新建的mem_section數組首地址存入與mem_section[root]*/
    mem_section[root] = section;

    return 0;
}
#else

到此爲止,mem_section數組已經申請完成,需要注意的一點是,申請的空間大小固定爲SECTIONS_PER_ROOT * sizeof(struct mem_section),即分配mem_section數組的時候時每SECTIONS_PER_ROOT個section在一起分配的。
這會導致出現一種情況,如果一個regin的開始start_pfn或結束end_pfn不是剛巧在SECTIONS_PER_ROOT個section的整數倍處的話,那麼就會對一部分不可用section分配mem_section結構體。如下圖這種情況,某個region的start_pfn是ROOT2中間的某個物理頁,灰色部分表示的是不可用的物理內存:
image

也就是說,只要某個ROOT中有可用的物理頁,那麼連帶着整個ROOT的section都會被分配相應的mem_section結構體。而只有像圖中ROOT1這種不包含可用物理頁的,纔不會爲其中的section分配mem_section結構體。


3.2.2 sparse_init()

先貼代碼,去除了一些不相關的部分:

mm/sparse.c

void __init sparse_init(void)
{
    unsigned long pnum;
    struct page *map;
        //...
    int size2;
    struct page **map_map;
        //...
        /*申請一個NR_MEM_SECTIONS(理論最大section數)大小的page*數組*/
    size2 = sizeof(struct page *) * NR_MEM_SECTIONS;
    map_map = memblock_virt_alloc(size2, 0);

    /*爲每個可用section分配一個page* mem_map, 並暫時用map_map數組來保存mem_map的地址*/
    alloc_usemap_and_memmap(sparse_early_mem_maps_alloc_node,
                            (void *)map_map);
    //...
    /*遍歷所有的section*/
    for (pnum = 0; pnum < NR_MEM_SECTIONS; pnum++) {
        /*對於沒有設置SECTION_MARKED_PRESENT標誌的section,直接跳過*/
        if (!present_section_nr(pnum))
            continue;
         //...
        /*在mam_map數組中取第pnum個page*作爲初始化pnum號section的參數(用於其section_mem_map成員的初始化)*/
        map = map_map[pnum];
        if (!map)
            continue;

        /*初始化pnum號section的mem_section結構體*/
        sparse_init_one_section(__nr_to_section(pnum), pnum, map,
                                usemap);
    }
        //...

        /*section和其對應的mem_map映射關係初始化完成,釋放map_map數組*/
    memblock_free_early(__pa(map_map), size2);
}

sparse_init()中有兩個主要的函數,alloc_usemap_and_mem_map()sparse_init_one_section()

alloc_usemap_and_mem_map()函數用於分配mem_map數組,並通過函數指針指定了實際的分配函數,這事因爲usemap和mem_map的分配都是通過alloc_usemap_mem_map()進行的,只不過使用的分配函數不同,這裏爲簡化問題省略了usemap的說明。這裏我們不對此函數做深入探討,只通過下圖給出其執行結果:

image

之後對每一個可用section調用sparse_init_one_section(),內容比較簡單:

mm/sparse.c

static int __meminit sparse_init_one_section(struct mem_section *ms,
        unsigned long pnum, struct page *mem_map,
        unsigned long *pageblock_bitmap)
{
    if (!present_section(ms))
        return -EINVAL;

    /將section_mem_map成員的“地址段”清零/
    ms->section_mem_map &= ~SECTION_MAP_MASK;

    /*將之前保存在map_map中的mem_map的地址和section的其實pfn一起編碼得到section_mem_map的“地址段”,
    從section_mem_map成員獲得mem_map的地址可以通過sparse_decode_mem_map()函數;
    同時,設置SECTION_HAS_MEM_MAP標誌*/
    ms->section_mem_map |= sparse_encode_mem_map(mem_map, pnum) |
                            SECTION_HAS_MEM_MAP;
    ms->pageblock_flags = pageblock_bitmap;

    return 1;
}

3.2.3 vmemmap

對於sparse memory,如果設置了CONFIG_SPARSEMEM_VMEMMAP,則對於mem_map的處理又稍有不同,實現主題在mm/sparse-vmemmap.c文件中的

void __init sparse_mem_maps_populate_node(struct page **map_map,
                                           unsigned long pnum_begin,
                                           unsigned long pnum_end,
                                           unsigned long map_count, int nodeid)
{
//...
}

主要工作是建立vmemmap數組中的各個元素地址與實際存儲單元的頁表映射。

vmemmap數組:內核空間中的VMEMMAP_START開始的1TB的空間用於虛擬內存映射,是一個虛擬的page數組。
該函數完成的功能就是對於所有可用的section,假設某個可用section的編號位pnum,則位vmemmap[pnum]分配實際的存儲單元,並建立頁表映射,另外,將vmemmap中元素的虛擬地址用於mem_section->memsection_mem_map成員的初始化。

vmemmap圖示:

image

如此一來,對於sparse memory,pfn_to_page以及page_to_pfn的操作邊簡化了。不需要先先定位mem_section結構再定位mem_map數組,而是直接通過pfn就可以定位page結構在虛擬內存中的位置(即vmemmap[pfn]),在通過頁表映射就可以找到實際的page結構。

但同時,又保存了一般的sparse memory特性,這事因爲,在判斷一個pfn是否可用時,還是需要使用mem_section結構中的SECTION_HAS_MEM_MAP標誌的。


4 回到實驗

搞清楚了配置了CONFIG_SPARSEMEM_VMEMMAP的sparse memory模型,實驗的實現就比較簡單了:

  • 方法1:遍歷所有section,對於可用section,則統計其頁面信息即可。
  • 方法2:for(pfn = 0~max),直接判斷pfn_valid(pfn),如果頁面可用,則通過vmemmap[pfn]找到其page結構,統計信息。

其他

之前遇到的爲什麼到0x40000號頁就不能訪問其page結構體的問題,0x40000出現的原因:0x38000 ~ 0x3ffff號頁是一個section,0x40000 ~ 0x47fff是其後一個section,而且是不可用的,於是就沒有爲其分配page數組。

至於爲什麼不可用,可以參照e820map的結果:

image

可以發現,0xd0000~0xffffff所有頁面都是不可用的,即這其中包含的6個section都是不可用的。

爲什麼其他section都可用?以葉框範圍0xc8000~0xcffff這個section爲例,因爲其包含了可用的region(0xcafff000,0xcaffffff)(BIOS-e820提供,之後內核又將其中一部分變爲reserved,但仍有部分可用),因此整個section都是可用的。


2016年11月22日

[email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章