記憶體分配指南¶
Linux 提供了多種用於記憶體分配的 API。你可以使用 kmalloc 或 kmem_cache_alloc 系列函式分配小塊記憶體,使用 vmalloc 及其派生函式分配大塊虛擬連續區域,或者直接使用 alloc_pages 從頁分配器請求頁面。也可以使用更專門的分配器,例如 cma_alloc 或 zs_malloc。
大多數記憶體分配 API 使用 GFP 標誌來表示記憶體應如何分配。GFP 縮寫代表 “get free pages”(獲取空閒頁面),這是底層的記憶體分配函式。
分配 API 的多樣性以及眾多的 GFP 標誌使得“我應該如何分配記憶體?”這個問題並不容易回答,儘管很可能你應該使用
kzalloc(<size>, GFP_KERNEL);
當然,在某些情況下,必須使用其他分配 API 和不同的 GFP 標誌。
Get Free Page 標誌¶
GFP 標誌控制分配器的行為。它們告訴分配器可以使用哪些記憶體區域,分配器應該如何努力尋找空閒記憶體,記憶體是否可以被使用者空間訪問等。Documentation/core-api/mm-api.rst 提供了 GFP 標誌及其組合的參考文件,這裡我們簡要概述它們的推薦用法。
大多數時候,
GFP_KERNEL是你所需要的。核心資料結構、可 DMA 記憶體、inode 快取以及許多其他分配型別都可以使用GFP_KERNEL。請注意,使用GFP_KERNEL意味著GFP_RECLAIM,這意味著在記憶體壓力下可能會觸發直接回收;呼叫上下文必須允許睡眠。如果分配是從原子上下文(例如中斷處理程式)執行的,請使用
GFP_NOWAIT。此標誌會阻止直接回收以及 I/O 或檔案系統操作。因此,在記憶體壓力下,GFP_NOWAIT分配很可能失敗。此標誌的使用者需要提供合適的備用方案,以應對此類故障(如適用)。如果你認為訪問記憶體儲備是合理的,並且除非分配成功,否則核心將面臨壓力,你可以使用
GFP_ATOMIC。從使用者空間觸發的不可信分配應受 kmem 記賬約束,並且必須設定
__GFP_ACCOUNT位。對於需要記賬的GFP_KERNEL分配,有一個方便的GFP_KERNEL_ACCOUNT快捷方式。使用者空間分配應使用
GFP_USER、GFP_HIGHUSER或GFP_HIGHUSER_MOVABLE標誌中的任意一個。標誌名稱越長,其限制越少。
GFP_HIGHUSER_MOVABLE不要求分配的記憶體能被核心直接訪問,並且意味著資料是可移動的。
GFP_HIGHUSER意味著分配的記憶體不可移動,但不需要被核心直接訪問。一個例子可能是直接將資料對映到使用者空間但沒有定址限制的硬體分配。
GFP_USER意味著分配的記憶體不可移動,並且必須被核心直接訪問。
你可能會注意到現有程式碼中有很多分配指定了 GFP_NOIO 或 GFP_NOFS。歷史上,它們用於防止由直接記憶體回收呼叫回 FS 或 IO 路徑並阻塞已持有的資源所導致的遞迴死鎖。自 4.12 版本以來,解決此問題的首選方法是使用 Documentation/core-api/gfp_mask-from-fs-io.rst 中描述的新作用域 API。
其他遺留的 GFP 標誌是 GFP_DMA 和 GFP_DMA32。它們用於確保分配的記憶體可以被具有有限定址能力的硬體訪問。因此,除非你正在為具有此類限制的裝置編寫驅動程式,否則請避免使用這些標誌。即使是具有限制的硬體,也最好使用 dma_alloc* API。
GFP 標誌和回收行為¶
記憶體分配可能會觸發直接或後臺回收,瞭解頁分配器將如何努力滿足某個請求非常有用。
GFP_KERNEL & ~__GFP_RECLAIM- 樂觀分配,完全不嘗試釋放記憶體。這是最輕量級的模式,甚至不會觸發後臺回收。應謹慎使用,因為它可能耗盡記憶體,導致下一個使用者觸發更激進的回收。
GFP_KERNEL & ~__GFP_DIRECT_RECLAIM(或GFP_NOWAIT) - 樂觀分配,不從當前上下文嘗試釋放記憶體,但如果記憶體區域低於低水位線,可以喚醒 kswapd 來回收記憶體。可在原子上下文中使用,或者當請求是效能最佳化且慢速路徑有其他備用方案時使用。
(GFP_KERNEL|__GFP_HIGH) & ~__GFP_DIRECT_RECLAIM(即GFP_ATOMIC) - 非睡眠分配,具有昂貴的備用方案,因此可以訪問部分記憶體儲備。通常用於中斷/下半部上下文,並帶有昂貴的慢速路徑備用方案。
GFP_KERNEL- 允許後臺和直接回收,並使用預設頁分配器行為。這意味著不昂貴的分配請求基本上不會失敗,但對此行為沒有保證,因此呼叫者必須正確檢查失敗(例如,OOM killer 的受害者目前允許失敗)。
GFP_KERNEL | __GFP_NORETRY- 覆蓋預設分配器行為,所有分配請求都會盡早失敗,而不是導致破壞性回收(此實現中為一輪迴收)。OOM killer 不會被呼叫。
GFP_KERNEL | __GFP_RETRY_MAYFAIL- 覆蓋預設分配器行為,所有分配請求會非常努力地嘗試。如果回收無法取得任何進展,請求將失敗。OOM killer 不會被觸發。
GFP_KERNEL | __GFP_NOFAIL- 覆蓋預設分配器行為,所有分配請求將無限迴圈直到成功。這可能非常危險,特別是對於較大的 ऑर्डर。
選擇記憶體分配器¶
分配記憶體最直接的方法是使用 kmalloc() 家族中的函式。為了安全起見,最好使用將記憶體清零的例程,例如 kzalloc()。如果需要為陣列分配記憶體,可以使用 kmalloc_array() 和 kcalloc() 輔助函式。輔助函式 struct_size()、array_size() 和 array3_size() 可用於安全地計算物件大小而不會溢位。
使用 kmalloc 可分配的塊的最大大小是有限的。實際限制取決於硬體和核心配置,但最佳實踐是,對於小於頁大小的物件使用 kmalloc。
使用 kmalloc 分配的塊的地址至少對齊到 ARCH_KMALLOC_MINALIGN 位元組。對於大小是 2 的冪的情況,對齊也保證至少是相應的大小。對於其他大小,對齊保證至少是該大小的最大 2 的冪的約數。
使用 kmalloc() 分配的塊可以使用 krealloc() 重新調整大小。與 kmalloc_array() 類似:提供了 krealloc_array() 形式的輔助函式用於重新調整陣列大小。
對於大型分配,你可以使用 vmalloc() 和 vzalloc(),或者直接從頁分配器請求頁面。透過 vmalloc 及相關函式分配的記憶體不是物理連續的。
如果你不確定分配大小對於 kmalloc 是否過大,可以使用 kvmalloc() 及其派生函式。它將嘗試使用 kmalloc 分配記憶體,如果分配失敗,將使用 vmalloc 重試。使用 kvmalloc 時對 GFP 標誌有一些限制;請參閱 kvmalloc_node() 參考文件。請注意,kvmalloc 可能返回非物理連續的記憶體。
如果你需要分配許多相同的物件,可以使用 slab 快取分配器。在使用快取之前,必須使用 kmem_cache_create() 或 kmem_cache_create_usercopy() 設定快取。如果快取的一部分可能被複制到使用者空間,則應使用第二個函式。快取建立後,kmem_cache_alloc() 及其便捷封裝器可以從該快取中分配記憶體。
當分配的記憶體不再需要時,必須將其釋放。
透過 kmalloc 分配的物件可以使用 kfree 或 kvfree 釋放。透過 kmem_cache_alloc 分配的物件可以使用 kmem_cache_free、kfree 或 kvfree 釋放,其中後兩者可能更方便,因為不需要 kmem_cache 指標。
相同的規則適用於釋放函式的 _bulk 和 _rcu 變體。
透過 vmalloc 分配的記憶體可以使用 vfree 或 kvfree 釋放。透過 kvmalloc 分配的記憶體可以使用 kvfree 釋放。透過 kmem_cache_create 建立的快取應在首先釋放所有分配的物件後,才能使用 kmem_cache_destroy 釋放。