关于内存实验的总结

关于统计内存信息实验的总结


实验要求

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 

/* 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在判断一个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中间的某个物理页,灰色部分表示的是不可用的物理内存:

也就是说,只要某个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日

[email protected]

你可能感兴趣的:(linux)