關於統計內存信息實驗的總結
實驗要求
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以後的部分是一個巨大空洞。
內核同樣提供了用於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在判斷一個pfn或section是否可用時會用到:
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中間的某個物理頁,灰色部分表示的是不可用的物理內存:
也就是說,只要某個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的說明。這裏我們不對此函數做深入探討,只通過下圖給出其執行結果:
之後對每一個可用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圖示:
如此一來,對於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的結果:
可以發現,0xd0000~0xffffff所有頁面都是不可用的,即這其中包含的6個section都是不可用的。
爲什麼其他section都可用?以葉框範圍0xc8000~0xcffff這個section爲例,因爲其包含了可用的region(0xcafff000,0xcaffffff)(BIOS-e820提供,之後內核又將其中一部分變爲reserved,但仍有部分可用),因此整個section都是可用的。
2016年11月22日