Linux 內(nèi)核是如何感知到硬件上的 NUMA 信息的?
在 Linux 程序運(yùn)行過(guò)程中,有一個(gè)對(duì)性能影響較大的特性,那就是 NUMA。在不少公司中,都通過(guò) numactl 等命令對(duì)運(yùn)行的服務(wù)進(jìn)行了 NUMA 綁定,進(jìn)而提高程序的運(yùn)行性能。
那么我們今天來(lái)深入了解一下 NUMA 的原理。在硬件上的 NUMA 組成為什么會(huì)影響程序的運(yùn)行性能,Linux 操作系統(tǒng)又是如何識(shí)別 NUMA 信息,來(lái)將 CPU 和內(nèi)存進(jìn)行分組劃分 node 的。
一、NUMA 介紹
NUMA 全稱是 Non-uniform memory access,是非一致性內(nèi)存訪問的意思。不過(guò)這段話還是由點(diǎn)費(fèi)解,我們需要看看硬件才能更好地理解它。
現(xiàn)代的 CPU 都在硬件內(nèi)部實(shí)現(xiàn)了一個(gè)內(nèi)存控制器,內(nèi)存條都會(huì)和這個(gè)內(nèi)存控制器進(jìn)行相連。
圖片
之前我們?cè)?nbsp;深入了解服務(wù)器 CPU 的型號(hào)、代際、片內(nèi)與片間互聯(lián)架構(gòu) 一文中提到過(guò),服務(wù)器 CPU 和個(gè)人 PC CPU 的一個(gè)很大的區(qū)別就是擴(kuò)展性。在一臺(tái)服務(wù)器的內(nèi)部是支持插2/4/8等多 CPU 的。每個(gè) CPU 都可以連接幾條的內(nèi)存。兩個(gè) CPU 之間如果想要訪問對(duì)方上連接的內(nèi)存條,中間就得跨過(guò) UPI 總線。
圖片
下面是一臺(tái)服務(wù)器的實(shí)際內(nèi)部圖片。中間兩個(gè)銀色長(zhǎng)方形的東東是罩著散熱片的 CPU,每個(gè) CPU 旁邊都有一些內(nèi)存插槽,支持插入多條內(nèi)存。
圖片
CPU 擴(kuò)展性的設(shè)計(jì)極大地提升了服務(wù)器上的 CPU 核數(shù)與內(nèi)存容量。但同時(shí)也帶來(lái)了另外一個(gè)問題,那就是 CPU 物理核在訪問不同的內(nèi)存條的時(shí)候延遲是不同的。這就是非一致性內(nèi)存訪問的含義。
其實(shí)不僅僅是跨 CPU 訪問存在延時(shí)差異。在服務(wù)器高核心 CPU 上,由于 Mesh 架構(gòu)、以及存在兩個(gè)內(nèi)存控制器,物理核訪問不同的內(nèi)存控制器上的內(nèi)存條也會(huì)有差異。只不過(guò)這個(gè)差異沒有跨 CPU 差異大。
這種問題的出現(xiàn)使得 Linux 操作系統(tǒng)不得不關(guān)注內(nèi)存訪問速度不平均的問題。你在 Linux 上執(zhí)行 numactl 命令可以查看你機(jī)器上的 NUMA 配置情況。拿我手頭的一臺(tái)虛擬機(jī)來(lái)舉例。
# numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3
node 0 size: 7838 MB
node 0 free: 6208 MB
node 1 cpus: 4 5 6 7
node 1 size: 7934 MB
node 1 free: 6589 MB
node distances:
node 0 1
0: 10 20
1: 20 10上面的輸出中展示了 Linux 把所有的 CPU 核心和內(nèi)存分成了兩個(gè) node。其中 node 0 中的擁有的 CPU 核心是 0、1、2、3 這四個(gè)核,總共擁有 7838 MB 的內(nèi)存。node 1 中擁有的核心是 4、5、6、7 四個(gè)核,擁有的內(nèi)存是 7934 MB。
另外 node distances 這里顯示了跨 node 進(jìn)行內(nèi)存訪問時(shí)一個(gè)大概的延時(shí)差距。同 node 內(nèi)部的內(nèi)存訪問肯定是最快的,跨 node 則相對(duì)較慢。
那么內(nèi)核是如何識(shí)別到底層的 NUMA 信息的呢?
二、Linux 對(duì) NUMA 信息的讀取
2.1 Linux 內(nèi)核識(shí)別如何識(shí)別內(nèi)存屬于哪個(gè)節(jié)點(diǎn)
在計(jì)算機(jī)的體系結(jié)構(gòu)中,除了操作系統(tǒng)和硬件外,其實(shí)中間還存在著一層固件,英文名叫 firmware。它是位于主板上的使用 SPI Nor Flash 存儲(chǔ)著的軟件。起著在硬件和操作系統(tǒng)中間承上啟下的作用。它負(fù)責(zé)著硬件自檢、初始化硬件設(shè)備、加載操作系統(tǒng)引導(dǎo)程序,并提供接口將控制權(quán)轉(zhuǎn)移到操作系統(tǒng)。
圖片
回到我們今天的話題。那么 CPU 和內(nèi)存條之間這種訪問非一致性特點(diǎn),Linux 就是通過(guò)固件來(lái)獲得這個(gè)知識(shí)的。其中在 Linux 和固件中間的接口規(guī)范是 ACPI(Advanced Configuration and Power Interface),高級(jí)配置和電源接口。
這是較新的 6.5 版本的文檔地址: https://uefi.org/sites/default/files/resources/ACPI_Spec_6_5_Aug29.pdf。感興趣的同學(xué)可以下載下來(lái)。
在這個(gè)接口規(guī)范中的第 17 章中描述了 NUMA 相關(guān)的內(nèi)容。在 ACPI 中定義了兩個(gè)表,分別是:
- SRAT(System Resource Affinity Table)。在這個(gè)表中表示的是 CPU 核和內(nèi)存的關(guān)系圖。包括有幾個(gè) node,每個(gè) node 里面有那幾個(gè) CPU 邏輯核,有哪些內(nèi)存。
- SLIT(System Locality Information Table)。在這個(gè)表中記錄的是各個(gè)結(jié)點(diǎn)之間的距離。
有了這個(gè)規(guī)范,CPU 讀取這兩個(gè)表就可以獲得 NUMA 系統(tǒng)的 CPU 及物理內(nèi)存分布信息。操作系統(tǒng)在啟動(dòng)的時(shí)候會(huì)執(zhí)行 start_kernel 這個(gè)核心函數(shù),然后會(huì)調(diào)用到
//file:arch/x86/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{
...
// 保存物理內(nèi)存檢測(cè)結(jié)果
e820__memory_setup();
...
// membloc內(nèi)存分配器初始化
e820__memblock_setup();
// 內(nèi)存初始化(包括 NUMA 機(jī)制初始化)
initmem_init();
}在 setup_arch 中顯示調(diào)用了 e820__memory_setup 來(lái)保存物理內(nèi)存檢測(cè)結(jié)果。然后調(diào)用 e820__memblock_setup 初始化內(nèi)存分配器。詳情參見Linux 內(nèi)核“偷吃”了你的內(nèi)存! 一文。在 initmem_init 完成了 NUMA 的初始化。
在 initmem_init 中,依次調(diào)用了 x86_numa_init、numa_init、x86_acpi_numa_init,最后執(zhí)行到了 acpi_numa_init 函數(shù)中來(lái)讀取 ACPI 中的 SRAT 表,獲取到各個(gè) node 中的 CPU 邏輯核、內(nèi)存的分布信息。
//file:drivers/acpi/numa/srat.c
int __init acpi_numa_init(void)
{
...
// 解析 SRAT 表中的 NUMA 信息
// 具體包括:CPU_AFFINITY、MEMORY_AFFINITY 等
if (!acpi_table_parse(ACPI_SIG_SRAT, acpi_parse_srat)) {
...
}
...
}在 SRAT 表讀取并解析完成后,Linux 操作系統(tǒng)就知道了內(nèi)存和 node 的關(guān)系了。numa 信息都最后保存在了 numa_meminfo 這個(gè)數(shù)據(jù)結(jié)構(gòu)中,這是一個(gè)全局的列表,每一項(xiàng)都是(起始地址, 結(jié)束地址, 節(jié)點(diǎn)編號(hào))的三元組,描述了內(nèi)存塊與 NUMA 節(jié)點(diǎn)的關(guān)聯(lián)關(guān)系。
//file:arch/x86/mm/numa.c
static struct numa_meminfo numa_meminfo __initdata_or_meminfo;
//file:arch/x86/mm/numa_internal.h
struct numa_meminfo {
int nr_blks;
struct numa_memblk blk[NR_NODE_MEMBLKS];
};2.2 memblock 分配器 關(guān)聯(lián) NUMA 信息
在此之后,Linux 就可以通過(guò) numa_meminfo 數(shù)組來(lái)獲取硬件 NUMA 信息了。前面在 一文中我們提到了內(nèi)核的 memblock 內(nèi)存分配器。有了 numa_meminfo 數(shù)組,memblock 就可以根據(jù)這個(gè)信息讀取到自己各個(gè) region 分別是屬于哪個(gè) node 的了。
這件工作是在 numa_init 中開始的。
//file:arch/x86/mm/numa.c
static int __init numa_init(int (*init_func)(void))
{
...
//2.1 把numa相關(guān)的信息保存在 numa_meminfo 中
init_func();
//2.2 memblock 添加 NUMA 信息,并為每個(gè) node 申請(qǐng)對(duì)象
numa_register_memblks(&numa_meminfo);
...
// 用于將各個(gè)CPU core與NUMA節(jié)點(diǎn)關(guān)聯(lián)
numa_init_array();
return0;
}在 numa_register_memblks 中完成了三件事情
- 將每一個(gè) memblock region 與 NUMA 節(jié)點(diǎn)號(hào)關(guān)聯(lián)
- 為每一個(gè) node 都申請(qǐng)一個(gè)表示它的內(nèi)核對(duì)象(pglist_data)
- 再次打印 memblock 信息
//file:arch/x86/mm/numa.c
static int __init numa_register_memblks(struct numa_meminfo *mi)
{
...
//1.將每一個(gè) memblock region 與 NUMA 節(jié)點(diǎn)號(hào)關(guān)聯(lián)
for (i = 0; i < mi->nr_blks; i++) {
struct numa_memblk *mb = &mi->blk[i];
memblock_set_node(mb->start, mb->end - mb->start,
&memblock.memory, mb->nid);
}
...
//2.為所有可能存在的node申請(qǐng)pglist_data結(jié)構(gòu)體空間
for_each_node_mask(nid, node_possible_map) {
...
//為nid申請(qǐng)一個(gè)pglist_data結(jié)構(gòu)體
alloc_node_data(nid);
}
//3.打印MemBlock內(nèi)存分配器的詳細(xì)調(diào)試信息
memblock_dump_all();
}這個(gè)函數(shù)的詳細(xì)邏輯就不展開了。我們來(lái)看下 memblock_dump_all。如果你開啟了 memblock=debug 啟動(dòng)參數(shù),在它執(zhí)行完后,memblock 內(nèi)存分配器的信息再次被打印了出來(lái)。
[ 0.010796] MEMBLOCK configuration:
[ 0.010797] memory size = 0x00000003fff78c00 reserved size = 0x0000000003d7bd7e
[ 0.010797] memory.cnt = 0x4
[ 0.010799] memory[0x0] [0x0000000000001000-0x000000000009efff], 0x000000000009e000 bytes on node 0 flags: 0x0
[ 0.010800] memory[0x1] [0x0000000000100000-0x00000000bffd9fff], 0x00000000bfeda000 bytes on node 0 flags: 0x0
[ 0.010801] memory[0x2] [0x0000000100000000-0x000000023fffffff], 0x0000000140000000 bytes on node 0 flags: 0x0
[ 0.010802] memory[0x3] [0x0000000240000000-0x000000043fffffff], 0x0000000200000000 bytes on node 1 flags: 0x0
[ 0.010803] reserved.cnt = 0x7
[ 0.010804] reserved[0x0] [0x0000000000000000-0x00000000000fffff], 0x0000000000100000 bytes on node 0 flags: 0x0
[ 0.010806] reserved[0x1] [0x0000000001000000-0x000000000340cfff], 0x000000000240d000 bytes on node 0 flags: 0x0
[ 0.010807] reserved[0x2] [0x0000000034f31000-0x000000003678ffff], 0x000000000185f000 bytes on node 0 flags: 0x0
[ 0.010808] reserved[0x3] [0x00000000bffe0000-0x00000000bffe3d7d], 0x0000000000003d7e bytes on node 0 flags: 0x0
[ 0.010809] reserved[0x4] [0x000000023fffb000-0x000000023fffffff], 0x0000000000005000 bytes flags: 0x0
[ 0.010810] reserved[0x5] [0x000000043fff9000-0x000000043fffdfff], 0x0000000000005000 bytes flags: 0x0
[ 0.010811] reserved[0x6] [0x000000043fffe000-0x000000043fffffff], 0x0000000000002000 bytes on node 1 flags: 0x0不過(guò)這次不同的是,每一段內(nèi)存地址范圍后面都跟上了 node 的信息,例如 on node 0、on node 1 等。
三、操作系統(tǒng)內(nèi)存識(shí)別過(guò)程總結(jié)
在剛開始操作系統(tǒng)啟動(dòng)的時(shí)候,操作系統(tǒng)通過(guò) e820 讀取到了內(nèi)存的布局,并將它打印到了日志中。
[ 0.000000] BIOS-provided physical RAM map:
[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000bffd9fff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000bffda000-0x00000000bfffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000feff4000-0x00000000feffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000043fffffff] usable接著內(nèi)核創(chuàng)建了 memblock 內(nèi)存分配器來(lái)進(jìn)行系統(tǒng)啟動(dòng)時(shí)的內(nèi)存管理。如果開啟了 memblock=debug 啟動(dòng)參數(shù),同樣能把它打印出來(lái)。
[ 0.010238] MEMBLOCK configuration:
[ 0.010239] memory size = 0x00000003fff78c00 reserved size = 0x0000000003c6d144
[ 0.010240] memory.cnt = 0x3
[ 0.010241] memory[0x0] [0x0000000000001000-0x000000000009efff], 0x000000000009e000 bytes flags: 0x0
[ 0.010243] memory[0x1] [0x0000000000100000-0x00000000bffd9fff], 0x00000000bfeda000 bytes flags: 0x0
[ 0.010244] memory[0x2] [0x0000000100000000-0x000000043fffffff], 0x0000000340000000 bytes flags: 0x0
[ 0.010245] reserved.cnt = 0x4
[ 0.010246] reserved[0x0] [0x0000000000000000-0x0000000000000fff], 0x0000000000001000 bytes flags: 0x0
[ 0.010247] reserved[0x1] [0x00000000000f5a40-0x00000000000f5b83], 0x0000000000000144 bytes flags: 0x0
[ 0.010248] reserved[0x2] [0x0000000001000000-0x000000000340cfff], 0x000000000240d000 bytes flags: 0x0
[ 0.010249] reserved[0x3] [0x0000000034f31000-0x000000003678ffff], 0x000000000185f000 bytes flags: 0x0不過(guò)到這里,Linux 操作系統(tǒng)還不知道內(nèi)存的 NUMA 信息。它通過(guò) ACPI 接口讀取固件中的 SRAT 表,將 NUMA 信息保存到 numa_meminfo 數(shù)組中。從此,Linux 就知道了硬件上的 NUMA 信息,并對(duì) memblock 內(nèi)存分配器也設(shè)置了 node 信息。并再次將其打印了出來(lái)。這次 memblock 的每一個(gè) region 中就都攜帶了 node 信息。
[ 0.010796] MEMBLOCK configuration:
[ 0.010797] memory size = 0x00000003fff78c00 reserved size = 0x0000000003d7bd7e
[ 0.010797] memory.cnt = 0x4
[ 0.010799] memory[0x0] [0x0000000000001000-0x000000000009efff], 0x000000000009e000 bytes on node 0 flags: 0x0
[ 0.010800] memory[0x1] [0x0000000000100000-0x00000000bffd9fff], 0x00000000bfeda000 bytes on node 0 flags: 0x0
[ 0.010801] memory[0x2] [0x0000000100000000-0x000000023fffffff], 0x0000000140000000 bytes on node 0 flags: 0x0
[ 0.010802] memory[0x3] [0x0000000240000000-0x000000043fffffff], 0x0000000200000000 bytes on node 1 flags: 0x0
[ 0.010803] reserved.cnt = 0x7
[ 0.010804] reserved[0x0] [0x0000000000000000-0x00000000000fffff], 0x0000000000100000 bytes on node 0 flags: 0x0
[ 0.010806] reserved[0x1] [0x0000000001000000-0x000000000340cfff], 0x000000000240d000 bytes on node 0 flags: 0x0
[ 0.010807] reserved[0x2] [0x0000000034f31000-0x000000003678ffff], 0x000000000185f000 bytes on node 0 flags: 0x0
[ 0.010808] reserved[0x3] [0x00000000bffe0000-0x00000000bffe3d7d], 0x0000000000003d7e bytes on node 0 flags: 0x0
[ 0.010809] reserved[0x4] [0x000000023fffb000-0x000000023fffffff], 0x0000000000005000 bytes flags: 0x0
[ 0.010810] reserved[0x5] [0x000000043fff9000-0x000000043fffdfff], 0x0000000000005000 bytes flags: 0x0
[ 0.010811] reserved[0x6] [0x000000043fffe000-0x000000043fffffff], 0x0000000000002000 bytes on node 1 flags: 0x0以上就是 Linux 內(nèi)存中 NUMA 機(jī)制的初始化大概過(guò)程。
總結(jié)
在現(xiàn)代服務(wù)器的非統(tǒng)一內(nèi)存訪問(NUMA)是一種用于多處理器硬件架構(gòu)下,識(shí)別和保存每個(gè) CPU 核和內(nèi)存條之間的連接拓?fù)浞浅5闹匾R驗(yàn)?CPU 只是和它直連的內(nèi)存訪問速度最快,訪問和其它 CPU 連接的內(nèi)存速度將會(huì)大大下降。
Linux 通過(guò)固件讀取 ACPI 規(guī)范中的 SRAT 和 SLIT 表識(shí)別 NUMA 信息,在系統(tǒng)啟動(dòng)過(guò)程中,經(jīng)一系列函數(shù)調(diào)用完成 NUMA 初始化,將信息保存到numa_meminfo,并使 memblock 分配器關(guān)聯(lián) NUMA 信息。最后通過(guò) e820 讀取內(nèi)存布局,再結(jié)合 ACPI 獲取的 NUMA 信息完成內(nèi)存識(shí)別及相關(guān)設(shè)置。
當(dāng)內(nèi)核有了硬件 NUMA 信息的拓?fù)鋱D后,我們?cè)趹?yīng)用側(cè)就可以通過(guò) numactl 等命令來(lái)優(yōu)化程序的性能了!
不過(guò)最后要補(bǔ)充說(shuō)一點(diǎn),關(guān)于 NUMA 綁定并不是有益無(wú)害。在業(yè)界也有不同的聲音。比如 Oracal 的技術(shù)大咖們認(rèn)為綁定 NUMA 可能在全局內(nèi)存并未用盡的情況下出現(xiàn)內(nèi)存分配錯(cuò)誤,導(dǎo)致系統(tǒng)出現(xiàn)劇烈抖動(dòng)。
























