kk Blog —— 通用基础


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

Linux kernel 内存屏障在RCU上的应用

http://blog.csdn.net/jianchaolv/article/details/7527647

内存屏障主要解决的问题是编译器的优化和CPU的乱序执行。

编译器在优化的时候,生成的汇编指令可能和c语言程序的执行顺序不一样,在需要程序严格按照c语言顺序执行时,需要显式的告诉编译不需要优化,这在linux下是通过barrier()宏完成的,它依靠volidate关键字和memory关键字,前者告诉编译barrier()周围的指令不要被优化,后者作用是告诉编译器汇编代码会使内存里面的值更改,编译器应使用内存里的新值而非寄存器里保存的老值。

同样,CPU执行会通过乱序以提高性能。汇编里的指令不一定是按照我们看到的顺序执行的。linux中通过mb()系列宏来保证执行的顺序。简单的说,如果在程序某处插入了mb()/rmb()/wmb()宏,则宏之前的程序保证比宏之后的程序先执行,从而实现串行化。

即使是编译器生成的汇编码有序,处理器也不一定能保证有序。就算编译器生成了有序的汇编码,到了处理器那里也拿不准是不 是会按照代码顺序执行。所以就算编译器保证有序了,程序员也还是要往代码里面加内存屏障才能保证绝对访存有序,这倒不如编译器干脆不管算了,因为内存屏障 本身就是一个sequence point,加入后已经能够保证编译器也有序。

处理器虽然乱序执行,但最终会得出正确的结果,所以逻辑上讲程序员本不需要关心处理器乱序的问题。但是在SMP并发执行的情况下,处理器无法知道并发程序之间的逻辑,比如,在不同core上的读者和写者之间的逻辑。简单讲,处理器只保证在单个core上按照code中的顺序给出最终结果。这就要求程序员通过mb()/rmb()/wmb()/read_barrier_depends来告知处理器,从而得到正确的并发结果。内存屏障、数据依赖屏障都是为了处理SMP环境下的数据同步问题,UP根本不存在这个问题。

下面分析下内存屏障在RCU上的应用:

1
2
3
4
5
6
7
8
9
10
#define rcu_assign_pointer(p, v) ({ \
	smp_wmb();                      \
	(p)= (v);                       \
})

#define rcu_dereference(p) ({     \
	typeof(p)_________p1 = p;     \
	smp_read_barrier_depends();   \
	(_________p1);                \
}) 

rcu_assign_pointer()通常用于写者的发布,rcu_dereference()通常用于读者的订阅。

写者:

1
2
3
4
5
6
7
p->a = 1;
p->b = 2;
p->c = 3;
rcu_assign_pointer(gp, p);

// 如果gp的原值马上会被改变/释放,则需要synchronize_rcu()/synchronize_net(),
// 如: 模块的卸载, 原gp指向函数被释放

读者:

1
2
3
4
5
6
rcu_read_lock();
p = rcu_dereference(gp);
if (p != NULL) {
	do_something_with(p->a, p->b, p->c);
}
rcu_read_unlock();

rcu_assign_pointer()是说,先把那块内存写好,再把指针指过去。这里使用的内存写屏障是为了保证并发的读者读到数据一致性。在这条语句之前的读者读到旧的指针和旧的内存,这条语句之后的读者读到新的指针和新的内存。如果没有这条语句,很有可能出现读者读到新的指针和旧的内存。也就是说,这里通过内存屏障刷新了p所指向的内存的值,至于gp本身的值有没有更新还不确定。实际上,gp本身值的真正更新要等到并发的读者来促发。

rcu_dereference() 原语用的是数据依赖屏障,smp_read_barrier_dependence,它要求后面的读操作如果依赖前面的读操作,则前面的读操作需要首先完成。根据数据之间的依赖,要读p->a, p->b, p->c, 就必须先读p,要先读p,就必须先读p1,要先读p1,就必须先读gp。也就是说读者所在的core在进行后续的操作之前,gp必须是同步过的当前时刻的最新值。如果没有这个数据依赖屏障,有可能读者所在的core很长一段时间内一直用的是旧的gp值。所以,这里使用数据依赖屏障是为了督促写者将gp值准备好,是为了呼应写者,这个呼应的诉求是通过数据之间的依赖关系来促发的,也就是说到了非呼应不可的地步了。

下面看看kernel中常用的链表操作是如何使用这样的发布、订阅机制的:

写者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline void list_add_rcu(struct list_head *new, struct list_head *head)
{
	__list_add_rcu(new, head, head->next);
}

static inline void __list_add_rcu(struct list_head * new,
struct list_head * prev, struct list_head * next)
{
	new->next = next;
	new->prev = prev;
	smp_wmb();
	next->prev = new;
	prev->next = new;
}

读者:

1
2
3
4
5
#define list_for_each_entry_rcu(pos, head, member)                \
	for(pos = list_entry((head)->next, typeof(*pos), member);     \
			prefetch(rcu_dereference(pos)->member.next),          \
			&pos->member!= (head);                                \
		pos= list_entry(pos->member.next, typeof(*pos), member))

写者通过调用list_add_rcu来发布新的节点,其实是发布next->prev, prev->next这两个指针。读者通过list_for_each_entry_rcu来订阅这连个指针,我们将list_for_each_entry_rcu订阅部分简化如下:

1
2
pos = prev->next;
prefetch(rcu_dereference(pos)->next);

读者通过rcu_dereference订阅的是pos,而由于数据依赖关系,又间接订阅了prev->next指针,或者说是促发prev->next的更新。

下面介绍下其他相关链表操作的函数:

safe版本的iterate的函数?为什么就safe了?

1
2
3
4
5
6
7
#define list_for_each_safe(pos,n, head)                    \
	for(pos = (head)->next, n = pos->next; pos != (head);  \
			pos= n, n = pos->next)

#define list_for_each(pos, head)                                \
	for(pos = (head)->next; prefetch(pos->next), pos != (head); \
			pos= pos->next)

当在iterate的过程中执行删除操作的时候,比如:

1
2
list_for_each(pos,head)
	list_del(pos)

这样会断链,为了避免这种断链,增加了safe版本的iterate函数。另外,由于preftech的缘故,有可能引用一个无效的指针LIST_POISON1。这里的safe是指,为避免有些cpu的preftech的影响,干脆在iterate的过程中去掉preftech。

还有一个既有rcu+safe版本的iterative函数:

1
2
3
4
#define list_for_each_safe_rcu(pos, n, head)              \
	for(pos = (head)->next;                               \
			n= rcu_dereference(pos)->next, pos != (head); \
			pos= n)

只要用这个版本的iterate函数,就可以和多个_rcu版本的写操作(如:list_add_rcu())并发执行。

kmalloc、vmalloc、malloc的区别

blog.csdn.net/macrossdzh/article/details/5958368

简单的说:
kmalloc和vmalloc是分配的是内核的内存,malloc分配的是用户的内存
kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续,malloc不保证任何东西(这点是自己猜测的,不一定正确)
kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相对较大
内存只有在要被DMA访问的时候才需要物理上连续
vmalloc比kmalloc要慢

详细的解释:
对于提供了MMU(存储管理器,辅助操作系统进行内存管理,提供虚实地址转换等硬件支持)的处理器而言,Linux提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB。 进程的4GB内存空间被人为的分为两个部分–用户空间与内核空间。用户空间地址分布从0到3GB(PAGE_OFFSET,在0x86中它等于0xC0000000),3GB到4GB为内核空间。
内核空间中,从3G到vmalloc_start这段地址是物理内存映射区域(该区域中包含了内核镜像、物理页框表mem_map等等),比如我们使用 的 VMware虚拟系统内存是160M,那么3G~3G+160M这片内存就应该映射物理内存。在物理内存映射区之后,就是vmalloc区域。对于 160M的系统而言,vmalloc_start位置应在3G+160M附近(在物理内存映射区与vmalloc_start期间还存在一个8M的gap 来防止跃界),vmalloc_end的位置接近4G(最后位置系统会保留一片128k大小的区域用于专用页面映射)

kmalloc和get_free_page申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系,virt_to_phys()可以实现内核虚拟地址转化为物理地址:

1
2
3
4
5
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
extern inline unsigned long virt_to_phys(volatile void * address)
{
	return __pa(address);
}

上面转换过程是将虚拟地址减去3G(PAGE_OFFSET=0XC000000)。

与之对应的函数为phys_to_virt(),将内核物理地址转化为虚拟地址:

1
2
3
4
5
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
extern inline void * phys_to_virt(unsigned long address)
{
	return __va(address);
}

virt_to_phys()和phys_to_virt()都定义在include/asm-i386/io.h中。

而vmalloc申请的内存则位于vmalloc_start~vmalloc_end之间,与物理地址没有简单的转换关系,虽然在逻辑上它们也是连续的,但是在物理上它们不要求连续。


blog.csdn.net/kris_fei/article/details/17243527

平台: msm8x25
系统: android 4.1
内核: 3.4.0

概念

由于系统的连续物理内存有限,这使得非连续物理内存的使用在linux内核中出现,这叫vmalloc机制。和前者一样,vmalloc机制中的虚拟地址也是连续的。

Vmallocinfo

Vmalloc机制并不是狭义地指使用vmalloc函数分配,其他还有如ioremap, iotable_init等。可以从/proc/vmallocinfo获取到此信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#cat /proc/vmallocinfo
0xf3600000-0xf36ff0001044480 binder_mmap+0xb0/0x224 ioremap
………..
0xf6680000-0xf66c1000 266240 kgsl_page_alloc_map_kernel+0x98/0xe8 ioremap
0xf6700000-0xf67ff0001044480 binder_mmap+0xb0/0x224 ioremap
…………….
0xf6f00000-0xf6f41000 266240 kgsl_page_alloc_map_kernel+0x98/0xe8 ioremap
0xf7200000-0xf72ff0001044480 binder_mmap+0xb0/0x224 ioremap
0xfa000000-0xfa001000   4096 iotable_init+0x0/0xb0 phys=c0800000 ioremap
……………..
0xfa105000-0xfa106000   4096 iotable_init+0x0/0xb0 phys=a9800000 ioremap
0xfa200000-0xfa3000001048576 pmd_empty_section_gap+0x0/0x3c ioremap
0xfa300000-0xfa4000001048576 iotable_init+0x0/0xb0 phys=100000 ioremap
0xfa400000-0xfa5000001048576 iotable_init+0x0/0xb0 phys=aa500000 ioremap
0xfa500000-0xfa6000001048576 pmd_empty_section_gap+0x0/0x3c ioremap
0xfa701000-0xfa702000   4096 iotable_init+0x0/0xb0 phys=c0400000 ioremap
…………..
0xfa800000-0xfa9000001048576 pmd_empty_section_gap+0x0/0x3c ioremap
0xfa900000-0xfb60000013631488 iotable_init+0x0/0xb0 phys=ac000000 ioremap
0xfefdc000-0xff000000 147456 pcpu_get_vm_areas+0x0/0x56c vmalloc

上面的列数意思依次是:虚拟地址,分配大小,哪个函数分配的,物理地址,分配类型。

后面会提到vmalloc size的划分是按照此info来修改的。

分配标志

是否划分到vamlloc区域主要是以下重要的标志来决定的:

File: kernel/include/linux/vmalloc.h

1
2
3
4
5
6
7
8
/* bits in flags ofvmalloc's vm_struct below */
#defineVM_IOREMAP    0x00000001     /* ioremap()and friends */
#define VM_ALLOC     0x00000002     /* vmalloc() */
#defineVM_MAP        0x00000004     /* vmap()ed pages */
#defineVM_USERMAP    0x00000008     /* suitable forremap_vmalloc_range */
#defineVM_VPAGES     0x00000010     /* buffer for pages was vmalloc'ed */
#defineVM_UNLIST     0x00000020     /* vm_struct is not listed in vmlist */
/* bits [20..32]reserved for arch specific ioremap internals */

Vmallocinfo中的函数,你可以对照源码看一下,在设置flag的时候就会有VM_IOREMAP, VM_ALLOC这些标志。

Vmalloc区域

Vmalloc的区域是由两个宏变量来表示: VMALLOC_START,VMALLOC_END.

File: kernel/arch/arm/include/asm/pgtable.h

1
2
3
#defineVMALLOC_OFFSET       (8*1024*1024)
#defineVMALLOC_START        (((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1))
#defineVMALLOC_END          0xff000000UL

VMALLOC_START:看上去会随着high_memory的值变化。

VMALLOC_OFFSET:系统会在low memory和VMALLOC区域留8M,防止访问越界。因此假如理论上vmalloc size有300M,实际可用的也是只有292M。

File: kernel/Documentation/arm/memory.txt有给出更好的解释:

1
VMALLOC_START   VMALLOC_END-1    vmalloc() / ioremap() space. Memory returned byvmalloc/ioremap will be dynamically placed in this region. Machine specificstatic mappings are also located here through iotable_init(). VMALLOC_START isbased upon the value of the high_memoryvariable, and VMALLOC_END is equal to 0xff000000.

下图摘自网络,看下VMALLOC_START和VMALLOC_END的位置。0xc0000000到VMALLOC_START为low memory虚拟地址区域。

Vmallocsize 计算

有了以上知识后我们看下vmalloc size是如何分配的,目前有两种方法,kernel默认分配一个, 以及开机从cmdline分配。

1. 从cmdline分配

File: device/qcom/msm7627a/BoardConfig.mk

BOARD_KERNEL_CMDLINE := androidboot.hardware=qcom loglevel=7vmalloc=200M

上面的值在build的时候会被赋值给kernel 的cmdline。

开机的时候early_vmalloc()会去读取vmalloc这个值。

File: kernel/arch/arm/mm/mmu.c

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
static int__init early_vmalloc(char *arg)
{
	/*cmdline中的vmalloc会被解析到vmlloc_reserve中。*/
	unsigned long vmalloc_reserve = memparse(arg, NULL);

	/*小于16M则用16M。*/
	if (vmalloc_reserve < SZ_16M) {
		vmalloc_reserve = SZ_16M;
		printk(KERN_WARNING
				"vmalloc area too small, limiting to %luMB\n",
				vmalloc_reserve >> 20);
	}

	/*大于可用虚拟地址内存则使用可用地址部分再减去32M。*/
	if (vmalloc_reserve > VMALLOC_END - (PAGE_OFFSET + SZ_32M)) {
		vmalloc_reserve = VMALLOC_END - (PAGE_OFFSET + SZ_32M);
		printk(KERN_WARNING
				"vmalloc area is too big, limiting to %luMB\n",
				vmalloc_reserve >> 20);
	}

	/*计算偏移起始地址。*/
	vmalloc_min = (void *)(VMALLOC_END - vmalloc_reserve);
	return 0;
}
early_param("vmalloc",early_vmalloc);

vmalloc_min会影响arm_lowmem_limit,arm_lowmem_limit其实就是high_memory。因为此过程不是我们要分析的重点,如果有兴趣可分析kernel/arch/arm/mm/mmu.c中的sanity_check_meminfo()函数。

所以,VMALLOC_START受到了hight_memory的影响而发生了变化,最终使得vmalloc size也变化了!

2. 开机默认分配:

File: kernel/arch/arm/mm/mmu.c

1
2
static void * __initdata vmalloc_min =
	(void *)(VMALLOC_END - (240 << 20) - VMALLOC_OFFSET);

当cmdline无vmalloc参数传进来的时候,early_vmalloc()函数也不会调用到,vmalloc_min的值就会被默认传进来了,默认是240M。

后面的步骤和方法1一样了!

开机log有memory layout 信息:

1
2
3
4
5
6
7
8
9
10
11
[    0.000000] [cpuid: 0] Virtual kernelmemory layout:
[    0.000000] [cpuid:0]     vector  : 0xffff0000 - 0xffff1000  (   4 kB)
[    0.000000] [cpuid:0]     fixmap  : 0xfff00000 - 0xfffe0000   (896 kB)
[    0.000000] [cpuid:0]     vmalloc : 0xf3000000 - 0xff000000   ( 192MB)
[    0.000000] [cpuid:0]     lowmem  : 0xc0000000 - 0xf2800000   (808 MB)
[    0.000000] [cpuid:0]     pkmap   : 0xbfe00000 -0xc0000000   (   2 MB)
[    0.000000] [cpuid:0]     modules : 0xbf000000 - 0xbfe00000  (  14 MB)
[    0.000000] [cpuid:0]       .text : 0xc0008000 -0xc0893034   (8749 kB)
[    0.000000] [cpuid:0]       .init : 0xc0894000 -0xc08cdc00   ( 231 kB)
[    0.000000] [cpuid:0]       .data : 0xc08ce000 -0xc09f8eb8   (1196 kB)
[    0.000000] [cpuid:0]        .bss : 0xc0a78eec -0xc0f427a8   (4903 kB)

其中看到vmalloc为192MB , cmdline中使用vmllaoc就是200M。

Lowmem为地段内存部分,可见lowmem和vmalloc中间有8M空隙。

Vmalloc该分配多大?

Linux内核版本从3.2到3.3 默认的vmalloc size由128M 增大到了240M,3.4.0上的

修改Commit信息如下:

To accommodate all static mappings on machines withpossible highmem usage, the default vmalloc area size is changed to 240 MB sothat VMALLOC_START is no higher than 0xf0000000 by default.

看其意思是因为开机的静态映射增加了,所以要扩大。

另外3.2到3.3版本的一个重大变化是将android引入到主线内核中。我想增大vmalloc size到240M是基于此考虑吧。当然,各家厂商都也可以基于自己平台来动态修改size的。

那么如何判断当前vmalloc size不足呢?

/proc/meminfo中有vmalloc信息:
VmallocTotal: 540672 kB
VmallocUsed: 165268 kB
VmallocChunk: 118788kB

事实上这里的VmallocUsed只是表示已经被真正使用掉的vmalloc区域,但是区域之前的空隙也就是碎片没有被计算进去。

所以,回到前面说的/proc/vmallocinfo,假设我们的vmalloc size就是200M。那么区域为0xf3000000- 0xff000000,从vmallocinfo中可以看到,前面大部分虚拟地址空间都用掉了,剩下0xfb600000到0xfefdc000这57M空间,假如申请了64M,那么就会失败了。

开机分配使用掉vmalloc之后到底该剩余多少目前没有具体依据,一般来说1GB RAM可以设置为400~600M。

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