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 做详细讨论。
平坦内存模型即只有一个内存节点,并且内存结点内部的物理地址是连续的,即不存在空洞。对于这种内存模型,内核是通过一个全局的 struct page *mem_map 数组来存放所有page,从而达到管理物理页的目的。在这里我们不做详细讨论。
非连续内存是指有多个内存节点,内存结点之间的物理地址可能不连续,但节点内部的物理地址是连续的。对于这种内存模型,每个结点对应的pg_data_t结构体中的node_mem_map成员来指向page数组,在此我们也不做详细讨论。因为相对于sparse memory内存模型,前两种内存模型结构比较简单,我们重点分析最后一种内存模型。
引入稀疏内存模型的原因和热插拔有关,将理论上最大支持的物理内存以section为单位进行分段,每个section对应的物理内存实际上可能存在也可能不存在,每进行一次热插拔,都会对相应的section进行调整,如设置一些标志,分配或回收section_memmap等。
文件
/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
/* 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)
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。
根据是否设置了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结构体。
直接从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();两个函数完成。
该函数主要是完成两个任务:
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结构体。
先贴代码,去除了一些不相关的部分:
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;
}
对于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标志的。
搞清楚了配置了CONFIG_SPARSEMEM_VMEMMAP
的sparse memory模型,实验的实现就比较简单了:
之前遇到的为什么到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日