linux内核memory model

一、背景
  操作系统的一个重要功能就是管理计算机中的各种硬件资源,比如说CPU、内存、显存、串口等,其中内存是这些资源中很珍贵的部分,所以为了能把内存管理好做了很多工作。硬件内存架构是不停地发展变化的,内核中管理内存的模型也在跟着硬件的变化而不停地演进。当前linux内核中有三种模型,分别是:flat memory model,Discontiguous memory model和sparse memory model。本文的参考代码是linux5.0,为了清晰地分析内存模型,先介绍相关的一些基本概念,具体如下:

1、什么是PFN:
  linux为了方便把物理内存管理起来,内核中把物理内存在逻辑上划分为一块块大小为page size的page frame,同时每个page frame都对应一个编号,称为PFN。其中page size是内核的配置以及硬件体系结构决定的,比如常见的page size为4K。根据PFN就可以算出这个page frame对应的物理地址,这样CPU就能通过地址总线访问这些物理页了。

  怎么计算物理地址的PFN呢,物理地址和PFN是一一对应的,比如page size为4K时,把物理地址左移12位就得到了PFN,可参考如下代码:

include/linux/pfn.h
#define PFN_PHYS(x)	((phys_addr_t)(x) << PAGE_SHIFT)
#define PHYS_PFN(x)	((unsigned long)((x) >> PAGE_SHIFT))

2、什么是struct page:
  在逻辑上把物理内存划分好后,为了跟踪每个page frame的状态,linux内核为每一个page frame配备了一个"struct page",用于跟踪对应物理页的状态,比如区分具体物理页保存的是进程的数据,还是内核的代码段或数据段。同样还得区分物理页是否是空闲的,如果物理页不存放任何有用的数据,那么它就是空闲的,当它用于存放进程数据、充当cache时,它就不是空闲状态。struct page会把物理页的各种信息都保存起来。

3、为什么要有内存模型了:
  有了PFN和struct page,还得把他们俩联系起来了,这就是pfn_to_page()与page_to_pfn()的工作,意思很明显了,PFN和page之间的相互转换就涉及到了内存模型。

  那为什么又是三种了,计算机刚起步的时候,物理内存都是连续的一整块,而且地址一般都是从0开始的,同时对于每个processor来说都一样,那时候生活是简单且美好的,只需要根据物理内存的大小分配一个struct page的数组,数组的index就是对应的PFN,看起一切都是那么美好。但时代在变化,计算机系统更是日新月异,内存早已不是那个简单单纯的它了,物理地址不一定从0开始了,也不一定连续了,内存中间可能有空洞了,而且还支持热插拔了。所以随着时间的推移,出现三种内存模型来应对硬件上的变化,下面详细地介绍每一个具体的模型。

二、flat memory model
  这是最简单的内存模型,所管理的物理内存就是连续的,没有空洞的,同时对于每个CPU来说都是一样的。在linux内核使用此模型时,会分配一个全局的struct page的数组mem_map,每个数组元素对应一个page frame,PFN和数组的下标是一种线性的对应关系,主要与ARCH_PFN_OFFSET有关,如果正好ARCH_PFN_OFFSET为0,数组的index就和PFN一一对应起来了,如下图所示:
在这里插入图片描述
  其实mem_map对应的数组是根据实际的物理内存大小动态分配,在flat memory model模型下,pfn_to_page()与page_to_pfn()的实现:

include/asm-generic/memory_model.h

#if defined(CONFIG_FLATMEM)
#define __pfn_to_page(pfn)	(mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page)	((unsigned long)((page) - mem_map) + \
				 ARCH_PFN_OFFSET)

二、Discontiguous memory model
  时间回溯到1999年,为了linux能在NUMA的硬件机器上更好地运行,做了很多工作,DISCONTIGMEM model就是其中部分成果,它就是用于处理不连续的物理内存。DISCONTIGMEM model引入了memory node的概念,同时memory node是NUMA内存管理的基础。从内存管理的角度来说,每个memory node都拥有一个独立的内存管理子系统,比如都有空闲页列表,使用页列表,最近最少访问的信息以及页使用的一些统计数据。在linux中,memory node是用struct pglist_data来表示,其中包含了此node特有的memory map。DISCONTIGMEM model前提是要求每个memory node内的内存在物理上是连续的,struct pglist_data中包含了一个类似flat memory map的struct page数组,从memory node内部来看,PFN和page之的转换与flat的一样,这样就完美地解决了内存不连续的问题。

  DISCONTIGMEM model必须要提供一种方式,即把包含在memory node中内存的PFN转换为相应的struct page,同样,它也要能根据struct page得到相应的PFN。当要把PFN转换为struct page时,首先从PFN中获取到memory node,然后通过"PFN - NODE_DATA(nid)->node_start_pfn"得到PFN在具体memory node中的page_offset,再根据"NODE_DATA(nid)->node_mem_map + page_offset"获得PFN对应的struct page;struct page中的flag中包含了memory node信息,故从struct page转换为PFN就简单了,请参考具体代码,不在赘述。

  DISCONTIGMEM model有个致命的弱点,它无法支持内存的热插拔。NUMA node的内存数量级对于支持热插拔来说太大了,但拆分NUMA node的话可能又会增加很多内存碎片和开销。每个NUMA node都有自己独立的内存管理子系统,对应的就是每个子系统都有相应的开销,拆分NUMA node会进一步加剧这些开销,这也是DISCONTIGMEM model无法支持热插拔的原因。DISCONTIGMEM model和NUMA model这两者本质上是不同的,但DISTONCTIGMEM是NUMA model的部分成果出现的,这种天然的关系导致两者之间的耦合度太高了。

  以下是假设有三个memory node对应的DISTONTIGMEM model的示意图
在这里插入图片描述
如下是DISCONTIGMEM model对应的代码

#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);\
})

#define __page_to_pfn(pg)						\
({	const struct page *__pg = (pg);					\
	struct pglist_data *__pgdat = NODE_DATA(page_to_nid(__pg));	\
	(unsigned long)(__pg - __pgdat->node_mem_map) +			\
	 __pgdat->node_start_pfn;					\
})

三、SPARSEMEM model
  计算机的硬件在不停的更新,物理内存的架构也在不断发展,linux内核的Memory model也要进行演化,使它能够更好地管理物理内存。在linux刚开始的时候,物理内存是起始物理地址为0的连续的、简单的线性序列,大小也就几个M。每个物理内存页,在mem_map数组中都对应一个条目,数组的index就对应物理页的PFN。后来出现了NUMA机器,也出现不连续的物理内存,伴随着也就出现了DISCONTIGMEM model。当前内存又开始支持热插拔了,且memory node中的内存也不一定连续了,DISCONTIGMEM model又无法满足这些情况了,SPARSEMEM model就涌现出来了。

  在物理内存不连续的机器上,SPARSSEMEM希望最终能够替代DISCONTIGMEM。相对于DISCONTIGMEM来说,SPARSEMEM与CONFIG_NUMA是完全隔离的,NUMA与DISCONTIGMEM是耦合在一起的。SPARSEMEM还有另外一个优点,它也不要求NUMA node中的内存一定是连续的。

  SPARSEMEM用一系列的section集合来抽象内存,其中section的大小是由具体的体系结构定义的,一般是1G,在linux内核中,用struct mem_section表示一个section。SPARSEMEM的section集合有两种方式管理,一种是静态方式,即静态定义了一个二维数组,用于管理section集合;另外一种是运行时动态分配内存的方式,这是通过是否定义定义CONFIG_SPARSEMEM_EXTREME来决定的,如下:

#ifdef CONFIG_SPARSEMEM_EXTREME
struct mem_section **mem_section;
#else
struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT]
	____cacheline_internodealigned_in_smp;
#endif

  从如上代码可看到,section的id分为两部分,一部分是ROOT,另外一部分是PER_ROOT,静态定义数组的方式不仅需要一大块物理连续的内存,同时对于特别稀疏的系统来说也是非常浪费内存的,所以定义CONFIG_SPARSEMEM_EXTREME宏,使用二重指针的是后续的patch更新的方案。二重指针的方式需要动态分配内存,而且SPARSEMEM又必须在系统启动很靠前的地方初始化,有的系统这时还是不支持动态分配内存的,故静态数组的方式还是保留的。

  当enable SPARSEMEM时,pfn_to_page()和page_to_pfn()同样有两种不同的实现方式,一种是经典方式,另一种是vmemmap的方式,它们是通过是否定义CONFIG_SPARSEMEM_VMEMMAP来区分的。
1、经典方式,非vmemmap方式:

#define __page_to_pfn(pg)					\
({	const struct page *__pg = (pg);				\
	int __sec = page_to_section(__pg);			\
	(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec)));	\
})

#define __pfn_to_page(pfn)				\
({	unsigned long __pfn = (pfn);			\
	struct mem_section *__sec = __pfn_to_section(__pfn);	\
	__section_mem_map_addr(__sec) + __pfn;		\
})

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;
}

  从如上代码可知PFN是分为两部分,section id以及相对于section_mem_map的偏移量,PFN转换为struct page时,首先从PFN中截取到section id,然后通过section id在section集合中获得对应的section,再根据section->section_mem_map得到此section对应的struct page数组的起始虚拟地址,最后根据"__section_mem_map_addr(__sec) + __pfn"得到PFN对应struct page地址。

  page转换为PFN时,page对应的section id是保存在page的flags中的,通过section id就获得了对应的section,最后
PFN=page - section->section_mem_map。把section id保存在page的flags的方法的最大问题是page->flags中的bit数目不一定够用,因为这个flags中包含了太多东西,各种page flag,node id,zone id,现在有增加了一个section_nr。更通用的方式就出来了,即vmemmap方式,后面我们会详细介绍。

  PFN既包含了section id,又保存了相对section->section_mem_map的偏移量,那么"page=section_mem_map + pfn"时不就多了section id吗?其实在初始化时,section_mem_map保存的是减去section id的虚拟地址,如下:

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)));
	...
	return coded_mem_map;
}

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

  section_mem_map指向的此section对应page数组的起始虚拟地址,每个section包含多少page数组成员呢,假设section管理1G大小的内存,物理页大小为4K,那么数组的条目个数为"1G/4K"。SPARSEMEM启动时是一个section,一个section初始化的,为每个section动态分配大小为"sizeof(struct page) * (1G/4K)"byte的内存。配置了SPARSEMEM_EXTREME,SPARSEMEM经典方式的框图如下:
在这里插入图片描述
2、vmemmap方式:
  为了实现SPARSEMEM的vmemmap,在内核的空间布局中,专门增加了一个vmemmap区域,也就是说专门划分出一段虚拟地址空间,用于实现PFN和page的相互转换,SPARSEMEM vmemmap的背后思想是将整个memory map数组映射到一个虚拟地址固定的、连续的vmemmap区域,通过操作vmemmap区域对应的页表,将此区域内需要用到的虚拟地址转换为物理地址,也就是说只会操作active的区域,当然对于一个具体的物理内存页来说,其对应的struct page的虚拟地址是固定的。

  SPARSEMEM vmemmap为了能把所有物理页和struct page映射起来需要很大一段虚拟地址空间,所以这种方式在32位的系统中不太适合,因为一般来说,32位系统中实际的物理内存大小是接近或者超过虚拟地址空间的,虚拟空间本身就捉襟见肘了。对于64位的系统,虚拟空间很大,使用SPARSEMEM vmemmap方式就很适合了。

  既要在内存布局中增加vmemmap区域,又要额外操作vmemmap区域对应的页表,那SPARSEMEM vmemmap有什么好处了?首先它使PFN和page之间相互转换的方式变得很简单;再一个对于一些能支持TLBs映射内核空间的体系结构来说,使用SPARSEMEM vmemmap方式,PFN和page之间的相互转换相对于CONFIG_FLATMEM可能会更高效,因为FLATMEM需要先读取变量"mem_map"保存的值,再加减offset才能得到相应的page或者PFN,但vmemmap就是一个常数,直接加减offset即可。

#define __pfn_to_page(pfn)	(vmemmap + (pfn))
#define __page_to_pfn(page)	(unsigned long)((page) - vmemmap)

  看如上的代码确实简单,且如丝般顺滑。下面是SPARSEMEM vmemmap的是总体框图:
在这里插入图片描述
  我们再来看看SPARSEMEM vmemmap是如何把vmemmap区域的虚拟地址空间和存放struct page数组的物理地址对应起来的。
a、sparse_init_nid()函数会调用sparse_mem_map_populate(),此函数会获取PFN在vmemmap中的虚拟地址,然后操作页
  表,使虚拟地址和物理地址对应起来;
b、sparse_mem_map_populate()首先根据PFN获取到vmemmap区域的虚拟地址,即__pfn_to_page(pfn) (vmemmap + (pfn))
  中的"vmemmap"是一个常量;
c、根据pfn得到虚拟地址后,通过vmemmap_populate()分配实际存放struct page数组的物理内存,同时修改虚拟地址对应的页
  表项,把分配的物理和虚拟地址对应起来;
d、在sparse_init_one_section()的作用是把pfn对应的虚拟地址保存到section_mem_map中;
e、因为section_mem_map保存的是section保存struct page数组的起始虚拟地址,但pfn中又包含了section id,所以
  在sparse_encode_mem_map()中会减去section id的部分。

static void __init sparse_init_nid(int nid, unsigned long pnum_begin,
				   unsigned long pnum_end,
				   unsigned long map_count)
{
	...
	sparse_buffer_init(map_count * section_map_size(), nid);
	for_each_present_section_nr(pnum_begin, pnum) {
		...
		map = sparse_mem_map_populate(pnum, nid, NULL);
		...
		sparse_init_one_section(__nr_to_section(pnum), pnum, map, usemap);
		usemap += usemap_longs;
	}
	...
}

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

struct page * __meminit sparse_mem_map_populate(unsigned long pnum, int nid,
		struct vmem_altmap *altmap)
{
	...
	map = pfn_to_page(pnum * PAGES_PER_SECTION);
	start = (unsigned long)map;
	end = (unsigned long)(map + PAGES_PER_SECTION);

	if (vmemmap_populate(start, end, nid, altmap))
		return NULL;
	return map;
}

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

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 > (1UL<<PFN_SECTION_SHIFT));
	BUG_ON(coded_mem_map & ~SECTION_MAP_MASK);
	return coded_mem_map;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值