Linux-2.6.32 NUMA架構之記憶體和排程

FreeeLinux發表於2017-01-10

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


Linux-2.6.32 NUMA架構之記憶體和排程

 

本文將以XLP832通過ICI互連形成的NUMA架構進行分析,主要包括記憶體管理和排程兩方面,參考核心版本2.6.32.9NUMA架構常見配置選項有: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 CPUNode的關係

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

#ifdef CONFIG_NUMA

       build_node_cpu_map();

#endif

 

build_node_cpu_map()函式工作:

a) 確定CPUNode的相互關係,做法很簡單:

#define cpu_to_node(cpu)       (cpu >> 5)

 #define cpumask_of_node    (NODE_CPU_MASK(node)) /* node0:0~31; node1: 32~63 */

 

說明:XLP832每個節點有1個物理CPU,每個物理CPU8個核,每個核有4個超線

程,因此每個節點對應32個邏輯CPU,按節點依次展開。另外,實際物理存在的CPU

數目是通過DTB傳遞給核心的;numa_node_id()可以獲取當前CPU所處的Node號。

 

b) 設定每個物理存在的節點的線上狀態,具體是通過node_set_online()函式來設定全域性變數

nodemask_t node_states[];

   這樣,類似於CPU號,Node號也就具有如下功能巨集:

   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_BASEECFG_LIMIT指定(注:但這2個暫存器本身是處於PCIE配置空間之中的)

 

PCIE配置空間:

PCIE配置空間與memory-mapped實體地址的對映方式:

XLP832實現了所有裝置都位於虛擬匯流排0上,每個節點有8個裝置,按節點依次排開。

DRAM對映暫存器組:

每個節點都獨立實現有幾組不同型別的DRAM(每組有8個相同型別的)暫存器可以配置DRAM空間對映到實體地址空間中的基址和大小,以及所屬的節點資訊(這些暫存器的值事先會由bootloader設好);這組暫存器位於虛擬匯流排0的裝置0/8/16/24(依次對應每個節點的第一個裝置號)Function0(每個裝置最多可定義8Function,每個Function有著獨立的PCIE 4KB的配置空間)PCIE配置空間中(這個配置空間實現的是DRAM/Bridge控制器)

 

本小節涉及到的3組不同型別的暫存器(注:按索引對應即DRAM_BAR,DRAM_LIMIT DRAM_NODE_TRANSLATION描述一個記憶體區域屬性)

 

第一組(DRAM空間對映物理空間基址)

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空間對映物理空間長度)

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

 

第三組(節點相關)

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中初始化有所屬zonenode資訊,由set_page_links()函式設定)等。

4.1 NUMA帶來的變化

1) pglist_data

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 page

range, 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)巨集返回節點istruct pglist_data結構,需要在架構相關的mmzone.h中實現;

b) #define MAX_ZONELISTS 2,請參考後面的“zonelist初始化”。

2) zone

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個節點2zone為例(zone從高到低排列, ZONE_DMA0表示節點0ZONE_DMA,其它類似)

a) Legacy方式

       

       每個節點只排列自己的zone

        b)Node方式

按節點順序依次排列,先排列本地節點的所有zone,再排列其它節點的所有zone

 

c) Zone方式

zone型別從高到低依次排列各節點的同相型別zone

 

可通過啟動引數“numa_zonelist_order”來配置zonelist order,核心定義了3種配置:

#define ZONELIST_ORDER_DEFAULT  0 /* 智慧選擇NodeZone方式 */

#define ZONELIST_ORDER_NODE     1 /* 對應Node方式 */

#define ZONELIST_ORDER_ZONE     2 /* 對應Zone方式 */

 

預設配置為ZONELIST_ORDER_DEFAULT,由核心通過一個演算法來判斷選擇NodeZone方式,演算法思想:

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個節點2ZONE為例,各節點的佈局(4XLP832物理CPU級聯)值如下:

上圖中,Node0Node2Node Distance25Node1Node3Node Distance25,其它的Node Distance15

4.2.2.1 優先進行Zone Reclaim

另外,當Node Distance超過20的時候,核心會在某個zone分配記憶體不足的時候,提前啟用本zone的記憶體回收工作,由全域性變數zone_reclaim_mode控制,build_zonelists()中:

/*

               * 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的值來控制回收模式,含義如下:

#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後,每個節點將關聯2zonelist

1) zonelist[0]中存放以Node方式或Zone方式組織的zonelist,包括所有節點的zone

2) zonelist[1]中只存放本節點的zoneLegacy方式;

zonelist[1]用來實現僅從節點自身zone中的記憶體分配(參考__GFP_THISNODE標誌)

 

5 SLAB初始化

配置NUMA後對SLAB(本文不涉及SLOBSLUB)的初始化影響不大,只是在分配一些變數採用類似Buddy系統的per_cpu_pageset(單面頁快取)CPU本地節點進行記憶體分配。

5.1 NUMA帶來的變化

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[] 中快取每個CPUSLAB 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->nodeidnuma_node_id()當前節點不相等時),將加入到物件所在節點的alien快取中(如果不存在此alien快取,此物件不會被快取,而是直接釋放給此物件所屬SLAB),否則加入本地快取或共享快取(本地快取溢位且存在shared快取時);當alien快取滿的時候,會呼叫cache_free_alien()搬遷至shared快取中(如果不存在shared快取,直接釋放給SLAB)

slab->nodeid記錄本SLAB記憶體塊(若干個頁面)所在的節點。

 

示例

例如2個節點,CPU0~31位於Node0CPU32~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指標”;

 

2SLAB節點

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初始化為0use_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定義如下:

#ifdef CONFIG_NUMA

#define GFP_THISNODE     (__GFP_THISNODE | __GFP_NOWARN | __GFP_NORETRY)

 
 
排程初始化

配置NUMA後負載均衡會多一層NUMA排程域,根據需要在topology.h中定義,示例:

#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小於0node取本地節點號(node = numa_node_id())

2) NODE_DATA(node)得到node對應的struct pglist_data結構,從而得到zonelist[2]

3) 如果gfp_flags含有__GFP_THISNODE標誌,僅在此節點分配記憶體,使用node

點的Legacy zonelist[1],否則使用其包含所有節點zonezonelist[0] (4.2.2.3)

4) 遍歷確定出來的zonelist結構中包含的每一個符合要求的zonegfp_flags指定了本

次分配中的最高的zone,如__GFP_HIGHMEM表示最高的zoneZONE_HIGH

5) 分配結束。

7.1.2 SLAB顯式分配

kmem_cache_alloc_node(cachep, gfp_flags, node)分配流程:

1) 如果node值為-1node取本地節點號(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結構。

struct device {

    … …

#ifdef CONFIG_NUMA

       int          numa_node;    /* NUMA node this device is close to */

#endif

    … …

}

 

__netdev_alloc_skb()的實現:

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_INTERLEAVEMPOL_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結構來描述:

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結構中包含的每一個符合要求的zonegfp_flags指定了本

次分配中的最高的zone,如__GFP_HIGHMEM表示最高的zoneZONE_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) 限定一組任務所允許使用的記憶體NodeCPU資源;

2) CPUSET在核心各子系統中新增的檢測程式碼很少,對核心沒有效能影響;

3) CPUSET的限定優先順序高於記憶體策略(針對於Node)和繫結(針對於CPU)

4) 沒有額外實現系統呼叫介面,只能通過/proc檔案系統和使用者互動。

 

本節只講述CPUSET的使用方法和說明。

8.1建立CPUSET

因為CPUSET只能使用/proc檔案系統訪問,所以第一步就要先mount cpuset檔案系統,配置CONFIG_CGROUPSCONFIG_CPUSETS/proc/filesystems中將有這個檔案系統。

CPUSET是分層次的,可以在cpuset檔案系統根目錄是最頂層的CPUSET,可以在其下建立CPUSET子項,建立方式很簡單即建立一個新的目錄。

 

mount命令:mount nodev –t cpuset /your_dirmount 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.procsCGROUPS檔案,為此CPUSET包含的執行緒組tgid集合;

   echo 100 > cgroup.procs

 

3) cpusCPUSET檔案,表示此CPUSET允許的CPU

  echo 0-8 > cpus

 

4) memsCPUSET檔案,表示此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_exclusivemem_hardwall,為CPUSET檔案,表示記憶體硬牆標記;預設為0

表示軟牆;有關CPUSET的記憶體硬牆(HardWall)和記憶體軟牆(SoftWall),下文會介紹;

 

8) memory_spread_pagememory_spread_slab,為CPUSET檔案,設定CPUSET中的

任務PageCacheSLAB(建立時置有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上分配新的頁面(將資料同

步到新頁面),這樣就避免了此任務的非本地節點的記憶體訪問。

上圖為單Node8CPU的系統。

1) 頂層CPUSET包含了系統中的所有CPU以及Node,而且是隻讀的,不能更改;

2) 頂層CPUSET包含了系統中的所有任務,可以更改;

3) child為新建立的子CPUSET,子CPUSET的資源不能超過父CPUSET的資源;

4) 新建立的CPUSETmemscpus都是空的,使用前必須先初始化;

5) 新增任務:設定taskscgroup.procs檔案;

6) 刪除任務:將任務重新新增至其它CPUSET(如頂層)就可以從本CPUSET刪除任務。

8.3 利用CPUSET限定CPUNode

    設定步驟:

1) 在某個父CPUSET中建立子CPUSET

2) 在子CPUSET目錄下,輸入指定的Node號至mems檔案;

3) 在子CPUSET目錄下,輸入指定的Node號至mems檔案;

4) 在子CPUSET目錄下,設定任務至tasksgroup.procs檔案;

5) 還可以設定memory_migrate1,啟用記憶體頁面的遷移功能。

 

這樣限定後,此CPUSET中所有的任務都將使用限定的CPUNode,但畢竟系統中的任務並不能完全孤立,比如還是可能會全域性共享Page Cache,動態庫等資源,因此核心在某些情況下還是可以允許打破這個限制,如果不允許核心打破這個限制,需要設定CPUSET的記憶體硬牆標誌即mem_exclusivemem_hardwall1即可;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”的功能。

 

8CPU的系統中,系統中存在一個物理域,現需要禁掉CPU4~CPU7的負載均衡,配置步驟為:

1) mkdir child”在頂層CPUSET中建立子CPUSET,記為child

2) echo 0-3 > child/cpus (新建CPUSETsched_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

相關文章