首发于术道经纬
Linux中的memory model

Linux中的memory model

内存模型(memory model)描述的是如何对物理内存进行管理的一种方法,本文将讨论memory model在Linux操作系统中是如何一步步演进的。

flat memory model - FLATMEM

从今天的视角来看,早期的系统物理内存通常不大(比如几十MB),那时的Linux使用平坦内存模型(flat memory model)来管理物理内存就足够有效了(关于flat memory model请参考这篇文章)。一个page frame用一个struct page结构体表示(参考这篇文章),整个物理内存可以用一个由所有struct page构成的数组mem_map表示,而经过页表查找得到的PFN(Page Frame Number,也叫PPN,关于PPN请参考这篇文章),正好可以拿来做这个数组的下标(或者说索引),pfn_to_page()函数就是专门干这个的:

#define __pfn_to_page(pfn)  (mem_map + ((pfn) - ARCH_PFN_OFFSET)) 

使用数组索引的方法固然简单高效,但是同其他使用索引的方法一样(比如页表中使用虚拟地址的某部分bit位作为索引),它都要求被索引的对象在物理内存里是连续分布的(数组就是这样的嘛)。对于页表,为了满足这个连续性的要求,即便不存在的映射关系也需要有对应的entry,这会浪费页表本身占用的内存空间,对此采用的方法是使用多级页表(请参考这篇文章)。而对于FLATMEM来说,如果其管理的的物理内存本身是连续的还好说,如果不连续的话,那么中间一部分物理地址是没有对应的物理内存的,形象的说就像是一个个洞(hole),这会浪费mem_map数组本身占用的内存空间。

discontiguous memory model - DISCONTIGMEM

那什么情况下物理内存是不连续的?那就要说到后来出现的NUMA(参考这篇文章)。为了有效的管理NUMA模式下的物理内存,一种被称为不连续内存模型(discontiguous memory model)的实现于1999年被引入Linux系统。在这种memory model中,NUMA中的每个node用一个叫pglist_data的结构体表示。假设每个node里的物理内存都是连续分布的,那么每个pg_data_t都对应一个mem_map数组。

应对不连续物理内存的问题似乎是解决了,可是现在你给我一个物理page的地址,使用DISCONTIGMEM的话,我怎么知道这个page是属于哪个node的呢,PFN中可没有包含node编号啊。pfn_to_page()之前干的活多轻松啊,就是索引下数组就得到数组元素struct page了,现在PFN和page之间的对应关系不是那么直接了,pfn_to_page的任务就开始重起来了。办法总比困难多,尤其是软件,即便是曲线救国,总还是能找到一些通过PFN找到node中的page的方法,最多看起来不是那么优雅,比如这样:

#define __pfn_to_page(pfn)            \ 
({    unsigned long __pfn = (pfn);        \ 
    unsigned long __nid = arch_pfn_to_nid(__pfn);  \ 
    NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\ 
})

此外,在DISCONTIGMEM中,每个node都有一套完整的内存管理框架,这要是node数目多的话,那这个开销就大了。

sparse memory model - SPARSEMEM

为了解决DISCONTIGMEM存在的这些弊端,没过几年,一种新的稀疏内存模型(sparse memory model)被召唤了出来。在SPARSEMEM中,被管理的物理内存由一个个任意大小的section(用struct mem_section表示)构成,因此整个物理内存可视为一个mem_section数组。每个mem_section包含一个间接指向struct page数组的指针。为了更有效的实现PFN和struct page之间的转换,PFN中的几个高位bit被用作mem_section数组的索引。

看到这里,你有没有觉得,这种实现方法和前面提到的多级页表的思路其实是非常相似的,两级页表里第一级是PGD,使用VPN(关于VPN同样参考这篇文章)的高位bit做索引,第二级是PTE,使用VPN的低位bit做索引。SPARSEMEM里呢,第一级是struct mem_section数组,使用PFN/PPN的高位bit做索引,第二级是struct page数组,使用PFN/PPN的低位bit做索引,代码实现是这样:

#define __pfn_to_page(pfn)  (vmemmap + (pfn)) 

其中vmemmap的实现和具体的处理器体系有关,以ARM64为例,

#define vmemmap ((struct page *)VMEMMAP_START - SECTION_ALIGN_DOWN(memstart_addr >> PAGE_SHIFT))                 

获知一个page frame所在的section依然靠的是这篇文章提到的page flags。

static inline unsigned long page_to_section(const struct page *page)
{
	return (page->flags >> SECTIONS_PGSHIFT) & SECTIONS_MASK;
}

内存模型和NUMA

内存模型表现的是CPU视角的内存分布情况,而NUMA体现的是CPU和内存的相对位置关系,两者本没有直接的对应关系。但在DISCONTIGMEM中,两者的耦合性是比较强的,而SPARSEMEM分离了这种耦合性。在SPARSEMEM中,不再是一个node对应一个mem_map数组,

#ifdef CONFIG_FLAT_NODE_MEM_MAP	/* means !SPARSEMEM */
	struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION

而是一个section对应一个mem_map数组

struct mem_section {
	unsigned long section_mem_map;
        ...
}

采用SPARSEMEM的系统可以是NUMA的,也可以是UMA的。


参考:

原创文章,转载请注明出处。

编辑于 09-05

文章被以下专栏收录