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等。
本文試圖從原理上介紹,盡量避免涉及代碼的實現細節。
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號之間的映射關系。
首先需要完成1.2節中描述的3個關鍵信息的初始化。
2.1 CPU和Node的關系
start_kernel()->setup_arch()->prom_init():
#ifdef CONFIG_NUMA
build_node_cpu_map();
#endif
build_node_cpu_map()函數工作:
a) 確定CPU與Node的相互關系,做法很簡單:
#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號也就具有如下功能宏:
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<n>,DRAM_LIMIT<n>和DRAM_NODE_TRANSLATION<n>描述一個內存區域屬性):
第一組(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”。
bootmem_init()->…->init_bootmem_node()->init_bootmem_core():
每個節點擁有各自的bootmem管理(code&data之前可以為空閑頁面)。
初始化流程最后會設置全局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
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)宏返回節點i的struct 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個節點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種配置:
#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()中:
/* * 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后,每個節點將關聯2個zonelist:
1) zonelist[0]中存放以Node方式或Zone方式組織的zonelist,包括所有節點的zone;
2) zonelist[1]中只存放本節點的zone即Legacy方式;
zonelist[1]用來實現僅從節點自身zone中的內存分配(參考__GFP_THISNODE標志)。
配置NUMA后對SLAB(本文不涉及SLOB或SLUB)的初始化影響不大,只是在分配一些變量采用類似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[] 中緩存每個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定義如下:
#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, \ } |
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結構。
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_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結構來描述:
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限制的內存策略,后面會介紹。
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從負載均衡中隔離出來。
1) /sys/devices/system/node/中記錄有系統中的所有內存節點信息;
2)任務額外關聯一個/proc/<tid>/numa_smaps文件信息;
3) tmpfs可以指定在某個Node上創建;
4) libnuma庫和其numactl小工具可以方便操作NUMA內存;
5) … …
2. ULK3
3. XLP® Processor Family Programming Reference Manual