FLATMEM

Untitled

简单的线性映射,将物理页和一个mem_map数组进行一一映射,适用于非NUMA架构,并且内存都是连续的。可以看到,只需要简单的计算就可以在pfn和struct page之间转换。这种模型的缺点就是内存存在空洞的时候会导致mem_map空间太大,造成内存浪费。

// 定义了第一个page frame的offset,也就是mem_map的第一个位置,指向的是
// 物理地址的第几个页,两者之间存在一个offset
#ifndef ARCH_PFN_OFFSET
#define ARCH_PFN_OFFSET		(0UL)
#endif
// 全局的page frame数组mem_map
#define __pfn_to_page(pfn)	(mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page)	((unsigned long)((page) - mem_map) + \\
				 ARCH_PFN_OFFSET)

DISCONTIGMEM

Untitled

基本思路和FLATMEM一致,只是将多个不连续的内存拆分成多组,每一组内存都是连续的,Linux 在2.6.25后彻底删除了,解决思路和多级页表很像。

SPARSEMEM

Untitled

将内存按照128M进行切割,每128M是一个Section,Section内的内存是连续的,和struct page是一一映射,每一个Section使用一个struct mem_section的结构表示,其内部section_mem_map用来表示struct page数组,维护了这个Section内所有的struct page。

struct mem_section {
	unsigned long section_mem_map;
	struct mem_section_usage *usage;
};

struct mem_section_usage {
#ifdef CONFIG_SPARSEMEM_VMEMMAP
	DECLARE_BITMAP(subsection_map, SUBSECTIONS_PER_SECTION);
#endif
	/* See declaration of similar field in struct zone */
	unsigned long pageblock_flags[0];
};

为什么section_mem_map不是struct page*类型,而是一个unsigned long呢? 这是因为Linux使用这个字段来额外存了一些其他的信息。

/*
 * We use the lower bits of the mem_map pointer to store
 * a little bit of information.  The pointer is calculated
 * as mem_map - section_nr_to_pfn(pnum).  The result is
 * aligned to the minimum alignment of the two values:
 *   1. All mem_map arrays are page-aligned.
 *   2. section_nr_to_pfn() always clears PFN_SECTION_SHIFT
 *      lowest bits.  PFN_SECTION_SHIFT is arch-specific
 *      (equal SECTION_SIZE_BITS - PAGE_SHIFT), and the
 *      worst combination is powerpc with 256k pages,
 *      which results in PFN_SECTION_SHIFT equal 6.
 * To sum it up, at least 6 bits are available on all architectures.
 * However, we can exceed 6 bits on some other architectures except
 * powerpc (e.g. 15 bits are available on x86_64, 13 bits are available
 * with the worst case of 64K pages on arm64) if we make sure the
 * exceeded bit is not applicable to powerpc.
 */
enum {
	SECTION_MARKED_PRESENT_BIT,
	SECTION_HAS_MEM_MAP_BIT,
	SECTION_IS_ONLINE_BIT,
	SECTION_IS_EARLY_BIT,
#ifdef CONFIG_ZONE_DEVICE
	SECTION_TAINT_ZONE_DEVICE_BIT,
#endif
	SECTION_MAP_LAST_BIT,
};
#define SECTION_MAP_MASK		(~(BIT(SECTION_MAP_LAST_BIT) - 1))
// 所以在拿到一个mem_section后,获取其struct page指针需要将部分bits去掉。
static inline struct page *__section_mem_map_addr(struct mem_section *section)
{
	unsigned long map = section->section_mem_map;
	map &= SECTION_MAP_MASK;
	return (struct page *)map;
}

让我们来进一步看看他是如何编码的,这里巧妙的对mem_map进行了编码,主要是两个部分,一个是利用部分bit来保存一些flag,另外一个则是section_mem_map的地址是减去了当前mem_section所对应的pfn,所以在用这个字段的时候就可以直接用全局的pfn,而不是mem_section内的局部pfn了。

static void __meminit sparse_init_one_section(struct mem_section *ms,
		unsigned long pnum, struct page *mem_map,
		struct mem_section_usage *usage, unsigned long flags)
{
	ms->section_mem_map &= ~SECTION_MAP_MASK;
	ms->section_mem_map |= sparse_encode_mem_map(mem_map, pnum)
		| SECTION_HAS_MEM_MAP | flags;
	ms->usage = usage;
}

/*
 * Subtle, we encode the real pfn into the mem_map such that
 * the identity pfn - section_mem_map will return the actual
 * physical page frame number.
 */
// section_mem_map并不是真正的指向struct page的地址,而是减去了section_nr_to_pfn(pnum)
// 这样的好处就是用mem_map + pfn(全局pfn) 就可以得到真正的mem_map地址。
static unsigned long sparse_encode_mem_map(struct page *mem_map, unsigned long pnum)
{
	unsigned long coded_mem_map =
		(unsigned long)(mem_map - (section_nr_to_pfn(pnum)));
	BUILD_BUG_ON(SECTION_MAP_LAST_BIT > PFN_SECTION_SHIFT);
	BUG_ON(coded_mem_map & ~SECTION_MAP_MASK);
	return coded_mem_map;
}

Linux又进一步将mem_section切成一个个root,一个root下面会有SECTIONS_PER_ROOT个struct mem_section。

// Linux将Section分成一个个root,一个root下,有SECTIONS_PER_ROOT个Section
// 这里是2^8(2^12 / 2^4) = 256个secion,
#define SECTIONS_PER_ROOT       (PAGE_SIZE / sizeof (struct mem_section))

每一个mem_section是128M是2^27次方,64位系统用四级页表的化总内存是2^46次方,用五级页表则需要2^52次方。这样就可以计算出系统最大的mem_section数量(NR_MEM_SECTIONS),然后和SECTIONS_PER_ROOT相除,向上取整就可以得到整个系统的mem_section root的数量。

// Section大小 2^27 = 128M
#define SECTION_SIZE_BITS	27 /* matt - 128 is convenient right now */
#define MAX_PHYSMEM_BITS	(pgtable_l5_enabled() ? 52 : 46)
// 内存最大2^46次方(4级页表),一个Section 2^27
// 因此NR_MEM_SECTIONS就等于最大的Section数量了
#define SECTIONS_SHIFT	(MAX_PHYSMEM_BITS - SECTION_SIZE_BITS)
#define NR_MEM_SECTIONS		(1UL << SECTIONS_SHIFT)
// 向上取整,得到section root的数量
#define NR_SECTION_ROOTS	DIV_ROUND_UP(NR_MEM_SECTIONS, SECTIONS_PER_ROOT)