一、 前言
非一致性内存访问(Non-Uniform Memory Access)结构是分布式共享内存(Distributed Shared Memory)体系结构的主要分支,它通过结合分布式内存技术和单一系统映像(SSI)技术,实现了SMP 系统的易编程性和 MPP系统的易扩展性的折中,已成为当今高性能服务器的主流体系结构之一。目前国外著名的服务器厂商都先后推出了基于 NUMA 架构的高性能服务器,如HP的Superdome、SGI 的 Altix 3000、Origin 3000、IBM 的 x440、NEC 的 TX7、AMD 的Opteron 等。
随着NUMA架构的高性能服务器被逐渐推广,系统软件针对这种分布式共享内存架构的特点,在调度器、存储管理和用户级接口等方面进行了大量的优化工作。例如,SGI的Origin 3000 ccNUMA系统在许多领域得到了广泛应用,是个非常成功的系统,为了优化Origin 3000的性能,SGI的IRIX操作系统在其上实现了CpuMemSets,通过将应用与处理器和内存的绑定,充分发挥NUMA系统本地访存的优势。Linux社区在自己的NUMA项目中也实现了CpuMemSets,并且在SGI的Altix 3000的服务器中得到实际应用。
本文将以 SGI 的 ProPack v2.2 为研究对象,分析 CpuMemSets 在Linux-2.4.20 中的具体实现。CpuMemSets 是 SGI 进行的一个开放源码项目,由针对 Linux2.4 内核的补丁、用户库、python 模块和 runon 等命令共四部分组成,以实现处理器和内存块的分区为目标,控制系统资源(处理器、内存块)面向内核、任务以及虚拟存储区的分配,为 dplace、RunOn 等 NUMA 工具提供支持,最终优化 Linux 系统的 NUMA 性能。
二、 相关工作
分区技术(Partition)最初出现在大型机(MainFrame)上,如今被广泛应用到服务器领域,支持在单个服务器上运行一个操作系统的多个实例或者多个操作系统的多个实例,主要特点是"机器独立、屏障可靠、单点管理"。在分区技术支持下,当前多台服务器运行的多个操作系统就可以在同一地点的一台服务器上同时运行,优于在一个组织中四处分散用多个服务器来支持不同的操作系统,从而有效地实现了服务器整合。支持分区技术的服务器可以当作应用服务器,运行Windows平台供市场部门使用;同时还可以运行Linux系统供工程部门使用。还可以在大多数用户运行Windows 2000 Advanced Server系统的同时,在另一个分区中为发展组测试其它操作系统;或者所有节点都应用在一个操作系统环境下。各种分区实现技术的主要差别体现在分区故障隔离手段(硬件或软件)、分区资源粒度、分区资源灵活性以、虚拟分区资源以及对动态分区重构的支持等方面。典型的有IBM的LPAR和DLAPAR(AIX 5L 5.1)、HP的nPartitions和vPartitions(HP-UX 11i)、SUN的Dynamic Domains(Solaris 8)、以及Compaq的Alpha Servers(Tru64 Unix 5.1)。但是,针对NUMA系统采用的分区技术与NUMA系统本身具有的单系统映像优势是矛盾的。
从用户的角度来看,NUMA系统提供了对本地主存和远程主存访问的透明性;但是,从性能的角度来看,由于存储模块物理上分布在不同的节点引起的存储访问延迟不一致现象,对系统的性能也带来了较大的影响。在这类系统中,一个节点对远程节点存储访问的延迟通常比本地访问延迟高1到2个数量级。页迁移与页复制是对数据进行动态局部性优化的主要方法之一。其实质是一种预测技术,根据收集到的信息预测将来对页面的访问情况,然后作出迁移或复制页面的决策。采用适当的页复制与页迁移策略可以减小cache容量和冲突失效,平衡远程和本地访问延迟的不一致,达到优化NUMA系统性能的目的。但是现有的页迁移与页复制策略大都过分依赖于体系结构和特殊的硬件支持,开销比较大,通用性也不好。
在NUMA结构的多处理器系统中,一个任务可以在任何一个处理器上运行,然而任务在各种情况的执行会被中断;被中断的任务在恢复执行的时候,如果选择恢复在另外一个处理器上执行,就会导致它失去原有的处理器cache数据。我们知道,访问cache数据只需要几个纳秒,而访问主存需要大约50纳秒。这时处理器运行的速度处在访问主存的级别上,直到任务运行了足够的时间,任务运行所需要的数据重新充满该处理器的cache为止。为解决这个问题,系统可以采用处理器亲近调度策略调度每个节点上的任务:系统记录下最后执行这个任务的处理器并维持这种关系,在恢复执行被中断的任务时,尽量恢复在最后执行这个任务的处理器上执行。但是,由于应用程序的特点各有不同,而且工作集具有动态属性,处理器亲近调度的作用是有限的。
用户是系统的使用者,也是性能的评判者,他们最清楚应用对系统的需求和评价指标。在一个大的NUMA系统中,用户往往希望控制一部分处理器和内存给某些特殊的应用。CpuMemSets允许用户更加灵活的控制(它可以重叠、划分系统的处理器和内存),允许多个进程将系统看成一个单系统映像,并且不需要重启系统,保障某些处理器和内存资源在不同的时间分配给指定的应用;也是对分区技术、页迁移和亲近调度策略的有益补充。
三、 系统实现
在介绍CpuMemSets在Linux-2.4.20中的具体实现之前,我们首先说明CpuMemSets涉及的几个基本概念,包括:
处理器:指承载任务调度的物理处理器,但是不包括DMA设备、向量处理器等专用处理器;
内存块:在SMP、UP系统中,所有的内存块与所有处理器的距离相等,因此不存在差别;但是在NUMA系统中,可以按照与处理器的距离对内存块划分等价类。此外,CpuMemSets不考虑具有速度差异的特殊存储器,如输入输出设备缓存、帧缓存等。
任务:一个任务,在任一时刻,或者等待事件、资源,或者被中断,或者在处理器上运行。
虚拟存储区:内核为每个任务维护的多个虚拟地址区域,可为多个任务共享。位于虚拟存储区内的页,或者没有分配,或者已分配但被换出,或者已分配且在内存中。可以指定允许分配给某个虚拟存储区的内存块以及分配顺序。
CpuMemSets为Linux提供了将系统服务和应用绑定在指定的处理器上进行调度、在指定的结点上分配内存的机制。CpuMemSets在已有的Linux调度和内存分配代码基础上增加了cpumemmap(cmm)和cpumemset(cms)两层结构,底层的cpumemmap层提供一个简单的映射对,实现系统的处理器号与应用的处理器号、系统的内存块号与应用的内存块号的映射。这种映射不一定是单射,一个系统号可以对应多个应用号。上层的cpumemset层负责说明允许把任务调度到哪些应用处理器号所对应的处理器上运行、可以从哪些应用内存块号所对应的内存块中为相应的内核或虚拟存储区分配内存页,也就是说,指定可供内核、任务、虚拟存储区使用的资源集合。在这种两层结构中,资源的系统号供内核执行调度和内存分配时使用;而资源的应用号供用户进程指定本应用的资源集合时使用。系统号在启动期间全系统范围内有效,而应用号仅仅相对于共享同一个cmm的所有用户进程有效。而且,由于负载平衡和热插拔引发的资源物理编号的变化对应用号是不可见的。
Linux的进程调度和内存分配在保持现有代码正常运转的基础上,添加了对CpuMemSets的支持,使用"系统处理器号"和"系统内存块号"以及其他数据结构如cpus_allowed和mems_allowed等实现资源的分区。此外,CpuMemSets的API提供了对cpusets、dplace、runon、psets、MPI、OpenMP、nodesets的支持,并且提供/proc接口以显示cmm和 cms的结构、设置以及与任务、虚拟存储区、内核的连接关系、系统资源号和应用资源号等信息。下面我们分别对cpumemmap和cpumemset、进程调度和内存分配、以及API这三个部分进行详细分析:
3.1 cmm&cms
3.1.1 数据结构
cpumemmap和cpumemset的数据结构如下所示,具体定义在include/linux/cpumemset.h中。Cpumemmap中的scpus和smems域分别指向一组系统处理器号和一组系统内存块号,实现应用的资源号(数组下标)与系统的资源号(数组元素值)的映射。Cpumemset中的acpus域指向一组应用处理器号,而amems域指向一组cms_memory_list_t类型的内存块列表。每个内存块列表描述了一组应用内存块号(mems)以及享有该列表的一组应用处理器号(cpus)。内存块分配策略由cpumemset中的policy域决定,缺省使用本地优先方式。Cpumemset通过cmm域与相应的cpumemmap建立关联。两个数据结构中的counter域的作用将在后文介绍。
【include/linux/cpumemset.h】
84 typedef struct cpumemmap {
85
int nr_cpus;
/* number of cpus in map */
86
int nr_mems;
/* number of mems in map */
87
cms_scpu_t *cpus;
/* array maps application to system cpu num */
88
cms_smem_t *mems;
/* array maps application to system mem num */
89
atomic_t counter;
/* reference counter */
90
} cpumemmap_t;
92 typedef struct cpumemset {
93
cms_setpol_t policy;
/* CMS_* policy flag :Memory allocation policy */
94
int nr_cpus;
/* Number of cpus in this CpuMemSet */
95
int nr_mems;
/* Number of Memory Lists in this CpuMemSet */
96
cms_acpu_t *cpus;
/* The 'nr_cpus' app cpu nums in this set */
97
cms_memory_list_t *mems;
/* Array 'nr_mems' Memory Lists */
98
unsigned long mems_allowed;
/* memory_allowed vector used by vmas */
99
cpumemmap_t *cmm;
/* associated cpumemmap */
100
atomic_t counter;
/* reference counter */