Linux内存管理:NUMA技术详解(非一致内存访问架构)

一.背景

所谓物理内存,就是安装在机器上的,实打实的内存设备(不包括硬件cache),被CPU通过总线访问。在多核系统中,如果物理内存对所有CPU来说没有区别,每个CPU访问内存的方式也一样,则这种体系结构被称为Uniform Memory Access(UMA)。

如果物理内存是分布式的,由多个cell组成(比如每个核有自己的本地内存),那么CPU在访问靠近它的本地内存的时候就比较快,访问其他CPU的内存或者全局内存的时候就比较慢,这种体系结构被称为Non-Uniform Memory Access(NUMA)。

以上是硬件层面上的NUMA,而作为软件层面的Linux,则对NUMA的概念进行了抽象。即便硬件上是一整块连续内存的UMA,Linux也可将其划分为若干的node。同样,即便硬件上是物理内存不连续的NUMA,Linux也可将其视作UMA。

所以,在Linux系统中,你可以基于一个UMA的平台测试NUMA上的应用特性。从另一个角度,UMA就是只有一个node的特殊NUMA,所以两者可以统一用NUMA模型表示。

传统的SMP(对称多处理器)中,所有处理器都共享系统总线,因此当处理器的数目增大时,系统总线的竞争冲突加大,系统总线将成为瓶颈,所以目前SMP系统的CPU数目一般只有数十个,可扩展能力受到极大限制。NUMA技术有效结合了SMP系统易编程性和MPP(大规模并行)系统易扩展性的特点,较好解决了SMP系统的可扩展性问题,已成为当今高性能服务器的主流体系结构之一。

在NUMA系统中,当Linux内核收到内存分配的请求时,它会优先从发出请求的CPU本地或邻近的内存node中寻找空闲内存,这种方式被称作local allocation,local allocation能让接下来的内存访问相对底层的物理资源是local的。

每个node由一个或多个zone组成(我们可能经常在各种对虚拟内存和物理内存的描述中迷失,但以后你见到zone,就知道指的是物理内存),每个zone又由若干page frames组成(一般page frame都是指物理页面)。

基于NUMA架构的高性能服务器有HP的Superdome、SGI的Altix 3000、IBM的 x440、NEC的TX7、AMD的Opteron等。

概念

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保证公平性。

二.NUMA存储管理

NUMA系统是由多个结点通过高速互连 络连接而成的,如图1是SGI Altix 3000 ccNUMA系统中的两个结点。

NUMA系统的结点通常是由一组CPU(如,SGI Altix 3000是2个Itanium2 CPU)和本地内存组成,有的结点可能还有I/O子系统。由于每个结点都有自己的本地内存,因此全系统的内存在物理上是分布的,每个结点访问本地内存和访问其它结点的远地内存的延迟是不同的,为了减少非一致性访存对系统的影响,在硬件设计时应尽量降低远地内存访存延迟(如通过Cache一致性设计等),而操作系统也必须能感知硬件的拓扑结构,优化系统的访存。

目前IA64 Linux所支持的NUMA架构服务器的物理拓扑描述是通过ACPI(Advanced Configuration and Power Interface)实现的。ACPI是由Compaq、Intel、Microsoft、Phoenix和Toshiba联合制定的BIOS规范,它定义了一个非常广泛的配置和电源管理,目前该规范的版本已发展到2.0,3.0o版本正在制定中,具体信息可以从 http://www.acpi.info 站上获得。ACPI规范也已广泛应用于IA-32架构的至强服务器系统中。

Linux对NUMA系统的物理内存分布信息是从系统firmware的ACPI表中获得的,最重要的是SRAT(System Resource Affinity Table)和SLIT(System Locality Information Table)表,其中SRAT包含两个结构:

  • Processor Local APIC/SAPIC Affinity Structure:记录某个CPU的信息;
  • Memory Affinity Structure:记录内存的信息;
  • SLIT表则记录了各个结点之间的距离,在系统中由数组node_distance[ ]记录。

    Linux采用Node、Zone和页三级结构来描述物理内存的,如图2所示,

    图2 Linux中Node、Zone和页的关系

    相关视频推荐

    90分钟了解Linux内存架构,numa的优势,slab的实现,vmalloc的原理

    linux内存管理问题-如何理出自己的思路出来,开发与面试双丰收

    学习地址:C/C++Linux鏈嶅姟鍣ㄥ紑鍙?/鍚庡彴鏋舵瀯甯堛€愰浂澹版暀鑲层€?-瀛︿範瑙嗛鏁欑▼-鑵捐璇惧爞

    需要C/C++ Linux服务器架构师学习资料加qun812855908(资料包括C/C++,Linux,golang技术,内核,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg,大厂面试题 等)

    2.1 结点

    Linux用一个struct pg_data_t结构来描述系统的内存,系统中每个结点都挂接在一个pgdat_list列表中,对UMA体系结构,则只有一个静态的pg_data_t结构contig_page_data。对NUMA系统来说则非常容易扩充,NUMA系统中一个结点可以对应Linux存储描述中的一个结点,具体描述见linux/mmzone.h。

    typedef struct pglist_data {    zone_t node_zones[MAX_NR_ZONES];    zonelist_t node_zonelists[GFP_ZONEMASK+1];    int nr_zones;    struct page *node_mem_map;    unsigned long *valid_addr_bitmap;    struct bootmem_data *bdata;    unsigned long node_start_paddr;    unsigned long node_start_mapnr;    unsigned long node_size;    int node_id;    struct pglist_data *node_next;} pg_data_t;

    下面就该结构中的主要域进行说明,

    说明

    Node_zones

    该结点的zone类型,一般包括ZONE_HIGHMEM、ZONE_NORMAL和ZONE_DMA三类

    Node_zonelists

    分配时内存时zone的排序。它是由free_area_init_core()通过page_alloc.c中的build_zonelists()设置zone的顺序

    nr_zones

    该结点的 zone 个数,可以从 1 到 3,但并不是所有的结点都需要有 3 个 zone

    node_mem_map

    它是 struct page 数组的第一页,该数组表示结点中的每个物理页框。根据该结点在系统中的顺序,它可在全局 mem_map 数组中的某个位置

    Valid_addr_bitmap

    用于描述结点内存空洞的位图

    node_start_paddr

    该结点的起始物理地址

    node_start_mapnr

    给出在全局 mem_map 中的页偏移,在free_area_init_core() 计算在 mem_map 和 lmem_map 之间的该结点的页框数目

    node_size

    该 zone 内的页框总数

    node_id

    该结点的 ID,全系统结点 ID 从 0 开始

    系统中所有结点都维护在 pgdat_list 列表中,在 init_bootmem_core 函数中完成该列表初始化工作。

    影响zonelist方式

    采用Node方式组织的zonelist为:

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

    zonelist[2]

    配置NUMA后,每个节点将关联2个zonelist:

    1) zonelist[0]中存放以Node方式或Zone方式组织的zonelist,包括所有节点的zone;

    2) zonelist[1]中只存放本节点的zone即Legacy方式;

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

    Page Frame

    虽然内存访问的最小单位是byte或者word,但MMU是以page为单位来查找页表的,page也就成了Linux中内存管理的重要单位。包括换出(swap out)、回收(relcaim)、映射等操作,都是以page为粒度的。

    因此,描述page frame的struct page自然成为了内核中一个使用频率极高,非常重要的结构体,来看下它是怎样构成的(为了讲解需要并非最新内核代码):

    struct page {    unsigned long flags;    atomic_t count;      atomic_t _mapcount;     struct list_head lru;    struct address_space *mapping;    unsigned long index;     ...  }
  • flags表示page frame的状态或者属性,包括和内存回收相关的PG_active, PG_dirty, PG_writeback, PG_reserved, PG_locked, PG_highmem等。其实flags是身兼多职的,它还有其他用途,这将在下文中介绍到。
  • count表示引用计数。当count值为0时,该page frame可被free掉;如果不为0,说明该page正在被某个进程或者内核使用,调用page_count()可获得count值。
  • _mapcount表示该page frame被映射的个数,也就是多少个page table entry中含有这个page frame的PFN。
  • lru是”least recently used”的缩写,根据page frame的活跃程度(使用频率),一个可回收的page frame要么挂在active_list双向链表上,要么挂在inactive_list双向链表上,以作为页面回收的选择依据,lru中包含的就是指向所在链表中前后节点的指针(参考这篇文章)。
  • 如果一个page是属于某个文件的(也就是在page cache中),则mapping指向文件inode对应的address_space(这个结构体虽然叫address_space,但并不是进程地址空间里的那个address space),index表示该page在文件内的offset(以page size为单位)。
  • 有了文件的inode和index,当这个page的内容需要和外部disk/flash上对应的部分同步时,才可以找到具体的文件位置。如果一个page是anonymous的,则mapping指向表示swap cache的swapper_space,此时index就是swapper_space内的offset。

    事实上,现在最新Linux版本的struct page实现中大量用到了union,也就是同一个元素在不同的场景下有不同的意义。这是因为每个page frame都需要一个struct page来描述,一个page frame占4KB,一个struct page占32字节,那所有的struct page需要消耗的内存占了整个系统内存的32/4096,不到1%的样子,说小也小,但一个拥有4GB物理内存的系统,光这一项的开销最大就可达30多MB。

    如果能在struct page里省下4个字节,那就能省下4多MB的内存空间,所以这个结构体的设计必须非常考究,不能因为多一种场景的需要就在struct page中增加一个元素,而是应该尽量采取复用的方式。

    需要注意的是,struct page描述和管理的是这4KB的物理内存,它并不关注这段内存中的数据变化。

    2.2 Zone

    每个结点的内存被分为多个块,称为zones,它表示内存中一段区域。一个zone用struct_zone_t结构描述,zone的类型主要有ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。ZONE_DMA位于低端的内存空间,用于某些旧的ISA设备。

    ZONE_NORMAL的内存直接映射到Linux内核线性地址空间的高端部分,许多内核操作只能在ZONE_NORMAL中进行。

    因为硬件的限制,内核不能对所有的page frames采用同样的处理方法,因此它将属性相同的page frames归到一个zone中。对zone的划分与硬件相关,对不同的处理器架构是可能不一样的。

    ?比如在i386中,一些使用DMA的设备只能访问0~16MB的物理空间,因此将0~16MB划分为了ZONE_DMA。ZONE_HIGHMEM则是适用于要访问的物理地址空间大于虚拟地址空间,不能建立直接映射的场景。除开这两个特殊的zone,物理内存中剩余的部分就是ZONE_NORMAL了。

    例如,在X86中,zone的物理地址如下:

    类型

    地址范围

    ZONE_DMA

    前16MB内存

    ZONE_NORMAL

    16MB – 896MB

    ZONE_HIGHMEM

    896 MB以上

    Zone是用struct zone_t描述的,它跟踪页框使用、空闲区域和锁等信息,具体描述如下:

    typedef struct zone_struct {    spinlock_t lock;    unsigned long free_pages;    unsigned long pages_min, pages_low, pages_high;    int need_balance;    free_area_t free_area[MAX_ORDER];    wait_queue_head_t * wait_table;    unsigned long wait_table_size;    unsigned long wait_table_shift;    struct pglist_data *zone_pgdat;    struct page *zone_mem_map;    unsigned long zone_start_paddr;    unsigned long zone_start_mapnr;char *name;unsigned long size;} zone_t;

    在其他一些处理器架构中,ZONE_DMA可能是不需要的,ZONE_HIGHMEM也可能没有。比如在64位的x64中,因为内核虚拟地址空间足够大,不再需要ZONE_HIGH映射,但为了区分使用32位地址的DMA应用和使用64位地址的DMA应用,64位系统中设置了ZONE_DMA32和ZONE_DMA。

    所以,同样的ZONE_DMA,对于32位系统和64位系统表达的意义是不同的,ZONE_DMA32则只对64位系统有意义,对32位系统就等同于ZONE_DMA,没有单独存在的意义。

    此外,还有防止内存碎片化的ZONE_MOVABLE和支持设备热插拔的ZONE_DEVICE。可通过“cat /proc/zoneinfo |grep Node”命令查看系统中包含的zones的种类。

    [rongtao@toa ~]$ cat /proc/zoneinfo |grep NodeNode 0, zone      DMANode 0, zone    DMA32[rongtao@toa ~]$ 

    下面就该结构中的主要域进行说明,

    ?当系统中可用的内存比较少时,kswapd将被唤醒,并进行页交换。如果需要内存的压力非常大,进程将同步释放内存。如前面所述,每个zone有三个阈值,称为pages_low,pages_min和pages_high,用于跟踪该zone的内存压力。pages_min的页框数是由内存初始化free_area_init_core函数,根据该zone内页框的比例计算的,最小值为20页,最大值一般为255页。当到达pages_min时,分配器将采用同步方式进行kswapd的工作;当空闲页的数目达到pages_low时,kswapd被buddy分配器唤醒,开始释放页;当达到pages_high时,kswapd将被唤醒,此时kswapd不会考虑如何平衡该zone,直到有pages_high空闲页为止。一般情况下,pages_high缺省值是pages_min的3倍。

    Linux存储管理的这种层次式结构可以将ACPI的SRAT和SLIT信息与Node、Zone实现有效的映射,从而克服了传统Linux中平坦式结构无法反映NUMA架构的缺点。当一个任务请求分配内存时,Linux采用局部结点分配策略,首先在自己的结点内寻找空闲页;如果没有,则到相邻的结点中寻找空闲页;如果还没有,则到远程结点中寻找空闲页,从而在操作系统级优化了访存性能。

    Zone虽然是用于管理物理内存的,但zone与zone之间并没有任何的物理分割,它只是Linux为了便于管理进行的一种逻辑意义上的划分。Zone在Linux中用struct zone表示(以下为了讲解需要,调整了结构体中元素的顺序):

    struct zone {     spinlock_t         lock;      unsigned long      spanned_pages;     unsigned long      present_pages;      unsigned long      nr_reserved_highatomic;         atomic_long_t      managed_pages;      struct free_area   free_area[MAX_ORDER];     unsigned long      _watermark[NR_WMARK];     long               lowmem_reserve[MAX_NR_ZONES];     atomic_long_t      vm_stat[NR_VM_ZONE_STAT_ITEMS];      unsigned long      zone_start_pfn;     struct pglist_data *zone_pgdat;     struct page        *zone_mem_map;     ...    } 
  • lock是用来防止并行访问struct zone的spin lock,它只能保护struct zone这个结构体哈,可不能保护整个zone里的所有pages。
  • spanned_pages是这个zone含有的总的page frames数目。在某些体系结构(比如Sparc)中,zone中可能存在没有物理页面的”holes”,spanned_pages减去这些holes里的absent pages就是present_pages。
  • nr_reserved_highatomic是为某些场景预留的内存,managed_pages是由buddy内存分配系统管理的page frames数目,其实也就是present_pages减去reserved pages。

  • free_area由free list空闲链表构成,表示zone中还有多少空余可供分配的page frames。_watermark有min(mininum), low, high三种,可作为启动内存回收的判断标准
  • zone_start_pfn是zone的起始物理页面 ,zone_start_pfn+spanned_pages就是该zone的结束物理页面 。zone_pgdat是指向这个zone所属的node的。zone_mem_map指向由struct page构成的mem_map数组。
  • 因为内核对zone的访问是很频繁的,为了更好的利用硬件cache来提高访问速度,struct zone中还有一些填充位,用于帮助结构体元素的cache line对齐。这和struct page对内存精打细算的使用形成了鲜明的对比,因为zone的种类很有限,一个系统中一共也不会有多少个zones,struct zone这个结构体的体积大点也没有什么关系。

    Node Distance

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

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

    三、NUMA调度器

    NUMA系统中,由于局部内存的访存延迟低于远地内存访存延迟,因此将进程分配到局部内存附近的处理器上可极大优化应用程序的性能。Linux 2.4内核中的调度器由于只设计了一个运行队列,可扩展性较差,在SMP平台表现一直不理想。当运行的任务数较多时,多个CPU增加了系统资源的竞争,限制了负载的吞吐率。在2.5内核开发时,Ingo Molnar写了一个多队列调度器,称为O(1),从2.5.2开始O(1)调度器已集成到2.5内核版本中。O(1)是多队列调度器,每个处理器都有一条自己的运行队列,但由于O(1)调度器不能较好地感知NUMA系统中结点这层结构,从而不能保证在调度后该进程仍运行在同一个结点上,为此,Eirch Focht开发了结点亲和的NUMA调度器,它是建立在Ingo Molnar的O(1)调度器基础上的,Eirch将该调度器向后移植到2.4.X内核中,该调度器最初是为基于IA64的NUMA机器的2.4内核开发的,后来Matt Dobson将它移植到基于X86的NUMA-Q硬件上。

    3.1 初始负载平衡

    在每个任务创建时都会赋予一个HOME结点(所谓HOME结点,就是该任务获得最初内存分配的结点),它是当时创建该任务时全系统负载最轻的结点,由于目前Linux中不支持任务的内存从一个结点迁移到另一个结点,因此在该任务的生命期内HOME结点保持不变。一个任务最初的负载平衡工作(也就是选该任务的HOME结点)缺省情况下是由exec()系统调用完成的,也可以由fork()系统调用完成。在任务结构中的node_policy域决定了最初的负载平衡选择方式。

    声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

    上一篇 2022年7月10日
    下一篇 2022年7月10日

    相关推荐