NUMA 記憶體策略

什麼是 NUMA 記憶體策略?

在 Linux 核心中,“記憶體策略”決定了核心在 NUMA 系統或模擬 NUMA 系統中從哪個節點分配記憶體。Linux 自 2.4.? 版本以來一直支援非統一記憶體訪問架構的平臺。當前的記憶體策略支援於 2004 年 5 月左右新增到 Linux 2.6 中。本文件旨在描述 2.6 記憶體策略支援的概念和 API。

記憶體策略不應與 cpusets(Documentation/admin-guide/cgroup-v1/cpusets.rst)混淆,cpusets 是一種管理機制,用於限制一組程序可以從中分配記憶體的節點。記憶體策略是一種 NUMA 感知應用程式可以利用的程式設計介面。當 cpuset 和策略都應用於一個任務時,cpuset 的限制優先。有關更多詳細資訊,請參閱下面的記憶體策略和 cpusets

記憶體策略概念

記憶體策略範圍

Linux 核心支援記憶體策略的 _範圍_,此處從最通用到最具體進行描述

系統預設策略

此策略“硬編碼”在核心中。它管理所有不受下面討論的更具體策略範圍控制的頁分配。當系統“正常執行”時,系統預設策略將使用下面描述的“本地分配”。然而,在啟動過程中,系統預設策略將被設定為在所有具有“足夠”記憶體的節點上交錯分配,以避免在啟動時分配過多,從而使初始啟動節點過載。

任務/程序策略

這是一種可選的、按任務的策略。當為特定任務定義時,此策略控制由任務或代表任務進行的所有不受更具體範圍控制的頁分配。如果任務未定義任務策略,則所有原本應受任務策略控制的頁分配將“回退”到系統預設策略。

任務策略適用於任務的整個地址空間。因此,它可以在 fork() [不帶 CLONE_VM 標誌的 clone()] 和 exec*() 呼叫中繼承。這允許父任務為從對記憶體策略一無所知的可執行映像 exec() 出來的子任務建立任務策略。有關任務可用於設定/更改其任務/程序策略的系統呼叫的概述,請參閱下面的記憶體策略 API 部分。

在多執行緒任務中,任務策略僅適用於安裝策略的執行緒(Linux 核心任務)以及該執行緒隨後建立的任何執行緒。在新任務策略安裝時存在的任何同級執行緒會保留其當前策略。

任務策略僅適用於策略安裝後分配的頁。當任務更改其任務策略時,任何已由任務發生缺頁的頁仍保留在其最初根據分配時策略分配的位置。

VMA 策略

“VMA”或“虛擬記憶體區域”是指任務虛擬地址空間的一個範圍。任務可以為其虛擬地址空間的一個範圍定義特定策略。有關用於設定 VMA 策略的 mbind() 系統呼叫的概述,請參閱下面的記憶體策略 API 部分。

VMA 策略將管理支援此地址空間區域的頁分配。任務地址空間中沒有明確 VMA 策略的任何區域將回退到任務策略,而任務策略本身可能會回退到系統預設策略。

VMA 策略有一些複雜細節

  • VMA 策略僅適用於匿名頁。這包括為匿名段(如任務棧和堆)分配的頁,以及使用 MAP_ANONYMOUS 標誌 mmap() 的地址空間的任何區域。如果 VMA 策略應用於檔案對映,並且該對映使用了 MAP_SHARED 標誌,則 VMA 策略將被忽略。如果檔案對映使用了 MAP_PRIVATE 標誌,VMA 策略僅在嘗試寫入對映時分配匿名頁時(即,在寫時複製時)應用。

  • VMA 策略在所有共享虛擬地址空間的任務(即執行緒)之間共享,無論策略何時安裝;它們也透過 fork() 繼承。然而,由於 VMA 策略引用任務地址空間的特定區域,並且地址空間在 exec*() 時被丟棄和重新建立,因此 VMA 策略不能透過 exec() 繼承。因此,只有 NUMA 感知應用程式才能使用 VMA 策略。

  • 任務可以在先前 mmap() 的區域的子範圍內安裝新的 VMA 策略。當發生這種情況時,Linux 會將現有虛擬記憶體區域拆分為 2 或 3 個 VMA,每個 VMA 都有自己的策略。

  • 預設情況下,VMA 策略僅適用於策略安裝後分配的頁。任何已發生缺頁到 VMA 範圍內的頁仍保留在其最初根據分配時策略分配的位置。然而,自 2.6.16 版本以來,Linux 透過 mbind() 系統呼叫支援頁遷移,因此頁內容可以移動以匹配新安裝的策略。

共享策略

從概念上講,共享策略適用於對映到一個或多個任務不同地址空間的“共享記憶體物件”。應用程式安裝共享策略的方式與 VMA 策略相同——使用 mbind() 系統呼叫指定對映共享物件的虛擬地址範圍。然而,與 VMA 策略(可被視為任務地址空間某個範圍的屬性)不同,共享策略直接應用於共享物件。因此,所有連線到該物件的任務都共享該策略,並且任何任務為該共享物件分配的所有頁都將遵守該策略。

截至 2.6.22 版本,只有透過 shmget() 或 mmap(MAP_ANONYMOUS|MAP_SHARED) 建立的共享記憶體段支援共享策略。當 Linux 新增共享策略支援時,相關資料結構被新增到了 hugetlbfs 共享記憶體段中。當時,hugetlbfs 不支援在缺頁時進行分配(即延遲分配),因此 hugetlbfs 共享記憶體段從未“連線”到共享策略支援。儘管 hugetlbfs 段現在支援延遲分配,但其對共享策略的支援尚未完成。

如上文VMA 策略部分所述,使用 MAP_SHARED 標誌 mmap() 的常規檔案的頁快取頁分配會忽略安裝在由共享檔案對映支援的虛擬地址範圍上的任何 VMA 策略。相反,共享頁快取頁(包括尚未由任務寫入的私有對映所支援的頁)遵循任務策略(如果有),否則遵循系統預設策略。

共享策略基礎設施支援對共享物件的子集範圍應用不同策略。然而,Linux 仍然會為每個不同策略的範圍拆分安裝策略的任務的 VMA。因此,連線到共享記憶體段的不同任務可以具有對映該共享物件的不同 VMA 配置。當一個任務在一個或多個區域範圍內安裝了共享策略時,可以透過檢查共享記憶體區域的任務的 /proc/<pid>/numa_maps 來看到這一點。

記憶體策略組成部分

NUMA 記憶體策略由“模式”、可選模式標誌和可選的節點集組成。模式決定策略的行為,可選模式標誌決定模式的行為,可選的節點集可以被視為策略行為的引數。

在內部,記憶體策略由一個引用計數結構 struct mempolicy 實現。此結構的詳細資訊將在下文根據解釋行為的需要進行討論。

NUMA 記憶體策略支援以下行為模式

預設模式--MPOL_DEFAULT

此模式僅在記憶體策略 API 中使用。在內部,MPOL_DEFAULT 在所有策略範圍中都轉換為 NULL 記憶體策略。當指定 MPOL_DEFAULT 時,任何現有的非預設策略都將被簡單地移除。因此,MPOL_DEFAULT 意味著“回退到下一個最具體的策略範圍。”

例如,NULL 或預設任務策略將回退到系統預設策略。NULL 或預設 VMA 策略將回退到任務策略。

當在記憶體策略 API 中指定時,預設模式不使用可選的節點集。

為該策略指定的節點集非空是錯誤的。

MPOL_BIND (繫結模式)

此模式指定記憶體必須來自策略指定的節點集。記憶體將從該集中具有足夠可用記憶體且最接近分配發生節點的節點進行分配。

MPOL_PREFERRED (首選模式)

此模式指定應嘗試從策略中指定的單個節點進行分配。如果該分配失敗,核心將根據平臺韌體提供的資訊,按照與首選節點距離遞增的順序搜尋其他節點。

在內部,首選策略使用單個節點——struct mempolicy 的 preferred_node 成員。當內部模式標誌 MPOL_F_LOCAL 設定時,preferred_node 被忽略,並且策略被解釋為本地分配。“本地”分配策略可以看作是一種從包含發生分配的 CPU 所在節點開始的首選策略。

使用者可以透過傳遞一個空節點掩碼來指定始終首選本地分配。如果傳遞空節點掩碼,則該策略不能使用下面描述的 MPOL_F_STATIC_NODES 或 MPOL_F_RELATIVE_NODES 標誌。

MPOL_INTERLEAVED (交錯模式)

此模式指定頁分配以頁粒度在策略中指定的節點間交錯進行。此模式在不同使用上下文中行為也略有不同

對於匿名頁和共享記憶體頁的分配,交錯模式使用缺頁地址在包含該地址的段 [VMA] 中的頁偏移量,對策略指定的節點集進行索引,再對策略指定的節點數取模。然後,它嘗試從選定節點開始分配一個頁,就好像該節點已由首選策略指定或已由本地分配選中一樣。也就是說,分配將遵循每個節點的 zonelist。

對於頁快取頁的分配,交錯模式使用每個任務維護的節點計數器來索引策略指定的節點集。此計數器在達到最高指定節點後會迴繞到最低指定節點。這傾向於根據頁的分配順序而不是基於地址範圍或檔案的任何頁偏移量,將頁分散到策略指定的節點上。在系統啟動期間,臨時交錯系統預設策略在此模式下工作。

MPOL_PREFERRED_MANY (多首選模式)

此模式指定分配應優先從策略中指定的節點掩碼中滿足。如果節點掩碼中的所有節點都存在記憶體壓力,則分配可以回退到所有現有 NUMA 節點。這實際上是 MPOL_PREFERRED 允許用於掩碼而不是單個節點的情況。

MPOL_WEIGHTED_INTERLEAVE (加權交錯模式)

此模式的操作與 MPOL_INTERLEAVE 相同,不同之處在於交錯行為是根據 /sys/kernel/mm/mempolicy/weighted_interleave/ 中設定的權重執行的。

加權交錯根據權重在節點上分配頁。例如,如果節點 [0,1] 的權重分別為 [5,2],則每當 node1 上分配 2 個頁時,node0 上將分配 5 個頁。

NUMA 記憶體策略支援以下可選模式標誌

MPOL_F_STATIC_NODES

此標誌指定,如果任務或 VMA 的允許節點集在記憶體策略定義後發生更改,則使用者傳遞的節點掩碼不應重新對映。

沒有此標誌,任何時候記憶體策略因允許節點集的變化而重新繫結時,首選節點掩碼 (Preferred Many)、首選節點 (Preferred) 或節點掩碼 (Bind, Interleave) 都會被重新對映到新的允許節點集。這可能導致使用先前不希望使用的節點。

使用此標誌,如果使用者指定的節點與任務 cpuset 允許的節點重疊,則記憶體策略將應用於它們的交集。如果兩組節點不重疊,則使用預設策略。

例如,考慮一個任務,它附加到一個包含記憶體節點 1-3 的 cpuset,並在此同一節點集上設定了交錯策略。如果 cpuset 的記憶體節點更改為 3-5,則交錯現在將在節點 3、4 和 5 上發生。然而,使用此標誌,由於使用者節點掩碼中只允許節點 3,“交錯”僅在該節點上發生。如果使用者節點掩碼中的任何節點現在都不允許使用,則使用預設行為。

MPOL_F_STATIC_NODES 不能與 MPOL_F_RELATIVE_NODES 標誌結合使用。它也不能用於使用空節點掩碼(本地分配)建立的 MPOL_PREFERRED 策略。

MPOL_F_RELATIVE_NODES

此標誌指定使用者傳遞的節點掩碼將相對於任務或 VMA 的允許節點集進行對映。核心儲存使用者傳遞的節點掩碼,如果允許節點發生變化,則原始節點掩碼將相對於新的允許節點集重新對映。

沒有此標誌(且沒有 MPOL_F_STATIC_NODES),任何時候記憶體策略因允許節點集的變化而重新繫結時,節點 (Preferred) 或節點掩碼 (Bind, Interleave) 都會被重新對映到新的允許節點集。該重新對映可能無法在連續重新繫結時保留使用者傳遞的節點掩碼與其允許節點集的相對性質:如果允許節點集恢復到其原始狀態,節點掩碼 1,3,5 可能會被重新對映到 7-9,然後再對映到 1-3。

使用此標誌,重新對映完成後,使用者傳遞的節點掩碼中的節點編號將相對於允許的節點集。換句話說,如果使用者節點掩碼中設定了節點 0、2 和 4,則策略將作用於允許節點集中的第一個(在 Bind 或 Interleave 情況下,是第三個和第五個)節點。使用者傳遞的節點掩碼錶示相對於任務或 VMA 允許節點集的節點。

如果使用者節點掩碼包含超出新允許節點集範圍的節點(例如,當允許節點集僅為 0-3 時,使用者節點掩碼中設定了節點 5),則重新對映將回繞到節點掩碼的開頭,如果尚未設定,則在記憶體策略節點掩碼中設定該節點。

例如,考慮一個任務,它附加到一個包含記憶體節點 2-5 的 cpuset,並在此同一節點集上使用 MPOL_F_RELATIVE_NODES 設定了交錯策略。如果 cpuset 的記憶體節點更改為 3-7,則交錯現在將在節點 3、5-7 上發生。如果 cpuset 的記憶體節點隨後更改為 0、2-3、5,則交錯將在節點 0、2-3、5 上發生。

由於一致的重新對映,使用此標誌準備節點掩碼以指定記憶體策略的應用程式應忽略其當前實際由 cpuset 施加的記憶體位置,並假定它們始終位於記憶體節點 0 到 N-1 上來準備節點掩碼,其中 N 是策略旨在管理的記憶體節點數。然後讓核心重新對映到任務 cpuset 允許的記憶體節點集,因為這可能會隨時間而變化。

MPOL_F_RELATIVE_NODES 不能與 MPOL_F_STATIC_NODES 標誌結合使用。它也不能用於使用空節點掩碼(本地分配)建立的 MPOL_PREFERRED 策略。

記憶體策略引用計數

為解決使用/釋放競爭,struct mempolicy 包含一個原子引用計數字段。內部介面 mpol_get()/mpol_put() 分別遞增和遞減此引用計數。mpol_put() 僅當引用計數歸零時才將結構釋放回 mempolicy kmem 快取。

當分配新的記憶體策略時,其引用計數被初始化為“1”,表示安裝新策略的任務所持有的引用。當記憶體策略結構的指標儲存在另一個結構中時,會新增另一個引用,因為任務的引用將在策略安裝完成後被丟棄。

在策略的執行時“使用”期間,我們嘗試最小化對引用計數的原子操作,因為這可能導致快取行在 CPU 和 NUMA 節點之間跳動。這裡的“使用”意味著以下之一:

  1. 查詢策略,無論是透過任務本身 [使用下面討論的 get_mempolicy() API] 還是透過另一個任務使用 /proc/<pid>/numa_maps 介面。

  2. 檢查策略以確定頁分配的策略模式以及任何關聯的節點或節點列表。這被認為是“熱路徑”。請注意,對於 MPOL_BIND,“使用”範圍擴充套件到整個分配過程,該過程可能在頁回收期間休眠,因為 BIND 策略節點掩碼透過引用用於過濾不合格的節點。

我們可以按如下方式避免在上述使用期間額外增加引用:

  1. 我們永遠不需要獲取/釋放系統預設策略,因為系統一旦啟動並執行,它就不會被更改或釋放。

  2. 為了查詢策略,我們不需要對目標任務的任務策略或 VMA 策略進行額外引用,因為我們在查詢期間總是以讀模式獲取任務 mm 的 mmap_lock。set_mempolicy() 和 mbind() API(見下文)在安裝或替換任務或 VMA 策略時總是以寫模式獲取 mmap_lock。因此,不可能出現一個任務或執行緒在另一個任務或執行緒查詢策略時釋放策略的情況。

  3. 任務或 VMA 策略的頁分配使用發生在缺頁路徑中,我們在該路徑中以讀模式持有 mmap_lock。同樣,因為替換任務或 VMA 策略需要以寫模式持有 mmap_lock,所以當我們在進行頁分配時,策略不會在我們使用它時被釋放。

  4. 共享策略需要特殊考慮。一個任務可以替換共享記憶體策略,而另一個任務(具有獨立的 mmap_lock)正在根據該策略查詢或分配頁。為了解決這種潛在的競爭,共享策略基礎設施在查詢期間,在持有共享策略管理結構的自旋鎖的同時,向共享策略添加了一個額外引用。這要求我們在“使用”完策略後丟棄此額外引用。我們必須在與非共享策略相同的查詢/分配路徑中丟棄共享策略上的額外引用。因此,共享策略被如此標記,並且額外引用是“有條件地”丟棄的——即,僅針對共享策略。

    由於這種額外的引用計數,以及我們必須在自旋鎖下以樹形結構查詢共享策略,因此共享策略在頁分配路徑中使用成本更高。對於由在不同 NUMA 節點上執行的任務共享的共享記憶體區域上的共享策略尤其如此。可以透過始終回退到共享記憶體區域的任務或系統預設策略,或者透過將整個共享記憶體區域預先調入記憶體並鎖定來避免這種額外開銷。然而,這可能不適用於所有應用程式。

記憶體策略 API

Linux 支援 4 個系統呼叫來控制記憶體策略。這些 API 始終隻影響呼叫任務、呼叫任務的地址空間或對映到呼叫任務地址空間中的某些共享物件。

注意

定義這些 API 和使用者空間應用程式引數資料型別的標頭檔案位於一個不屬於 Linux 核心的軟體包中。帶有“sys_”字首的核心系統呼叫介面定義在 <linux/syscalls.h> 中;模式和標誌定義在 <linux/mempolicy.h> 中。

設定 [任務] 記憶體策略

long set_mempolicy(int mode, const unsigned long *nmask,
                                unsigned long maxnode);

將呼叫任務的“任務/程序記憶體策略”設定為由“mode”引數指定的模式和由“nmask”定義的節點集。“nmask”指向一個包含至少“maxnode”個節點 ID 的位掩碼。可以透過將“mode”引數與標誌組合來傳遞可選模式標誌(例如:MPOL_INTERLEAVE | MPOL_F_STATIC_NODES)。

有關更多詳細資訊,請參閱 set_mempolicy(2) 手冊頁

獲取 [任務] 記憶體策略或相關資訊

long get_mempolicy(int *mode,
                   const unsigned long *nmask, unsigned long maxnode,
                   void *addr, int flags);

根據“flags”引數,查詢呼叫任務的“任務/程序記憶體策略”,或指定虛擬地址的策略或位置。

有關更多詳細資訊,請參閱 get_mempolicy(2) 手冊頁

為任務地址空間範圍安裝 VMA/共享策略

long mbind(void *start, unsigned long len, int mode,
           const unsigned long *nmask, unsigned long maxnode,
           unsigned flags);

mbind() 將由 (mode, nmask, maxnodes) 指定的策略作為 VMA 策略安裝到由“start”和“len”引數指定的呼叫任務地址空間範圍內。可以透過“flags”引數請求附加操作。

有關更多詳細資訊,請參閱 mbind(2) 手冊頁。

為任務地址空間範圍設定主節點

long sys_set_mempolicy_home_node(unsigned long start, unsigned long len,
                                 unsigned long home_node,
                                 unsigned long flags);

sys_set_mempolicy_home_node 為任務地址範圍內存在的 VMA 策略設定主節點。該系統呼叫僅更新現有記憶體策略範圍的主節點。其他地址範圍將被忽略。主節點是頁分配將從中獲取的最近的 NUMA 節點。指定主節點會覆蓋預設分配策略,使記憶體分配接近執行 CPU 的本地節點。

記憶體策略命令列介面

雖然不嚴格屬於 Linux 記憶體策略實現的一部分,但存在一個命令列工具 numactl(8),它允許使用者

  • 透過 set_mempolicy(2)、fork(2) 和 exec(2) 為指定程式設定任務策略

  • 透過 mbind(2) 為共享記憶體段設定共享策略

numactl(8) 工具與包含記憶體策略系統呼叫封裝的庫的執行時版本一起打包。一些發行版將標頭檔案和編譯時庫打包在單獨的開發包中。

記憶體策略和 cpusets

記憶體策略在 cpusets 中工作,如上所述。對於需要一個或一組節點的記憶體策略,節點被限制在 cpuset 約束允許其記憶體的節點集中。如果為策略指定的節點掩碼包含 cpuset 不允許的節點且未使用 MPOL_F_RELATIVE_NODES,則使用為策略指定的節點集與具有記憶體的節點集的交集。如果結果為空集,則該策略被視為無效,無法安裝。如果使用 MPOL_F_RELATIVE_NODES,策略的節點將對映並摺疊到任務允許的節點集中,如前所述。

當兩個 cpusets 中的任務共享訪問一個記憶體區域(例如透過 shmget() 或使用 MAP_ANONYMOUS 和 MAP_SHARED 標誌的 mmap() 建立的共享記憶體段),並且任何任務在該區域上安裝共享策略時,記憶體策略和 cpusets 的互動可能會出現問題。在這種情況下,只有在兩個 cpusets 中都允許其記憶體的節點才能用於策略。獲取此資訊需要“跳出”記憶體策略 API 來使用 cpuset 資訊,並且需要知道其他任務可能連線到共享區域的 cpuset。此外,如果 cpusets 允許的記憶體集是分離的,“本地”分配是唯一有效的策略。