kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

Linux-2.6.32 NUMA架构之内存和调度

http://blog.chinaunix.net/uid-7295895-id-3076420.html

Linux-2.6.32 NUMA架构之内存和调度

本文将以XLP832通过ICI互连形成的NUMA架构进行分析,主要包括内存管理和调度两方面,参考内核版本2.6.32.9;NUMA架构常见配置选项有:CONFIG_SMP, CONFIG_NUMA, CONFIG_NEED_MULTIPLE_NODES, CONFIG_NODES_SHIFT, CONFIG_SPARSEMEM, CONFIG_CGROUPS, CONFIG_CPUSETS, CONFIG_MIGRATION等。

本文试图从原理上介绍,尽量避免涉及代码的实现细节。

1 NUMA架构简介

NUMA(Non Uniform Memory Access)即非一致内存访问架构,市面上主要有X86_64(JASPER)和MIPS64(XLP)体系。

1.1 概念

NUMA具有多个节点(Node),每个节点可以拥有多个CPU(每个CPU可以具有多个核或线程),节点内使用共有的内存控制器,因此节点的所有内存对于本节点的所有CPU都是等同的,而对于其它节点中的所有CPU都是不同的。节点可分为本地节点(Local Node)、邻居节点(Neighbour Node)和远端节点(Remote Node)三种类型。

本地节点:对于某个节点中的所有CPU,此节点称为本地节点;
邻居节点:与本地节点相邻的节点称为邻居节点;
远端节点:非本地节点或邻居节点的节点,称为远端节点。

邻居节点和远端节点,称作非本地节点(Off Node)。

CPU访问不同类型节点内存的速度是不相同的:本地节点>邻居节点>远端节点。访问本地节点的速度最快,访问远端节点的速度最慢,即访问速度与节点的距离有关,距离越远访问速度越慢,此距离称作Node Distance。

常用的NUMA系统中:硬件设计已保证系统中所有的Cache是一致的(Cache Coherent, ccNUMA);不同类型节点间的Cache同步时间不一样,会导致资源竞争不公平,对于某些特殊的应用,可以考虑使用FIFO Spinlock保证公平性。

1.2 关键信息

1) 物理内存区域与Node号之间的映射关系;
2) 各Node之间的Node Distance;
3) 逻辑CPU号与Node号之间的映射关系。

2 XLP832 NUMA初始化

首先需要完成1.2节中描述的3个关键信息的初始化。

2.1 CPU和Node的关系

start_kernel()->setup_arch()->prom_init():

1
2
3
#ifdef CONFIG_NUMA
	build_node_cpu_map();
#endif

build_node_cpu_map()函数工作:

a) 确定CPU与Node的相互关系,做法很简单:

1
2
#define cpu_to_node(cpu)       (cpu >> 5)
#define cpumask_of_node    (NODE_CPU_MASK(node)) /* node0:0~31; node1: 32~63 */

说明:XLP832每个节点有1个物理CPU,每个物理CPU有8个核,每个核有4个超线程,因此每个节点对应32个逻辑CPU,按节点依次展开。另外,实际物理存在的CPU数目是通过DTB传递给内核的;numa_node_id()可以获取当前CPU所处的Node号。

b) 设置每个物理存在的节点的在线状态,具体是通过node_set_online()函数来设置全局变量

nodemask_t node_states[];

这样,类似于CPU号,Node号也就具有如下功能宏:

1
2
for_each_node(node);
for_each_online_node(node);

详细可参考include/linux/nodemask.h

2.2 Node Distance确立

作用:建立buddy时用,可以依此来构建zonelist,以及zone relaim(zone_reclaim_mode)使用,详见后面的4.2.2节。

2.3 内存区域与Node的关系

start_kernel()->setup_arch()->arch_mem_init->bootmem_init()->nlm_numa_bootmem_init():

nlm_get_dram_mapping();

XLP832上电后的默认memory-mapped物理地址空间分布:

其中PCIE配置空间映射地址范围为[0x1800_0000, 0x1BFF_FFFF],由寄存器ECFG_BASE和ECFG_LIMIT指定(注:但这2个寄存器本身是处于PCIE配置空间之中的)。

PCIE配置空间:
PCIE配置空间与memory-mapped物理地址的映射方式:

XLP832实现了所有设备都位于虚拟总线0上,每个节点有8个设备,按节点依次排开。

DRAM映射寄存器组:
每个节点都独立实现有几组不同类型的DRAM(每组有8个相同类型的)寄存器可以配置DRAM空间映射到物理地址空间中的基址和大小,以及所属的节点信息(这些寄存器的值事先会由bootloader设好);这组寄存器位于虚拟总线0的设备0/8/16/24(依次对应每个节点的第一个设备号)的Function0(每个设备最多可定义8个Function,每个Function有着独立的PCIE 4KB的配置空间)的PCIE配置空间中(这个配置空间实现的是DRAM/Bridge控制器)。

本小节涉及到的3组不同类型的寄存器(注:按索引对应即DRAM_BAR,DRAM_LIMIT和 DRAM_NODE_TRANSLATION描述一个内存区域属性):

第一组(DRAM空间映射物理空间基址):

1
2
3
4
5
6
7
8
DRAM_BAR0: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x54
DRAM_BAR1: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x55
DRAM_BAR2: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x56
DRAM_BAR3: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x57
DRAM_BAR4: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x58
DRAM_BAR5: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x59
DRAM_BAR6: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x5A
DRAM_BAR7: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x5B

第二组(DRAM空间映射物理空间长度):

1
2
3
4
5
6
7
8
DRAM_LIMIT0: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x5C
DRAM_LIMIT1: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x5D
DRAM_LIMIT2: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x5E
DRAM_LIMIT3: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x5F
DRAM_LIMIT4: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x60
DRAM_LIMIT5: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x61
DRAM_LIMIT6: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x62
DRAM_LIMIT7: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x63

第三组(节点相关):

1
2
3
4
5
6
7
8
DRAM_NODE_TRANSLATION0: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x64
DRAM_NODE_TRANSLATION1: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x65
DRAM_NODE_TRANSLATION2: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x66
DRAM_NODE_TRANSLATION3: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x67
DRAM_NODE_TRANSLATION4: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x68
DRAM_NODE_TRANSLATION5: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x69
DRAM_NODE_TRANSLATION6: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x6A
DRAM_NODE_TRANSLATION7: PCIe Bus 0, Device 0/8/16/24, Function 0, Register 0x6B

根据上述的PCIE配置空间memory-mapped映射方式便可直接获取寄存器中的值,就可以建立各个节点中的所有内存区域(最多8个区域)信息。关于这些寄存器的使用可以参考“XLP® Processor Family Programming Reference Manual”的“Chapter 7 Memory and I/O Subsystem”。

3 Bootmem初始化

bootmem_init()->…->init_bootmem_node()->init_bootmem_core():

每个节点拥有各自的bootmem管理(code&data之前可以为空闲页面)。

4 Buddy初始化

初始化流程最后会设置全局struct node_active_region early_node_map[]用于初始化Buddy系统,for_each_online_node()遍历所有在线节点调用free_area_init_node()初始化,主要初始化每个zone的大小和所涉及页面的struct page结构(flags中初始化有所属zone和node信息,由set_page_links()函数设置)等。

4.1 NUMA带来的变化

1) pglist_data

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct pglist_data {
	struct zone node_zones[MAX_NR_ZONES];
	struct zonelist node_zonelists[MAX_ZONELISTS];
	int nr_zones;
	struct bootmem_data *bdata;
	unsigned long node_start_pfn;
	unsigned long node_present_pages; /* total number of physical pages */
	unsigned long node_spanned_pages; /* total size of physical pagerange, including holes */
	int node_id;
	wait_queue_head_t kswapd_wait;
	struct task_struct *kswapd;
	int kswapd_max_order;
} pg_data_t;

a)上节的bootmem结构的描述信息存放在NODE_DATA(node)-> bdata中;NODE_DATA(i)宏返回节点i的struct pglist_data结构,需要在架构相关的mmzone.h中实现;
b) #define MAX_ZONELISTS 2,请参考后面的“zonelist初始化”。

2) zone

1
2
3
4
5
6
7
8
9
10
11
12
struct zone {
#ifdef CONFIG_NUMA
	int node;
	/*
	 * zone reclaim becomes active if more unmapped pages exist.
	 */
	unsigned long        min_unmapped_pages;
	unsigned long        min_slab_pages;
	struct per_cpu_pageset   *pageset[NR_CPUS];
#else
	… …
};

a)最终调用kmalloc_node()为pageset成员在每个CPU的对应的内存节点分配内存;
b)min_unmapped_pages 对应/proc/sys/vm/min_unmapped_ratio,默认值为1;
min_slab_pages对应/proc/sys/vm/min_slab_ratio,默认值为5;
作用:当剩余可回收的非文件映射和SLAB页面超过这2个值时,才激活当前zone回收;

c) 增加了zone对应的节点号。

4.2 zonelist初始化

本节讲述zonelist的构建方式,实现位于start_kernel()->build_all_zonelists()中,zonelist的组织方式非常关键(这一点与以前的2.6.21内核版本不一样,2.6.32组织得更清晰)。

4.2.1 zonelist order

NUMA系统中存在多个节点,每个节点对应一个struct pglist_data结构,此结构中可以包含多个zone,如:ZONE_DMA, ZONE_NORMAL,这样就产生几种排列顺序,以2个节点2个zone为例(zone从高到低排列, ZONE_DMA0表示节点0的ZONE_DMA,其它类似):

a) Legacy方式

每个节点只排列自己的zone;

b)Node方式

按节点顺序依次排列,先排列本地节点的所有zone,再排列其它节点的所有zone。

c) Zone方式

按zone类型从高到低依次排列各节点的同相类型zone。

可通过启动参数“numa_zonelist_order”来配置zonelist order,内核定义了3种配置:

1
2
3
#define ZONELIST_ORDER_DEFAULT  0 /* 智能选择Node或Zone方式 */
#define ZONELIST_ORDER_NODE     1 /* 对应Node方式 */
#define ZONELIST_ORDER_ZONE     2 /* 对应Zone方式 */

默认配置为ZONELIST_ORDER_DEFAULT,由内核通过一个算法来判断选择Node或Zone方式,算法思想:

a) alloc_pages()分配内存是按照ZONE从高到低的顺序进行的,例如上节“Node方式”的图示中,从ZONE_NORMAL0中分配内存时,ZONE_NORMAL0中无内存时将落入较低的ZONE_DMA0中分配,这样当ZONE_DMA0比较小的时候,很容易将ZONE_DMA0中的内存耗光,这样是很不理智的,因为还有更好的分配方式即从ZONE_NORMAL1中分配;

b) 内核会检测各ZONE的页面数来选择Zone组织方式,当ZONE_DMA很小时,选择ZONELIST_ORDER_DEFAULT时,内核将倾向于选择ZONELIST_ORDER_ZONE方式,否则选择ZONELIST_ORDER_NODE方式。

另外,可以通过/proc/sys/vm/numa_zonelist_order动态改变zonelist order的分配方式。

4.2.2 Node Distance

上节中的例子是以2个节点为例,如果有>2个节点存在,就需要考虑不同节点间的距离来安排节点,例如以4个节点2个ZONE为例,各节点的布局(如4个XLP832物理CPU级联)值如下:

上图中,Node0和Node2的Node Distance为25,Node1和Node3的Node Distance为25,其它的Node Distance为15。

4.2.2.1 优先进行Zone Reclaim

另外,当Node Distance超过20的时候,内核会在某个zone分配内存不足的时候,提前激活本zone的内存回收工作,由全局变量zone_reclaim_mode控制,build_zonelists()中:

1
2
3
4
5
6
/*
 * If another node is sufficiently far away then it is better
 * to reclaim pages in a zone before going off node.
 */
if (distance > RECLAIM_DISTANCE)
	zone_reclaim_mode = 1;

通过/proc/sys/vm/zone_reclaim_mode可以动态调整zone_reclaim_mode的值来控制回收模式,含义如下:

1
2
3
4
#define RECLAIM_OFF    0
#define RECLAIM_ZONE  (1<<0)     /* Run shrink_inactive_list on the zone */
#define RECLAIM_WRITE (1<<1)     /* Writeout pages during reclaim */
#define RECLAIM_SWAP  (1<<2)     /* Swap pages out during reclaim */
4.2.2.2 影响zonelist方式

采用Node方式组织的zonelist为:

即各节点按照与本节点的Node Distance距离大小来排序,以达到更优的内存分配。

4.2.3 zonelist[2]

配置NUMA后,每个节点将关联2个zonelist:
1) zonelist[0]中存放以Node方式或Zone方式组织的zonelist,包括所有节点的zone;
2) zonelist[1]中只存放本节点的zone即Legacy方式;

zonelist[1]用来实现仅从节点自身zone中的内存分配(参考__GFP_THISNODE标志)。

5 SLAB初始化

配置NUMA后对SLAB(本文不涉及SLOB或SLUB)的初始化影响不大,只是在分配一些变量采用类似Buddy系统的per_cpu_pageset(单面页缓存)在CPU本地节点进行内存分配。

5.1 NUMA带来的变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct kmem_cache {
	struct array_cache *array[NR_CPUS];
	… …
	struct kmem_list3 *nodelists[MAX_NUMNODES];
};

struct kmem_list3 {
	… …
	struct array_cache *shared;    /* shared per node */
	struct array_cache **alien;    /* on other nodes */
	… …
};

struct slab {
	… …
	unsigned short nodeid;
	… …
};

上面的4种类型的指针变量在SLAB初始化完毕后将改用kmalloc_node()分配的内存。具体实现请参考enable_cpucache(),此函数最终调用alloc_arraycache()和alloc_kmemlist()来分配这些变量代表的空间。

nodelists[MAX_NUMNODES]存放的是所有节点对应的相关数据,本文称作SLAB节点。每个节点拥有各自的数据;

注:有些非NUMA系统比如非连续内存系统可能根据不同的内存区域定义多个节点(实际上Node Distance都是0即物理内存访问速度相同),所以这些变量并没有采用CONFIG_NUMA宏来控制,本文暂称为NUMA带来的变化。

5.2 SLAB缓存

配置NUMA后,SLAB将有三种类型的缓存:本地缓存(当前CPU的缓存),共享缓存(节点内的缓存)和外部缓存(节点间的缓存)。

SLAB系统分配对象时,先从本地缓存中查找,如果本地缓存为空,则将共享缓存中的缓存搬运本地缓存中,重新从本地缓存中分配;如果共享缓存为空,则从SLAB中进行分配;如果SLAB中已经无空闲对象,则分配新的SLAB后重新分配本地缓存。

SLAB系统释放对象时,先不归还给SLAB (简化分配流程,也可充分利用CPU Cache),如果是同节点的SLAB对象先放入本地缓存中,如果本地缓存溢出(满),则转移一部分(以batch为单位)至共享缓存中;如果是跨节点释放,则先放入外部缓存中,如果外部缓存溢出,则转移一部分至共享缓存中,以供后续分配时使用;如果共享缓存溢出,则调用free_block()函数释放溢出的缓存对象。

关于这三种类型缓存的大小以及参数设置,不在本文的讨论范围。

本地缓存
kmem_cache-> array[] 中缓存每个CPU的SLAB cached objects;

共享缓存
kmem_list3[]->shared(如果存在shared缓存)中缓存与当前CPU同节点的所有CPU (如XLP832 NUMA系统中的Node0包含为CPU0~CPU31) 本地缓存溢出的缓存,详细实现请参考cache_flusharray();另外,大对象SLAB不存在共享缓存。

外部缓存
kmem_list3[]->alien中存放其它节点的SLAB cached objects,当在某个节点上分配的SLAB 的object在另外一个节点上被释放的时候(即slab->nodeid与numa_node_id()当前节点不相等时),将加入到对象所在节点的alien缓存中(如果不存在此alien缓存,此对象不会被缓存,而是直接释放给此对象所属SLAB),否则加入本地缓存或共享缓存(本地缓存溢出且存在shared缓存时);当alien缓存满的时候,会调用cache_free_alien()搬迁至shared缓存中(如果不存在shared缓存,直接释放给SLAB);

slab->nodeid记录本SLAB内存块(若干个页面)所在的节点。

示例

例如2个节点,CPU0~31位于Node0,CPU32~CPU63位于Node1:

64个(依次对应于CPU0~CPU63)本地缓存
kmem_cache->array[0~31]:在Node0分配“array_cache结构+cached Objs指针”;
kmem_cache->array[32~63]:在Node1分配“array_cache结构+cached Objs指针”;

2个SLAB节点
kmem_cache->nodelists[0]:在Node0分配“kmem_list3结构”;
kmem_cache->nodelists[1]:在Node1分配“kmem_list3结构”;

SLAB节点0(CPU0~CPU31)共享缓存和外部缓存alien[1]
kmem_cache->nodelists[0]->shared:在Node0分配“array_cache结构+cached Objs指针”;
kmem_cache->nodelists[0]->alien:在Node0分配“节点数sizeof(void)”;
kmem_cache->nodelists[0]->alien[0]:置为NULL;
kmem_cache->nodelists[0]->alien[1]:在Node0分配“array_cache结构+cached Objs指针”;

SLAB节点1(CPU32~CPU63)共享缓存和外部缓存alien[0]
kmem_cache->nodelists[1]->shared:在Node1分配“array_cache结构+cached Objs指针”;
kmem_cache->nodelists[1]->alien:在Node1分配“节点数sizeof(void)”;
kmem_cache->nodelists[1]->alien[0]:在Node1分配“array_cache结构+cached Objs指针”;
kmem_cache->nodelists[1]->alien[1]:置为NULL;

另外,可以用内核启动参数“use_alien_caches”来控制是否开启alien缓存:默认值为1,当系统中的节点数目为1时,use_alien_caches初始化为0;use_alien_caches目的是用于某些多节点非连续内存(访问速度相同)的非NUMA系统。

由上可见,随着节点个数的增加,SLAB明显会开销越来越多的缓存,这也是SLUB涎生的一个重要原因。

5.3 __GFP_THISNODE

SLAB在某个节点创建新的SLAB时,都会置__GFP_THISNODE标记向Buddy系统提交页面申请,Buddy系统中看到此标记,选用申请节点的Legacy zonelist[1],仅从申请节点的zone中分配内存,并且不会走内存不足流程,也不会重试或告警,这一点需要引起注意。

SLAB在申请页面的时候会置GFP_THISNODE标记后调用cache_grow()来增长SLAB;

GFP_THISNODE定义如下:

1
2
#ifdef CONFIG_NUMA
#define GFP_THISNODE     (__GFP_THISNODE | __GFP_NOWARN | __GFP_NORETRY)

6 调度初始化

配置NUMA后负载均衡会多一层NUMA调度域,根据需要在topology.h中定义,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define SD_NODE_INIT (struct sched_domain) {                 \
	.parent             = NULL,                              \
	.child              = NULL,                              \
	.groups             = NULL,                              \
	.min_interval       = 8,                                 \
	.max_interval       = 32,                                \
	.busy_factor        = 32,                                \
	.imbalance_pct      = 125,                               \
	.cache_nice_tries   = 1,                                 \
	.flags              = SD_LOAD_BALANCE | SD_BALANCE_EXEC, \
	.last_balance       = jiffies,                           \
	.balance_interval   = 1,                                 \
	.nr_balance_failed  = 0,                                 \
}

顺便提一下,2.6.32对于实时任务不走负载均衡流程,采用了全局优先级调度的思想,保证实时任务的及时运行;这样的做法同时也解决了低版本内核在处理同一个逻辑CPU上相同最高优先级实时任务的负载均衡的时延。

7 NUMA内存分配

Zonelist[2]组织方式在NUMA内存分配过程中起着至关重要的作用,它决定了整个页面在不同节点间的申请顺序和流程。

7.1显式分配

显式分配即指定节点的分配函数,此类基础分配函数主要有2个:Buddy系统的 alloc_pages_node()和SLAB系统的kmem_cache_alloc_node(),其它的函数都可以从这2个派生出来。

例如,kmalloc_node()最终调用kmem_cache_alloc_node()进行分配。

7.1.1 Buddy显式分配

alloc_pages_node(node, gfp_flags, order)分配流程:
1) 如果node小于0,node取本地节点号(node = numa_node_id());
2) NODE_DATA(node)得到node对应的struct pglist_data结构,从而得到zonelist[2];
3) 如果gfp_flags含有__GFP_THISNODE标志,仅在此节点分配内存,使用node节点的Legacy zonelist[1],否则使用其包含所有节点zone的zonelist[0] (见4.2.2.3节);
4) 遍历确定出来的zonelist结构中包含的每一个符合要求的zone,gfp_flags指定了本次分配中的最高的zone,如__GFP_HIGHMEM表示最高的zone为ZONE_HIGH;
5) 分配结束。

7.1.2 SLAB显式分配

kmem_cache_alloc_node(cachep, gfp_flags, node)分配流程:
1) 如果node值为-1,node取本地节点号(node = numa_node_id());
2) 如果node < -1,则执行fall back行为,此行为与用户策略有关,有点类似隐式分配:
a) 根据用户策略(包括CPUSET和内存策略)依次选取节点,根据gfp_flags选取合适的zonelist进行分配;
b) 如果内存不足分配失败,则跳过内存策略直接进行隐式Buddy页面分配(仍受CPUSET的限定,关于CPUSET和内存策略后面会介绍),最终构建成新的SLAB并完成本次分配;转5);
3) 如果node是正常节点号,则先在node节点上根据gfp_flags选取合适的zonelist进行分配;
4) 如果3)中node节点内存不足分配失败,转2) a)执行fall back行为。
5) 分配结束。

注:fall back行为指的是某个节点上内存不足时会落到此节点的zonelist[0]中定义的其它节点zone分配。

7.1.3 设备驱动

配置CONFIG_NUMA后,设备会关联一个NUMA节点信息,struct device结构中会多一个numa_node字段记录本设备所在的节点,这个结构嵌套在各种类型的驱动中,如struct net_device结构。

1
2
3
4
5
6
7
struct device {
	… …
	#ifdef CONFIG_NUMA
		int          numa_node;    /* NUMA node this device is close to */
	#endif
	… …
}

__netdev_alloc_skb()的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct sk_buff *__netdev_alloc_skb(struct net_device *dev,
		unsigned int length, gfp_t gfp_mask)
{
	int node = dev->dev.parent ? dev_to_node(dev->dev.parent) : -1;
	struct sk_buff *skb;

	skb = __alloc_skb(length + NET_SKB_PAD, gfp_mask, 0, node);
	if (likely(skb)) {
		skb_reserve(skb, NET_SKB_PAD);
		skb->dev = dev;
	}
	return skb;
}

__alloc_skb()最终调用kmem_cache_alloc_node()和kmalloc_node()在此node上分配内存。

7.2 隐式分配和内存策略

隐式分配即不指定节点的分配函数,此类基础分配函数主要有2个:Buddy系统的 alloc_pages()和SLAB系统的kmem_cache_alloc(),其它的函数都可以从这2个派生出来。

隐式分配涉及到NUMA内存策略(Memory Policy),内核定义了四种内存策略。

注:隐式分配还涉及到CPUSET,本文后面会介绍。

7.2.1 内存策略

内核mm/mempolicy.c中实现了NUMA内存的四种内存分配策略:MPOL_DEFAULT, MPOL_PREFERRED, MPOL_INTERLEAVE和MPOL_BIND,内存策略会从父进程继承。

MPOL_DEFAULT:使用本地节点的zonelist;
MPOL_PREFERRED:使用指定节点的zonelist;
MPOL_BIND: 设置一个节点集合,只能从这个集合中节点的zone申请内存:

1)无__GFP_THISNODE申请标记,使用本地节点的zonelist[0];
2)置有__GFP_THISNODE申请标记,如果本地节点:
a)在集合中,使用本地节点的zonelist[1];
b)不在集合中,使用集合中最小节点号的zonelist[1];

MPOL_INTERLEAVE:采用Round-Robin方式从设定的节点集合中选出某个节点,使用此节点的zonelist;

内核实现的内存策略,用struct mempolicy结构来描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct mempolicy {
	atomic_t refcnt;
	unsigned short mode;              /* See MPOL_* above */
	unsigned short flags;             /* See set_mempolicy() MPOL_F_* above */
	union {
		short         preferred_node; /* preferred */
		nodemask_t    nodes;          /* interleave/bind */
		/* undefined for default */
	} v;
	union {
		nodemask_t cpuset_mems_allowed;     /* relative to these nodes */
		nodemask_t user_nodemask;           /* nodemask passed by user */
	} w;
};

成员mode表示使用四种分配策略中的哪一种,联合体v根据不同的分配策略记录相应的分配信息。

另外,MPOL_PREFERRED策略有一种特殊的模式,当其flags置上MPOL_F_LOCAL标志后,将等同于MPOL_DEFAULT策略,内核默认使用此种策略,见全局变量default_policy。

内存策略涉及的分配函数有2个:alloc_pages_current()和alloc_page_vma(),可以分别为不同任务以及任务的不同VMA设置内存策略。

7.2.2 Buddy隐式分配

以默认的NUMA内存策略为例讲解,alloc_pages(gfp_flags, order)分配流程:
1) 得到本地节点对应的struct pglist_data结构,从而得到zonelist[2];
2) 如果gfp_flags含有__GFP_THISNODE标志,仅在此节点分配内存即使用本地节点的Legacy zonelist[1],否则使用zonelist[0] (见4.2.2.3节);
3) 遍历确定出来的zonelist结构中包含的每一个符合要求的zone,gfp_flags指定了本次分配中的最高的zone,如__GFP_HIGHMEM表示最高的zone为ZONE_HIGH;
4) 分配结束。

7.2.3 SLAB隐式分配

以默认的NUMA内存策略为例讲解,kmem_cache_alloc(cachep, gfp_flags)分配流程:
1) 调用____cache_alloc()函数在本地节点local_node分配,此函数无fall back行为;
2) 如果1)中本地节点内存不足分配失败,调用____cache_alloc_node(cachep, gfp_flags,local_node)再次尝试在本地节点分配,如果还失败此函数会进行fall back行为;
3) 分配结束。

7.3 小结

上文提到的所有的内存分配函数都允许fall back行为,但有2种情况例外:
1) __GFP_THISNODE分配标记限制了只能从某一个节点上分配内存;
2) MPOL_BIND策略,限制了只能从一个节点集合中的节点上分配内存;
(gfp_zone(gfp_flags) < policy_zone的情况,MPOL_BIND不限制节点)。

注:还有一种情况,CPUSET限制的内存策略,后面会介绍。

8 CPUSET

CPUSET基于CGROUP的框架构建的子系统,有如下特点:
1) 限定一组任务所允许使用的内存Node和CPU资源;
2) CPUSET在内核各子系统中添加的检测代码很少,对内核没有性能影响;
3) CPUSET的限定优先级高于内存策略(针对于Node)和绑定(针对于CPU);
4) 没有额外实现系统调用接口,只能通过/proc文件系统和用户交互。

本节只讲述CPUSET的使用方法和说明。

8.1 创建CPUSET

因为CPUSET只能使用/proc文件系统访问,所以第一步就要先mount cpuset文件系统,配置CONFIG_CGROUPS和CONFIG_CPUSETS后/proc/filesystems中将有这个文件系统。

CPUSET是分层次的,可以在cpuset文件系统根目录是最顶层的CPUSET,可以在其下创建CPUSET子项,创建方式很简单即创建一个新的目录。

mount命令:mount nodev –t cpuset /your_dir或mount nodev –t cgroup –o cpuset /your_dir

Mount成功后,进入mount目录,这个就是最顶层的CPUSET了(top_cpuset),下面附一个演示例子:

8.2 CPUSET文件

介绍几个重要的CPUSET文件:
1) tasks,实际上是CGROUPS文件,为此CPUSET包含的线程pid集合;
echo 100 > tasks

2) cgroup.procs是CGROUPS文件,为此CPUSET包含的线程组tgid集合;
echo 100 > cgroup.procs

3) cpus是CPUSET文件,表示此CPUSET允许的CPU;
echo 0-8 > cpus

4) mems是CPUSET文件,表示此CPUSET允许的内存节点; echo 0-1 > mems (对应于struct task_struct中的mems_allowed字段)

5) sched_load_balance,为CPUSET文件,设置cpus集合的CPU是否参与负载均衡; echo 0 > sched_load_balance (禁止负载均衡);默认值为1表示开启负载均衡;

6) sched_relax_domain_level,为CPUSET文件,数值代表某个调度域级别,大于此级别的调度域层次将禁用闲时均衡和唤醒均衡,而其余级别的调度域都开启; 也可以通过启动参数“relax_domain_level”设置,其值含义:
-1 : 无效果,此为默认值
0 - 设置此值会禁用所有调度域的闲时均衡和唤醒均衡
1 - 超线程域
2 - 核域
3 - 物理域
4 - NUMA域
5 - ALLNODES模式的NUMA域

7) mem_exclusive和mem_hardwall,为CPUSET文件,表示内存硬墙标记;默认为0,表示软墙;有关CPUSET的内存硬墙(HardWall)和内存软墙(SoftWall),下文会介绍;

8) memory_spread_page和memory_spread_slab,为CPUSET文件,设定CPUSET中的任务PageCache和SLAB(创建时置有SLAB_MEM_SPREAD)以Round-Robin方式使用内存节点(类似于MPOL_INTERLEAVE);默认为0,表示未开启;struct task_struct结构中增加成员cpuset_mem_spread_rotor记录下次使用的节点号;

9) memory_migrate,为CPUSET文件,表明开启此CPUSET的内存迁移,默认为0;

当一个任务从一个CPUSET1(mems值为0)迁移至另一个CPUSET2(mems值为1)的时候,此任务在节点0上分配的页面内容将迁移至节点1上分配新的页面(将数据同步到新页面),这样就避免了此任务的非本地节点的内存访问。

上图为单Node,8个CPU的系统。

1) 顶层CPUSET包含了系统中的所有CPU以及Node,而且是只读的,不能更改;
2) 顶层CPUSET包含了系统中的所有任务,可以更改;
3) child为新创建的子CPUSET,子CPUSET的资源不能超过父CPUSET的资源;
4) 新创建的CPUSET的mems和cpus都是空的,使用前必须先初始化;
5) 添加任务:设置tasks和cgroup.procs文件;
6) 删除任务:将任务重新添加至其它CPUSET(如顶层)就可以从本CPUSET删除任务。

8.3 利用CPUSET限定CPU和Node

设置步骤:
1) 在某个父CPUSET中创建子CPUSET;
2) 在子CPUSET目录下,输入指定的Node号至mems文件;
3) 在子CPUSET目录下,输入指定的Node号至mems文件;
4) 在子CPUSET目录下,设定任务至tasks或group.procs文件;
5) 还可以设置memory_migrate为1,激活内存页面的迁移功能。

这样限定后,此CPUSET中所有的任务都将使用限定的CPU和Node,但毕竟系统中的任务并不能完全孤立,比如还是可能会全局共享Page Cache,动态库等资源,因此内核在某些情况下还是可以允许打破这个限制,如果不允许内核打破这个限制,需要设定CPUSET的内存硬墙标志即mem_exclusive或mem_hardwall置1即可;CPUSET默认是软墙。

硬软墙用于Buddy系统的页面分配,优先级高于内存策略,请参考内核函数:

cpuset_zone_allowed_hardwall()和cpuset_zone_allowed_softwall()

另外,当内核分不到内存将导致Oops的时候,CPUSET所有规则将被打破,毕竟一个系统的正常运行才是最重要的:
1) __GFP_THISNODE标记分配内存的时候(通常是SLAB系统);
2) 中断中分配内存的时候;
3) 任务置有TIF_MEMDIE标记即被内核OOM杀死的任务。

8.4 利用CPUSET动态改变调度域结构

利用sched_load_balance文件可以禁用掉某些CPU的负载均衡,同时重新构建调度域,此功能类似启动参数“isolcpus”的功能。

8个CPU的系统中,系统中存在一个物理域,现需要禁掉CPU4~CPU7的负载均衡,配置步骤为:
1) “mkdir child”在顶层CPUSET中创建子CPUSET,记为child;
2) “echo 0-3 > child/cpus ”(新建CPUSET的sched_load_balance默认是是打开的);
3) “echo 0 > sched_load_balance”关闭顶层CPUSET的负载均衡。

操作过程见下图:

由图可见,CPU4~CPU7的调度域已经不存在了,具体效果是将CPU4~CPU7从负载均衡中隔离出来。

9 NUMA杂项

1) /sys/devices/system/node/中记录有系统中的所有内存节点信息;
2)任务额外关联一个/proc//numa_smaps文件信息;
3) tmpfs可以指定在某个Node上创建;
4) libnuma库和其numactl小工具可以方便操作NUMA内存;
5) … …

10 参考资料

  1. www.kernel.org
  2. ULK3
  3. XLP® Processor Family Programming Reference Manual

Linux内存管理

http://blog.csdn.net/myarrow/article/details/8624687

http://blog.csdn.net/myarrow/article/details/8682819

1. Linux物理内存三级架构

对于内存管理,Linux采用了与具体体系架构不相关的设计模型,实现了良好的可伸缩性。它主要由内存节点node、内存区域zone和物理页框page三级架构组成。

内存节点node

内存节点node是计算机系统中对物理内存的一种描述方法,一个总线主设备访问位于同一个节点中的任意内存单元所花的代价相同,而访问任意两个不同节点中的内存单元所花的代价不同。在一致存储结构(Uniform Memory Architecture,简称UMA)计算机系统中只有一个节点,而在非一致性存储结构(NUMA)计算机系统中有多个节点。Linux内核中使用数据结构pg_data_t来表示内存节点node。如常用的ARM架构为UMA架构。

内存区域zone

内存区域位于同一个内存节点之内,由于各种原因它们的用途和使用方法并不一样。如基于IA32体系结构的个人计算机系统中,由于历史原因使得ISA设备只能使用最低16MB来进行DMA传输。又如,由于Linux内核采用

• 物理页框page

2. Linux虚拟内存三级页表

Linux虚拟内存三级管理由以下三级组成:
• PGD: Page Global Directory (页目录)
• PMD: Page Middle Directory (页目录)
• PTE: Page Table Entry (页表项)

每一级有以下三个关键描述宏:
• SHIFT
• SIZE
• MASK

如页的对应描述为:

1
2
3
4
/* PAGE_SHIFT determines the page size  asm/page.h */  
#define PAGE_SHIFT      12  
#define PAGE_SIZE       (_AC(1,UL) << PAGE_SHIFT)  
#define PAGE_MASK       (~(PAGE_SIZE-1))  

数据结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* asm/page.h */  
typedef unsigned long pteval_t;  

typedef pteval_t pte_t;  
typedef unsigned long pmd_t;  
typedef unsigned long pgd_t[2];  
typedef unsigned long pgprot_t;  

#define pte_val(x)      (x)  
#define pmd_val(x)      (x)  
#define pgd_val(x)  ((x)[0])  
#define pgprot_val(x)   (x)  

#define __pte(x)        (x)  
#define __pmd(x)        (x)  
#define __pgprot(x)     (x)  
2.1 Page Directory (PGD and PMD)

每个进程有它自己的PGD( Page Global Directory),它是一个物理页,并包含一个pgd_t数组。其定义见<asm/page.h>。 进程的pgd_t数据见 task_struct -> mm_struct -> pgd_t * pgd;

ARM架构的PGD和PMD的定义如下<arch/arm/include/asm/pgtable.h>:

1
2
3
4
5
6
7
8
9
10
11
#define PTRS_PER_PTE  512    // PTE中可包含的指针<u32>数 (21-12=9bit)  
#define PTRS_PER_PMD  1  
#define PTRS_PER_PGD  2048   // PGD中可包含的指针<u32>数 (32-21=11bit)</p><p>#define PTE_HWTABLE_PTRS (PTRS_PER_PTE)  
#define PTE_HWTABLE_OFF  (PTE_HWTABLE_PTRS * sizeof(pte_t))  
#define PTE_HWTABLE_SIZE (PTRS_PER_PTE * sizeof(u32))
/*  
 * PMD_SHIFT determines the size of the area a second-level page table can map  
 * PGDIR_SHIFT determines what a third-level page table entry can map  
 */  
#define PMD_SHIFT  21  
#define PGDIR_SHIFT  21

虚拟地址SHIFT宏图:

虚拟地址MASK和SIZE宏图:

2.2 Page Table Entry

PTEs, PMDs和PGDs分别由pte_t, pmd_t 和pgd_t来描述。为了存储保护位,pgprot_t被定义,它拥有相关的flags并经常被存储在page table entry低位(lower bits),其具体的存储方式依赖于CPU架构。

每个pte_t指向一个物理页的地址,并且所有的地址都是页对齐的。因此在32位地址中有PAGE_SHIFT(12)位是空闲的,它可以为PTE的状态位。

PTE的保护和状态位如下图所示:

2.3 如何通过3级页表访问物理内存

为了通过PGD、PMD和PTE访问物理内存,其相关宏在asm/pgtable.h中定义。

• pgd_offset

根据当前虚拟地址和当前进程的mm_struct获取pgd项的宏定义如下:

1
2
3
4
5
6
7
/* to find an entry in a page-table-directory */  
#define pgd_index(addr)     ((addr) >> PGDIR_SHIFT)  //获得在pgd表中的索引  

#define pgd_offset(mm, addr)    ((mm)->pgd + pgd_index(addr)) //获得pmd表的起始地址  

/* to find an entry in a kernel page-table-directory */  
#define pgd_offset_k(addr)  pgd_offset(&init_mm, addr)  

• pmd_offset

根据通过pgd_offset获取的pgd 项和虚拟地址,获取相关的pmd项(即pte表的起始地址)

1
2
/* Find an entry in the second-level page table.. */  
#define pmd_offset(dir, addr)   ((pmd_t *)(dir))   //即为pgd项的值  

• pte_offset

根据通过pmd_offset获取的pmd项和虚拟地址,获取相关的pte项(即物理页的起始地址)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef CONFIG_HIGHPTE  
#define __pte_map(pmd)      pmd_page_vaddr(*(pmd))  
#define __pte_unmap(pte)    do { } while (0)  
#else  
#define __pte_map(pmd)      (pte_t *)kmap_atomic(pmd_page(*(pmd)))  
#define __pte_unmap(pte)    kunmap_atomic(pte)  
#endif  

#define pte_index(addr)     (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))  

#define pte_offset_kernel(pmd,addr) (pmd_page_vaddr(*(pmd)) + pte_index(addr))  

#define pte_offset_map(pmd,addr)    (__pte_map(pmd) + pte_index(addr))  
#define pte_unmap(pte)          __pte_unmap(pte)  

#define pte_pfn(pte)        (pte_val(pte) >> PAGE_SHIFT)  
#define pfn_pte(pfn,prot)   __pte(__pfn_to_phys(pfn) | pgprot_val(prot))  

#define pte_page(pte)       pfn_to_page(pte_pfn(pte))  
#define mk_pte(page,prot)   pfn_pte(page_to_pfn(page), prot)  

#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)  
#define pte_clear(mm,addr,ptep) set_pte_ext(ptep, __pte(0), 0)  

其示意图如下图所示:

2.4 根据虚拟地址获取物理页的示例代码

根据虚拟地址获取物理页的示例代码详见<mm/memory.c中的函数follow_page>。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/** 
 * follow_page - look up a page descriptor from a user-virtual address 
 * @vma: vm_area_struct mapping @address 
 * @address: virtual address to look up 
 * @flags: flags modifying lookup behaviour 
 * 
 * @flags can have FOLL_ flags set, defined in <linux/mm.h> 
 * 
 * Returns the mapped (struct page *), %NULL if no mapping exists, or 
 * an error pointer if there is a mapping to something not represented 
 * by a page descriptor (see also vm_normal_page()). 
 */  
struct page *follow_page(struct vm_area_struct *vma, unsigned long address,  
		    unsigned int flags)  
{  
	pgd_t *pgd;  
	pud_t *pud;  
	pmd_t *pmd;  
	pte_t *ptep, pte;  
	spinlock_t *ptl;  
	struct page *page;  
	struct mm_struct *mm = vma->vm_mm;  

	page = follow_huge_addr(mm, address, flags & FOLL_WRITE);  
	if (!IS_ERR(page)) {  
		BUG_ON(flags & FOLL_GET);  
		goto out;  
	}  

	page = NULL;  
	pgd = pgd_offset(mm, address);  
	if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))  
		goto no_page_table;  

	pud = pud_offset(pgd, address);  
	if (pud_none(*pud))  
		goto no_page_table;  
	if (pud_huge(*pud) && vma->vm_flags & VM_HUGETLB) {  
		BUG_ON(flags & FOLL_GET);  
		page = follow_huge_pud(mm, address, pud, flags & FOLL_WRITE);  
		goto out;  
	}  
	if (unlikely(pud_bad(*pud)))  
		goto no_page_table;  

	pmd = pmd_offset(pud, address);  
	if (pmd_none(*pmd))  
		goto no_page_table;  
	if (pmd_huge(*pmd) && vma->vm_flags & VM_HUGETLB) {  
		BUG_ON(flags & FOLL_GET);  
		page = follow_huge_pmd(mm, address, pmd, flags & FOLL_WRITE);  
		goto out;  
	}  
	if (pmd_trans_huge(*pmd)) {  
		if (flags & FOLL_SPLIT) {  
		    split_huge_page_pmd(mm, pmd);  
		    goto split_fallthrough;  
		}  
		spin_lock(&mm->page_table_lock);  
		if (likely(pmd_trans_huge(*pmd))) {  
		    if (unlikely(pmd_trans_splitting(*pmd))) {  
		        spin_unlock(&mm->page_table_lock);  
		        wait_split_huge_page(vma->anon_vma, pmd);  
		    } else {  
		        page = follow_trans_huge_pmd(mm, address,  
		                         pmd, flags);  
		        spin_unlock(&mm->page_table_lock);  
		        goto out;  
		    }  
		} else  
		    spin_unlock(&mm->page_table_lock);  
		/* fall through */  
	}  
split_fallthrough:  
	if (unlikely(pmd_bad(*pmd)))  
		goto no_page_table;  

	ptep = pte_offset_map_lock(mm, pmd, address, &ptl);  

	pte = *ptep;  
	if (!pte_present(pte))  
		goto no_page;  
	if ((flags & FOLL_WRITE) && !pte_write(pte))  
		goto unlock;  

	page = vm_normal_page(vma, address, pte);  
	if (unlikely(!page)) {  
		if ((flags & FOLL_DUMP) ||  
		    !is_zero_pfn(pte_pfn(pte)))  
		    goto bad_page;  
		page = pte_page(pte);  
	}  

	if (flags & FOLL_GET)  
		get_page(page);  
	if (flags & FOLL_TOUCH) {  
		if ((flags & FOLL_WRITE) &&  
		    !pte_dirty(pte) && !PageDirty(page))  
		    set_page_dirty(page);  
		/* 
		 * pte_mkyoung() would be more correct here, but atomic care 
		 * is needed to avoid losing the dirty bit: it is easier to use 
		 * mark_page_accessed(). 
		 */  
		mark_page_accessed(page);  
	}  
	if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {  
		/* 
		 * The preliminary mapping check is mainly to avoid the 
		 * pointless overhead of lock_page on the ZERO_PAGE 
		 * which might bounce very badly if there is contention. 
		 * 
		 * If the page is already locked, we don't need to 
		 * handle it now - vmscan will handle it later if and 
		 * when it attempts to reclaim the page. 
		 */  
		if (page->mapping && trylock_page(page)) {  
		    lru_add_drain();  /* push cached pages to LRU */  
		    /* 
		     * Because we lock page here and migration is 
		     * blocked by the pte's page reference, we need 
		     * only check for file-cache page truncation. 
		     */  
		    if (page->mapping)  
		        mlock_vma_page(page);  
		    unlock_page(page);  
		}  
	}  
unlock:  
	pte_unmap_unlock(ptep, ptl);  
out:  
	return page;  

bad_page:  
	pte_unmap_unlock(ptep, ptl);  
	return ERR_PTR(-EFAULT);  

no_page:  
	pte_unmap_unlock(ptep, ptl);  
	if (!pte_none(pte))  
		return page;  

no_page_table:  
	/* 
	 * When core dumping an enormous anonymous area that nobody 
	 * has touched so far, we don't want to allocate unnecessary pages or 
	 * page tables.  Return error instead of NULL to skip handle_mm_fault, 
	 * then get_dump_page() will return NULL to leave a hole in the dump. 
	 * But we can only make this optimization where a hole would surely 
	 * be zero-filled if handle_mm_fault() actually did handle it. 
	 */  
	if ((flags & FOLL_DUMP) &&  
		(!vma->vm_ops || !vma->vm_ops->fault))  
		return ERR_PTR(-EFAULT);  
	return page;  
}

1. First Fit分配器

First Fit分配器是最基本的内存分配器,它使用bitmap而不是空闲块列表来表示内存。在bitmap中,如果page对应位为1,则表示此page已经被分配,为0则表示此page没有被分配。为了分配小于一个page的内存块,First Fit分配器记录了最后被分配的PFN (Page Frame Number)和分配的结束地址在页内的偏移量。随后小的内存分配被Merge到一起并存储到同一页中。

First Fit分配器不会造成严重的内存碎片,但其效率较低,由于内存经常通过线性地址进行search,而First Fit中的小块内存经常在物理内存的开始处,为了分配大块内存而不得不扫描前面大量的内存。

2. Boot Memory分配器

物理内存分配器如何分配内存来初始化其自己呢?

答案是:通过Boot Memory分配器来实现,而Boot Memory分配器则通过最基本的First Fit分配器来实现。

2.1 Boot Map定义

Boot Map通过数据结构bootmem_data来定义,详见<linux/bootmem.h>,其定义如下所示:

1
2
3
4
5
6
7
8
typedef struct bootmem_data {  
	unsigned long node_boot_start; // 描述的物理内存的起始地址  
	unsigned long node_low_pfn;    // 结束物理地址,即ZONE_NORMAL的结束  
	void *node_bootmem_map;        // 描述“使用或空闲的位图”的地址  
	unsigned long last_offset;     // 最后被分配的页内偏移量,即在llast_pos描述的物理页中,  
		                         // 从last_offset开始,没有被分配   
	unsigned long last_pos;        // 最后被分配的页的PFN  
} bootmem_data_t;  

所有bootmem_data被放于全局变量bdata_list中。

2.2 Boot Memory分配器初始化

每一个CPU架构被要求提供setup_arch函数,它负责获取初始化boot memory分配器的必要参数。不同的CPU架构通过不同的函数来实现,如ARM通过bootmem_init来实现。它负责获取以下参数:

1
2
3
4
5
• min_low_pfn: 系统中可获得的最小的PFN,装载kernel image结束之后的第一页,在mm/bootmem.c中定义
• max_low_pfn:低端内存(ZONE_NORMAL)中可获得的最大PFN
• highstart_pfn:高端内存(ZONE_HIGHMEM)的起始PFN
• highend_pfn:高端内存(ZONE_HIGHMEM)的结束PFN
• max_pfn:系统中可获得的最大的PFN, 在mm/bootmem.c中定义

PFN是在物理内存map的偏移量,以page为单位。Kernel可直接访问ZONE_NORMAL,其偏移量为:PAGE_OFFSET。

通过以上5个参数明确了可用物理内存之后,调用init_bootmem->init_bootmem_core来初始化contig_page_data。它主要完成以下两件事:
1) 将把与此node对应pgdat_data_t插入到pgdat_list中
2) 初始化bootmem_data_t的中参数,并分配表示页分配状态的bitmap,其大小为: (end_pfn-start_pfn+7)/8

bitmap的物理地址为:bootmem_data_t->node_boot_start
bitmap的虚拟地直为:bootmem_data_t->node_bootmem_map

2.3 分配内存

• reserve_bootmem:用于预留物理页面。但用于通用的内存分配是低率的,它主要用于各种驱动(如:Video Codec)预留内存。

常用的内存分配函数如下(in UMA架构,我们常的ARM架构为UMA架构):

1
2
3
4
• alloc_bootmem
• alloc_bootmem_low
• alloc_bootmem_pages
• alloc_bootmem_low_pages

其调用关系如下图所示:

2.3.1 __alloc_bootmem

__alloc_bootmem() 需要以下参数:
• pgdat 用于分配内存块的节点,在UMA架构中,它被忽略,因为它总是为:contig_page_data
• size 指定请求分配的内存大小,以字节为单位
• align 请求以多少字节对齐,地于小块内存分配,一般以SMP_CACHE_BYTES对齐,如在X86上,与L1硬件cache对齐
• goal 偏好的分配内存的起始地址,

2.3.2 __alloc_bootmem_core

它从goal指定的地址开始,线性地扫描内存,以寻找可以满足内存分配要求的内存块。它的另外一项功能是决定是否需要把新分配的内存块与以前已经分配的内存块merge到一起。

分配内存常用函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#ifdef CONFIG_NO_BOOTMEM  
/* We are using top down, so it is safe to use 0 here */  
#define BOOTMEM_LOW_LIMIT 0  
#else  
#define BOOTMEM_LOW_LIMIT __pa(MAX_DMA_ADDRESS)  
#endif  

#define alloc_bootmem(x) \  
	__alloc_bootmem(x, SMP_CACHE_BYTES, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_align(x, align) \  
	__alloc_bootmem(x, align, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_nopanic(x) \  
	__alloc_bootmem_nopanic(x, SMP_CACHE_BYTES, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_pages(x) \  
	__alloc_bootmem(x, PAGE_SIZE, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_pages_nopanic(x) \  
	__alloc_bootmem_nopanic(x, PAGE_SIZE, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_node(pgdat, x) \  
	__alloc_bootmem_node(pgdat, x, SMP_CACHE_BYTES, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_node_nopanic(pgdat, x) \  
	__alloc_bootmem_node_nopanic(pgdat, x, SMP_CACHE_BYTES, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_pages_node(pgdat, x) \  
	__alloc_bootmem_node(pgdat, x, PAGE_SIZE, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_pages_node_nopanic(pgdat, x) \  
	__alloc_bootmem_node_nopanic(pgdat, x, PAGE_SIZE, BOOTMEM_LOW_LIMIT)  

#define alloc_bootmem_low(x) \  
	__alloc_bootmem_low(x, SMP_CACHE_BYTES, 0)  
#define alloc_bootmem_low_pages(x) \  
	__alloc_bootmem_low(x, PAGE_SIZE, 0)  
#define alloc_bootmem_low_pages_node(pgdat, x) \  
	__alloc_bootmem_low_node(pgdat, x, PAGE_SIZE, 0)  
2.4 释放内存

调用free_bootmem来释放内存。

1
2
3
4
5
6
7
8
9
10
11
void __init free_bootmem(unsigned long addr, unsigned long size)  
{  
	unsigned long start, end;  

	kmemleak_free_part(__va(addr), size);  

	start = PFN_UP(addr);  
	end = PFN_DOWN(addr + size);  

	mark_bootmem(start, end, 0, 0);  
}

SMP、NUMA体系结构

http://www.cnblogs.com/yubo/archive/2010/04/23/1718810.html

blog.csdn.net/skymanwu/article/details/7551670

从系统架构来看,目前的商用服务器大体可以分为三类,即对称多处理器结构 (SMP : Symmetric Multi-Processor) ,非一致存储访问结构 (NUMA : Non-Uniform Memory Access) ,以及海量并行处理结构 (MPP : Massive Parallel Processing) 。它们的特征分别描述如下:

1. SMP(Symmetric Multi-Processor)

SMP (Symmetric Multi Processing),对称多处理系统内有许多紧耦合多处理器,在这样的系统中,所有的CPU共享全部资源,如总线,内存和I/O系统等,操作系统或管理数据库的复本只有一个,这种系统有一个最大的特点就是共享所有资源。多个CPU之间没有区别,平等地访问内存、外设、一个操作系统。操作系统管理着一个队列,每个处理器依次处理队列中的进程。如果两个处理器同时请求访问一个资源(例如同一段内存地址),由硬件、软件的锁机制去解决资源争用问题。Access to RAM is serialized; this and cache coherency issues causes performance to lag slightly behind the number of additional processors in the system.

所谓对称多处理器结构,是指服务器中多个 CPU 对称工作,无主次或从属关系。各 CPU 共享相同的物理内存,每个 CPU 访问内存中的任何地址所需时间是相同的,因此 SMP 也被称为一致存储器访问结构 (UMA : Uniform Memory Access) 。对 SMP 服务器进行扩展的方式包括增加内存、使用更快的 CPU 、增加 CPU 、扩充 I/O( 槽口数与总线数 ) 以及添加更多的外部设备 ( 通常是磁盘存储 ) 。

SMP 服务器的主要特征是共享,系统中所有资源 (CPU 、内存、 I/O 等 ) 都是共享的。也正是由于这种特征,导致了 SMP 服务器的主要问题,那就是它的扩展能力非常有限。对于 SMP 服务器而言,每一个共享的环节都可能造成 SMP 服务器扩展时的瓶颈,而最受限制的则是内存。由于每个 CPU 必须通过相同的内存总线访问相同的内存资源,因此随着 CPU 数量的增加,内存访问冲突将迅速增加,最终会造成 CPU 资源的浪费,使 CPU 性能的有效性大大降低。实验证明, SMP 服务器 CPU 利用率最好的情况是 2 至 4 个 CPU 。

2. NUMA(Non-Uniform Memory Access)

由于 SMP 在扩展能力上的限制,人们开始探究如何进行有效地扩展从而构建大型系统的技术, NUMA 就是这种努力下的结果之一。利用 NUMA 技术,可以把几十个 CPU( 甚至上百个 CPU) 组合在一个服务器内。其 CPU 模块结构如图 2 所示:


图 2.NUMA 服务器 CPU 模块结构

NUMA 服务器的基本特征是具有多个 CPU 模块,每个 CPU 模块由多个 CPU( 如 4 个 ) 组成,并且具有独立的本地内存、 I/O 槽口等。由于其节点之间可以通过互联模块 ( 如称为 Crossbar Switch) 进行连接和信息交互,因此每个 CPU 可以访问整个系统的内存 ( 这是 NUMA 系统与 MPP 系统的重要差别 ) 。显然,访问本地内存的速度将远远高于访问远地内存 ( 系统内其它节点的内存 ) 的速度,这也是非一致存储访问 NUMA 的由来。由于这个特点,为了更好地发挥系统性能,开发应用程序时需要尽量减少不同 CPU 模块之间的信息交互。

利用 NUMA 技术,可以较好地解决原来 SMP 系统的扩展问题,在一个物理服务器内可以支持上百个 CPU 。比较典型的 NUMA 服务器的例子包括 HP 的 Superdome 、 SUN15K 、 IBMp690 等。

但 NUMA 技术同样有一定缺陷,由于访问远地内存的延时远远超过本地内存,因此当 CPU 数量增加时,系统性能无法线性增加。如 HP 公司发布 Superdome 服务器时,曾公布了它与 HP 其它 UNIX 服务器的相对性能值,结果发现, 64 路 CPU 的 Superdome (NUMA 结构 ) 的相对性能值是 20 ,而 8 路 N4000( 共享的 SMP 结构 ) 的相对性能值是 6.3 。从这个结果可以看到, 8 倍数量的 CPU 换来的只是 3 倍性能的提升。