Linux 大頁記憶體 Huge Pages 虛擬記憶體

2023-05-27 15:00:09

Linux為什麼要有大頁記憶體?為什麼DPDK要求必須要設定大頁記憶體?這都是由系統架構決定的,系統架構發展到現在,又是在原來的基礎上一點點演變的。一開始為了解決一個問題,大家設計了一個很好的方案,隨著事物的發展,發現無法滿足需求,就在原來的基礎上改進,慢慢的變成了現在的樣子。不過技術革新一直在進行,包括現在。

實體記憶體 Physical address

實體記憶體就是電腦的記憶體條,上面的每一個方塊就是儲存晶片,晶片中還有顆粒。存取資料的時候,會使用各種技術,儘可能從多個記憶體條,每個記憶體條的多個儲存晶片獲取資料,這樣多通道,並行大,速度更快。

虛擬記憶體 Linear address (also known as virtual address)

程式執行在作業系統上,不可能直接存取實體記憶體。一是太複雜,需要自己管理記憶體,哪些被其他程序佔用了,哪些可以用,如果連續空間不夠,如何拼接等;二是不安全,使用者可以直接存取到其他程序的資料。所以作業系統在後續增加了虛擬記憶體的概念。

每個程序看到的都是整個可用記憶體,比如4G,所有程序看到的都是4G,自己程序維護一張表,當程序存取記憶體時,只能存取到虛擬記憶體表,由作業系統再對映到具體的實體記憶體。這樣的好處除了解決了上面的問題,還有幾個優點:一是系統同一管理,更合理,可以做更多優化,比如同一塊資料多個程序讀取,只需要在記憶體儲存一份即可,節省了空間(類庫的載入);二是程式申請記憶體,並不一定會使用,或者說不會立馬使用,那麼系統可以不分配實體記憶體,當程式真正存取記憶體時,觸發中斷,系統再對映到實體記憶體,節省資源;三是程式存取的都是連續資源,具體記憶體分配是由系統管理,簡化了開發。

MMU Memory Management Unit 記憶體管理單元

用來把虛擬記憶體地址轉化為實體記憶體地址的硬體,提供實體記憶體資料存取和許可權控制。

頁表 虛擬記憶體表

虛擬記憶體向實體記憶體對映時,需要建立資料結構儲存對應的資料,如果完全一一對映,肯定會需要很多資源,所以為了減少記憶體空間的消耗,又提出了新的方案:把記憶體分為一塊塊的資料(每塊稱作為一頁資料),然後對映這些塊,這樣就可以減少對映條目,降低記憶體佔用。虛擬記憶體往往稱作頁;實體記憶體按照同樣大小分割,有時候稱作塊或者幀(frame)

MMU儲存的針對這些記憶體頁的表,稱作為頁表。頁表中每一條資料儲存了對映的實體記憶體頁的位置和在該頁內資料的偏移,這樣就能找到具體的記憶體單元了。

除了這種使用頁表的頁式管理方式,還有段式和段頁式兩種。

多級分頁

使用頁表後,資料量還是很大,比如32G的記憶體,使用4K作為一頁,那麼需要8,388,608個頁表條目。為了進一步減少資源消耗,我們發現大部分程式是不需要全部記憶體的,一個應用執行起來,並不是必須要32G的記憶體,有可能只有幾百兆。多級分頁就做了進一步優化:

比如32G,每級頁表按照1G分,有32條記錄;1G再按照10M分,就有100條記錄,以此類推。開始只建立第一張表,後續需要的時候,再動態建立其他的表,大大減少了頁表的記錄數。

Translation Lookaside Buffer TLB

多級分頁後,資料量少了,但是增加了一個問題,就是存取效率,原來直接存取記憶體,只需要一次;增加了頁表後,需要兩次,先存取頁表,再存取實體記憶體;多級頁表,需要多次。這相當於成倍的增加了存取記憶體的時間。TLB就是為了解決這個問題的,把常用的頁表,放到CPU的快取記憶體,避免存取記憶體,直接在快取中獲取,提高效率。

大頁記憶體

我們知道,計算機中每個硬體的運算速度是不一樣的,其順序就是:硬碟/網路(IO) < 記憶體 < 記憶體快取(如果有) < CPU L3(CPU3級快取) < CPU L2 < CPU L1 < CPU,每個之間的差距都是幾倍甚至幾十上百倍的,同樣其空間大小也是相差數量級的,只不過正好反過來。硬碟可以做到TB甚至PB,記憶體常見的只有幾十GB或者幾百GB,而CPU的快取,3級的可能有幾十兆,而1級的往往只有位元組級別的了。由於這個原因,TLB是不可能把所有的頁表都對映到快取中,那麼在CPU快取中的TLB命中率越高,效能提升越大。如果命中率很低,不僅沒有提升,還額外增加了快取的存取,反而降低了效率。

如果程式頻繁的存取一塊很大的記憶體,並且是無序的,比如頻繁搜尋一個100G的無規律的文字資料,這個時候就會導致CPU中的快取頻頻失效,因為CPU快取的頁表條目有限,程式使用的條目超出了快取TLB的範圍,就會不斷的刪除舊的,加入新的,實際上如果快取足夠大,會發現剛刪除的條目又被加了進來。

如何解決呢?就是提高命中率,怎樣提高命中率呢?就是TLB中儲存的資料條目雖然固定,但是其解析記憶體可以擴充套件,擴充套件到可以包含程式存取的空間大小,這樣,不管程式如何亂序存取,因為頁表都儲存在TLB中了,都可以命中。比如原來TLB儲存100條,由於一頁4K,按照最簡單計算,就是400K的空間。我把一頁設定為4M,那就可以表示400M的空間,如果程式存取的記憶體在400M以內,就可以完全命中。

檢視大頁記憶體

cat /proc/meminfo|grep -i huge
AnonHugePages:      4096 kB
ShmemHugePages:        0 kB
HugePages_Total:       4
HugePages_Free:        2
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:    1048576 kB
Hugetlb:         4194304 kB

HugePages_TotalHugepagesize都大於0,就表示設定了大頁記憶體,從字面意思也很好理解:HugePages_Total表示大頁記憶體的個數;Hugepagesize表示一個大頁記憶體的大小。同樣Hugetlb大於0也表示設定了大頁記憶體,這個是計算下來的總數。不過有的系統可能沒有這個欄位。

設定大頁記憶體

方法一 修改grub

grub的檔案位置在/boot目錄下,大部分有如下兩個地方,一個是Legacy,一個是UEFI,不同系統可能有細微區別:/boot/grub2/grub.cfg /boot/efi/EFI/openEuler/grub.cfg。開啟檔案,找到如下位置

 menuentry 'openEuler (4.19.90-2003.4.0.0036.oe1.x86_64) 20.03 (LTS)' --class openeuler --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-4.19.90-2003.4.0.0036.oe1.x86_    64-advanced-db3b87ac-c948-4347-b1a4-e8ca943688b6' {
     load_video
     set gfxpayload=keep
     insmod gzio
     insmod part_msdos
     insmod ext2
     set root='hd0,msdos1'
     if [ x$feature_platform_search_hint = xy ]; then
       search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 --hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1 --hint='hd0,msdos1'  75039a04-9431-47c8-923c-795ba2b37e3e
     else
       search --no-floppy --fs-uuid --set=root 75039a04-9431-47c8-923c-795ba2b37e3e
     fi
     linux   /vmlinuz-4.19.90-2003.4.0.0036.oe1.x86_64 root=/dev/mapper/openeuler-root ro resume=/dev/mapper/openeuler-swap rd.lvm.lv=openeuler/root rd.lvm.lv=openeuler/swap rhgb quiet quiet crashkernel=51    default_hugepagesz=1G hugepagesz=1G hugepages=4
     initrd /initramfs-4.19.90-2003.4.0.0036.oe1.x86_64.img
 }

要找對地方,這是啟動系統的選項,還有一個是用作安全啟動的。在倒數第二行增加default_hugepagesz=1G hugepagesz=1G hugepages=4,這三個欄位分別表示預設大頁記憶體大小,就是如果不設定,就用這個預設設定;設定的大頁記憶體大小;設定的大頁記憶體個數。重啟系統,再次檢視就可以看到大頁記憶體資訊。

掛載大頁記憶體

mount -t hugetlbfs nodev /mnt/huge
mount -t hugetlbfs hugetlbfs /mnt/huge

必須保證目錄/mnt/huge存在,引數意義:-t指定掛載格式,hugetlbfs表示是大頁記憶體,後面表示掛載裝置,可以寫hugetlbfs,也可以寫nodev不掛載裝置,最後是掛載目錄。

NUMA

為什麼需要了解NUMA呢?因為與上面的大頁記憶體設定有關。瞭解NUMA,又需要先知道SMP(Symmetric Multi-Processor)對稱多處理器結構。SMP就是指系統中多個CPU對稱工作,每個CPU的優先順序一樣,存取資源(記憶體)速度一樣,沒有主次之分。SMP又叫做UMA(Uniform Memory Access)一致記憶體存取。

但是隨著CPU的發展,核心越來越多,存取同一資源的競爭越來越大,導致CPU效能受到限制,所以推出了NUMA(Non Uniform Memory Access)非一致記憶體存取架構。

NUMA設計

SMP或者UMA,CPU統一經過北橋(記憶體控制器)存取記憶體。
NUMA,CPU把記憶體控制器做到CPU內部,一般一個CPU socket一個。每個CPU內部的記憶體控制器與一部分記憶體連線。CPU存取自己連線的記憶體,速度很快,叫做本地記憶體,可以通過QPI(Quick Path Interconnect)匯流排存取其他的記憶體,不過速度會慢。

Node Socket Core Processor

把多個core封裝到一起,叫做一個CPU Socket,系統中根據Socket定義Node,也就是正常情況Node數量與Socket相同,或者一個是軟體概念,一個是硬體概念。

Core就是物理CPU,原來是單CPU,效能不夠,研發了多CPU架構,現在一個Core就相當於原來的一塊CPU

Thread,也叫做邏輯CPU,或者Processor,是把core通過超執行緒記錄模擬出來的處理單元。

lscpu
lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                12
On-line CPU(s) list:   0-11
Thread(s) per core:    1
Core(s) per socket:    6
Socket(s):             2
NUMA node(s):          2
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 45
Stepping:              7
CPU MHz:               1200.000
BogoMIPS:              3999.47
Virtualization:        VT-x
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              15360K
NUMA node0 CPU(s):     0-5
NUMA node1 CPU(s):     6-11

CPU(s) 表示邏輯CPU個數
Thread(s) per core 表示一個core可以實現的超執行緒數
Core(s) per socket 表示一個socket上的core數
Socket(s) 表示socket的個數
NUMA node(s) 表示NOMA的結點數

這裡如何計算呢?首先一塊中央處理器(CPU),有多個插槽socket,每個socket中又有多個core,每個core又可以使用超執行緒技術模擬出多個執行緒(這個是邏輯CPU)。所以CPU(s)=SOckets * Cores * Threads

numactl

numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 15516 MB
node 0 free: 11705 MB
node 1 cpus: 8 9 10 11 12 13 14 15
node 1 size: 16100 MB
node 1 free: 1386 MB
node distances:
node   0   1 
  0:  10  21 
  1:  21  10

CPU分為兩個node,也就是有兩個CPU socket。
第一個node包含0-7號cpu,第二個node包含8-15個cpu
第一個node直連的記憶體是15516MB,第二個node直連的記憶體是161000MB
第一個node直連的記憶體空閒是11705MB,第二個node直連的記憶體是1386MB
node distances表示每個node存取對應的記憶體的距離,比如node0存取node0的距離是10,存取node1的距離是21。

numactl還有其他作用,比如繫結程式在哪一個node上執行,指定在哪個node上分配記憶體等。

numastat

numastat
                           node0           node1
numa_hit                18743091         9973793
numa_miss                      0               0
numa_foreign                   0               0
interleave_hit            101249          101451
local_node              18656240         9048143
other_node                 86851          925650

numa_hit 在node關聯的記憶體上申請的記憶體數量
numa_miss 不在node關聯的記憶體上申請的記憶體數量
numa_foreign 在其他node關聯的記憶體上申請的記憶體數量
interleave_hit 採用interleave策略申請的記憶體數量
local_node 該node上的程序在該node關聯的記憶體上申請的記憶體數量
other_node 該node上的程序在其他node關聯的記憶體上申請的記憶體數量

關閉開啟NUMA

在上面設定大頁記憶體的一行加上numa=off,會關閉numa,理論上刪除該欄位,預設是開啟。

調優經驗

linux分配記憶體的策略是優先在自己node上分配記憶體,如果不夠再考慮其他node上的記憶體,這是合理的。不過如果程式繫結的node記憶體不夠,可以考慮繫結到其他node或者給程式執行的node多分配一些記憶體。

檢視Node上的大頁記憶體

上面我們介紹了,現代CPU都是有很多Node的,在NUMA架構下,設定在grub.cfg中的大頁記憶體是被多個Node平分的。
比如我機器上按照上面設定了4個1G的大頁記憶體,檢視每個Node上的資訊,Node0和Node1上各兩個。

$ cat /sys/devices/system/node/node0/hugepages/hugepages-1048576kB/nr_hugepages
2

$ cat /sys/devices/system/node/node1/hugepages/hugepages-1048576kB/nr_hugepages
2

大頁記憶體設定目錄介紹

/sys/devices/system/node目錄下,可以看到系統中每一個Node對應的目錄。在每個Node目錄下/sys/devices/system/node/node0/hugepages,有關於大頁記憶體的設定資訊,一般有兩個目錄hugepages-1048576kB hugepages-2048kB,這是Linux系統支援的兩種大頁,一個是1G,一個是2M。

在每個大頁記憶體目錄下有三個檔案free_hugepages nr_hugepages surplus_hugepages,分別表示當前Node,當前大頁記憶體中空閒的大頁記憶體數、設定的大頁記憶體數,超出使用的大頁記憶體數。

方法二 臨時設定大頁記憶體

如果想臨時修改某個Node的大頁記憶體,可以直接向對應的nr_hugepages寫入對應數位,比如 echo 2 > /sys/devices/system/node/node0/hugepages/hugepages-1048576kB/nr_hugepages

如果不想每個Node自己控制,可以向系統統一的大頁記憶體檔案寫入數位 echo 2 > /sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages,這個目錄與上面的目錄不一樣,儲存的是整個系統的大頁記憶體設定。

https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt
Understanding Linux Kernel