# slab 記憶體配置器 > 資料整理: [jserv](https://wiki.csie.ncku.edu.tw/User/jserv) ## 簡介 slab 一詞源於 SunOS 原始程式碼,開發者選擇這詞彙,是相較於 "object" 或 "cache",slab 更具辨識度,該詞彙的[原意](https://dictionary.cambridge.org/dictionary/english/slab)是針對石、木、金屬、食物等的板或片段。根據 Jeff Bonwick 發表在 1994 年的論文〈[The slab Allocator: An Object-Caching Kernel](https://www.usenix.org/conference/usenix-summer-1994-technical-conference/slab-allocator-object-caching-kernel)〉,記憶體配置器管理記憶體區域 (arenas) 的策略可區分為 sequential-fit, buddy 和 segregated-storage 這三個手法。slab 配置器屬於 segregated-storage。本文為了讓讀者易於對照論文,像是 cache, object, slab 和 buffer 一類的術語,保留原文。 所有的 slab 具有相同的空間,通常設定為為記憶體管理單元 (MMU) 的頁面 (page) 的大小,在 POSIX 相容的系統上,可藉由 `sysconf(_SC_PAGESIZE)` 查詢。slab 的記憶體從後端配置器中設定,這意味著從更大的記憶體區塊 (block) 中請求記憶體,通常至少是一個頁面的大小。一個 slab 包含多個大小相同的 buffer,slab 的資料部分(slab Data) 置於 slab 的結尾,以減少非法記憶體存取導致的損壞風險。一個 cache 包含多個 slab,cache 中的 slab 透過雙向鏈結串列連接,freelist 指標指向至少有個空閒 buffer 的 slab。 ![image](https://hackmd.io/_uploads/r18P0hBEA.png) 當 buffer 被釋放時,配置器會檢查 slab 中剩餘的空閒 buffer 的數量。若一個 slab 中所有的 buffer 都被釋放,則將該 slab 移到鏈結串列的末尾,確保 freelist 指標指向至少有一個空閒 buffer 的 slab。若 cache 中的 slab 數量低於一定的閾值(工作集 [work set] 大小),該 slab 不會立即返回後端配置器,以降低重新設定和初始化 slab 的開銷。 Buffer 大小會事先定義,例如 8B, 16B, 32B, 48B 等,每個大小對應一個包含雙向鏈結串列的 cache。設定記憶體時,配置器會將請求的大小匹配到不小於該請求的最小 buffer 大小,然後從對應的 cache 中找到有空閒 buffer 的 slab 進行設定。超過半頁大小的請求,直接從後端配置器設定。 釋放記憶體時,配置器藉由檢查是否對齊頁面大小來區分 buffer 和大型 object 的地址。buffer 不用對齊頁面大小,而大型 object 則要對齊頁面大小。這樣即可避免使用雜湊表進行映射的需求,確保記憶體管理的高效。 [userland-slab-allocator](https://github.com/bbu/userland-slab-allocator) 提供文字模式的 slab 配置動態展示。 ## 實作示範 儘管 Jeff Bonwick 描述的 slab 起初用於核心物件的管理,但同樣適用於使用者空間。記憶體配置器請求一個頁面大小的記憶體區塊,並將其分割成固定大小的片段。每個片段可以容納一個指標或整數,並鏈接成一個串列,串列的頭部指向第一個空閒節點。 以下是 Marek Vavrusa 提供的[實作程式碼](https://marek.vavrusa.com/memory/): ```c struct slab { void **head; }; /* Create page-aligned slab */ struct slab *slab = NULL; posix_memalign(&slab, page_size, page_size); slab->head = (void **) ((char *) slab + sizeof(struct slab)); /* Create a NULL-terminated slab freelist */ char* item = (char *) slab->head; for (unsigned i = 0; i < item_count; ++i) { *((void **) item) = item + item_size; item += item_size; } *((void **) item) = NULL; ``` 記憶體的配置操作只需取出鏈結串列的開頭,而記憶體釋放操作則相當於將一個新的開頭節點置入鏈結串列。這裡可留意到一個技巧:若 slab 對齊到頁面大小的邊界,可藉由向下取整數到頁面大小,以較低的成本來獲取 slab 指標。 ```c /* Free an element */ struct slab *slab = (void *) ((size_t) ptr & PAGESIZE_BITS); *((void **) ptr) = (void *) slab->head; slab->head = (void **) ptr; /* Allocate an element */ if ((item = slab->head)) { slab->head = (void **) *item; } else { /* No elements left. */ } ``` ## Linux 核心的 slab ![image](https://hackmd.io/_uploads/S1OvgRI4A.png) 以下是 Linux 核心的用語: * slab: 一組包含一個或多個連續頁面的記憶體,這些頁面包含特定大小的核心物件。 * slab cache: 多個相同類型 slab 的容器。有兩種類型的 cache: - 專用 cache: 由 Linux 核心建立,每個 cache 僅用於保存一種(常用)物件類型,例如 `mm_struct` 或 `cred_jar` - 通用 cache: 可容納任何特定大小 (加上 padding) 的物件。通常,這些 cache 中的物件大小為 2 的冪。當呼叫 `kmalloc()` 時,返回的已配置記憶體將在這些 cache 之一(例如 `kmalloc-32`, `kmalloc-64` 等)中,實際則取決於請求的大小。 * SLOB / SLAB / SLUB: 都是 Linux 核心中可用的 slab 配置器,較新的 Linux 核心版本已棄置 slob 和 slab,因此新的 Linux 僅提供 SLUB 這個 slab 的改進版本。 要顯示不同 slab cache 的資訊,我們可執行以下命令: ```shell $ sudo cat /proc/slabinfo ``` 參考輸出: ``` slabinfo - version: 2.1 # name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> ... kmalloc-8k 1700 1700 8192 4 8 kmalloc-4k 6329 6552 4096 8 8 kmalloc-2k 20791 20896 2048 16 8 kmalloc-1k 20906 20960 1024 32 8 kmalloc-512 337585 506176 512 64 8 kmalloc-256 85546 89472 256 64 4 kmalloc-128 297466 302272 128 64 2 kmem_cache_node 1070 1088 128 64 2 kmem_cache 896 896 256 64 4 ``` ### SLUB 配置器 我們專注於 SLUB 配置器,不僅因它是 Linux 核心預設的記憶體配置器,而且針對多核處理器進行調整。 Linux 核心中的記憶體配置會比使用者模式中略為單純,因為 metadata 通常不與資料主體存在放同一個空間 (例如使用者模式的 heap),而是儲存在不同的結構中。 每個 CPU 都有一個有效的 slab 用於每種類型的物件。這意味著,當在某個 CPU 上配置某種類型/大小的物件時,僅從該 CPU 的有效 slab 中配置空間,直到沒有更多的空閒空間。當沒有更多空間時,另一個 slab 將被標記為該 CPU 的有效 slab。需要注意的是,不同的處理器有不同的有效的 slab。 在 SLUB 配置器中,`kmem_cache` 結構體負責分類管理。說到分類,我們總是按照某些屬性進行分類,對應到真實世界,就是說賣場中各項商品的用途。 SLUB 的分類依據是什麼呢?首先是按照名字分類。呼叫 `kmem_cache_create()` 時,首個參數是 name,而這個名字保存在 kmem_cache 結構體的 name 成員中。我們可藉由以下命令查看系統中 SLUB 的分類: ``` $ cat /proc/slabinfo ``` 隨即可見 `task_struct` 和 `inode_cache` 等資訊。 另一種分類方式是按照大小。我們可在上述命令的輸出中看到 `kmalloc-512` 和 `kmalloc-256` 等字樣,這些是按照大小配置的記憶體快取。 按照名字分類隱含按大小分類的意義,這裡單獨列出是為了強調 SLUB 中對大小的計算,也就是進行預先存取 (prefetch) 的依據,後者的目的是提高系統性能。在解釋其作法前,我們回顧現實生活的案例:超級市場中,貨架上琳瑯滿目的商品都可算是預先存取,超市按照估計預先將商品放在貨架上,等待顧客購買。若每種商品貨架上只有一個,於是眾多顧客購買後,就要等待商家補貨,方可滿足購物需求,預先存取則可節省顧客的平均購買時間。 再來看商場服務人員的工作:在顧客購買商品的過程,服務人員也會持續補充貨架。倘若每當一個商品被拿走,服務人員就去倉庫取一個商品補充,那未免太繁瑣。因此,當貨架上的商品賣完時,服務人員才會去倉庫取出一批商品補充貨架。換言之,預先存取可降低服務人員的工作負擔。 不過前述的「一批」商品的數量該是多少?若拿太少,就不能提升每次取貨的效率,而若拿多,則貨架上放不下。生活中如此,Linux 核心也有對應的考量。 記憶體管理模組具備多個層次,SLUB 配置器建立在頁面配置器之上,因此可理解為頁面是 SLUB 的貨架。超市貨物上架前,應當做好以下估算: * 貨架的大小 * 貨架上能放多少貨物 SLUB 同樣需要計算以下: * 用多大的頁面來作為貨架 * 每個頁面中可以放多少對象 這些資料都保存在 `kmem_cache` 結構體中,整個計算過程在 `calculate_sizes()` 函式。注意:這裡說的頁面不是單個的實體頁面,而是核心中 `struct page` 對應的頁面。 用更細緻的案例來解說: 1. 假如申請的結構體 (貨物) 大小是 512 位元組,那麼頁面 (貨架) 可選擇 4K 位元組大小,每個頁面 (貨架) 上可存放 8 個結構體 (貨物)。 2. 假如申請的結構體 (貨物) 大小是 2050 位元組,那麼頁面 (貨架) 可選擇 8K 位元組大小,每個頁面 (貨架) 上可以存放 3 個結構體 (貨物)。 實際計算時,第二種情況的值可能不是如此,因為這樣選擇會導致較大的記憶體空間浪費,核心可能選擇更大的頁面,以減少記憶體浪費的發生。 - [ ] [`struct kmem_cache`](https://elixir.bootlin.com/linux/v6.9.2/source/mm/slab.h#L251) 該結構體是 slab cache 的存取介面。每種類型的物件都有一個對應的 `kmem_cache` 實例,並且每個大小的通用 cache 也有一個實例。它用來管理給定物件類型/大小的所有 slab。 ``` +------------------------------+ |name | | (char *) | +------------------------------+ |object_size | = original object size |inuse | = ALIGN(object_size, sizeof(void *)) |size | = ALIGN(inuse + padding + debug space, s->align) |align | |offset | | (int) | |reserved | +------------------------------+ |oo | |min | |max | | (kmem_cache_order_objects) | | +--------------------------+ | |order | | |order_objects | +---+--------------------------+ ``` 該結構體的重要成員: * `const char *name` : slab cache 的名稱,如 `kmalloc-32` 等 * `unsigned int object_size` : cache 中物件的大小 * `unsigned int object_size` : 包括 metadata 的物件大小,如果存在 metadata * `struct kmem_cache_cpu __percpu *cpu_slab` : 指向 `struct kmem_cache_cpu` 的指標,並非尋常的指標,而是個 `__per_cpu` 指標,意味著地址空間不同,即需要額外的運算來取得 `kmem_cache_cpu` 物件的實際地址 * `struct kmem_cache_node *node[MAX_NUMNODES]` : 指向 `kmem_cache_node` 結構體陣列的指標 * `unsigned int offset` : 在物件中的偏移量,指向下個空閒物件的指標儲存位置(當物件在 freelist 中時)。 * `unsigned long random`:僅在設定 `CONFIG_SLAB_FREELIST_HARDENED` 時存在,用於變更 freelist 中指標的地址 * `unsigned int *random_seq`:僅在設定 `CONFIG_SLAB_FREELIST_RANDOM` 時存在。這是一個隨機序列,以確保初始 freelist (建立 slab 時包含所有物件) 並非循序,而是隨機順序。換言之,意味著物件不會在開始時按順序配置 名為 `slab_caches` 的變數是個包含所有 slab cache 的雙向鏈結串列。名為 kmalloc_caches 的變數則包含所有的 kmalloc (通用) cache,其第一個索引表示 kmalloc 的 cache 類型,如下: * `KMALLOC_NORMAL` = 0: 對應於名為 `kmalloc-8`, `kmalloc-16` 等的 cache * `KMALLOC_CGROUP`:對應於名為 `kmalloc-cg-8`, `kmalloc-cg-16` 等的 cache * `KMALLOC_RECLAIM`:對應於名為 `kmalloc-rcl-8`, `kmalloc-rcl-16` 等的 cache * `KMALLOC_DMA`:對應於名為 `dma-kmalloc-8`, `dma-kmalloc-16` 等的 cache 第二個索引表示該 cache 中的物件大小。索引 n 對應於 kmalloc-2^n^,其中包含大小從 $2^{n-1}+1$ 到 $2^{n}$ 的物件。 - [ ] [`struct kmem_cache_cpu`](https://elixir.bootlin.com/linux/v6.9.2/source/mm/slub.c#L384) 該結構管理目前 CPU 的有效 slab。該結構體的重要成員如下: * `void **freelist` : 指向 slab 中下個可用物件的指標,亦即這個 slab 中的下次配置將返回此指標指向的地址 * `struct slab *slab` : 指向有效 slab 的指標 - [ ] [`struct kmem_cache_node`](https://elixir.bootlin.com/linux/v6.9.2/source/mm/slub.c#L425) 該結構體跟蹤部分 slab (亦即有空間的 slab),而非有效的 slab。根據配置,它還跟蹤已滿的 slab。該結構體的重要成員如下: * `unsigned long nr_partial`:部分 slab 的數量 * `struct list_head partial`:部分 slab 的串列 * `struct list_head full`:僅在設定 `CONFIG_SLUB_DEBUG` 時定義,是已滿 slab 的串列 - [ ] [`struct slab`](https://elixir.bootlin.com/linux/v6.9.2/source/mm/slab.h#L52) 該結構體包含一個 slab 的 metadata,包含以下: * `struct kmem_cache *slab_cache` : 該 slab 所屬的 slab cache,由 kmem_cache 表示 * `void *freelist` : slab 中第一個空閒物件 ### 建立 `kmem_cache` 結構體 kmem_cache 結構體是 SLUB 進行配置時的中心節點。在該節點上,我們可獲得所有關於這個 SLUB 的資訊。前述 `kmem_cache` 結構體內部的成員在建立後就不會改變,主要用來儲存 SLUB 的大小資訊,這裡我們關注以下: * cpu_slab * NUMA node 可見 SLUB 引入二個層級來管理記憶體:per-CPU 層級和 NUMA 節點 層級。下圖展示 `kmem_cache` 剛建立時的樣貌,可見一切都剛開始,內容都是空的。 ``` kmem_cache +------------------------------+ |name | | (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | |tid | #cpu | | (unsigned long) | | |freelist (void **) | NULL | |page (struct page *) | NULL | |partial (struct page *) | NULL +---+--------------------------+ |node[MAX_NUMNODES] | | (struct kmem_cache_node*) | | +--------------------------+ | |list_lock | | | (spinlock_t) | | |nr_partial | 0 | | (unsigned long) | | |partial | empty | | (struct list_head) | +---+--------------------------+ ``` ### 配置記憶體 由於採用分層設計,SLUB 配置時根據每個層次是否存在記憶體區分為幾種情況。在 Linux 核心運作過程,SLUB 按照以下順序檢查是否有可用的記憶體: * cpu_slab $\to$ freelist * cpu_slab $\to$ partial * node $\to$ partial 原理不難懂,就是先檢查最容易取得的空間是否有可用記憶體。以下展現 SLUB 在不同狀況下的樣貌。 - [ ] 什麼地方都沒有記憶體 按照上述順序,這個過程應該是以上三個空間中都沒有記憶體的情況。不過這也是 SLUB 剛建立後第一次配置時的情況,就是從 buddy 系統中取得一個頁面,裝到 cpu_slab $\to$ freelist 中。這步執行於 `allocate_slab()`。 這種情況的初始狀態是所有空間都是空的,但在下圖中展示二個狀態,目的是為了突出 page $\to$ freelist 指標的變化。 ``` kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | | |freelist | | | (void **) | | | | | |page +--|--->+--------+--------+--------+--------+ | | (struct page *) | | | | | | | | | +-------------------|--+ +--------+--------+--------+--------+ | | |freelist ---+ | | | |inuse = objects | | | |frozen = 1 | | | +----------------------+ | |partial | = NULL | | (struct page *) | +---+--------------------------+ kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | | |freelist ------|----+ = page->freelist | | (void **) | | | | | v | |page | +--------+--------+--------+--------+ | | (struct page *) | | | | | | | | +----------------------+ +--------+--------+--------+--------+ | | |freelist = NULL | | | |inuse = objects | | | |frozen = 1 | | | +----------------------+ | |partial | = NULL | | (struct page *) | +---+--------------------------+ ``` - [ ] freelist 上有記憶體 該情況也稱為 fast path,因為在該情況下,只需移動 freelist 指標即可,這步執行於 `slab_alloc_node()`。 下圖展示 freelist 從 a 移動到 A。注意 freelist 保存的是一個鏈結串列,而非線性儲存,因此指標的變化不一定是後移。 ``` kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | a A | |freelist ------|-------------+.................. | | (void **) | | . | | | v v | |page | +--------+--------+--------+--------+ | | (struct page *) | | | | | | | | +----------------------+ +--------+--------+--------+--------+ | | |freelist = NULL | | | |inuse = objects | | | |frozen = 1 | | | +----------------------+ | | | | |partial | = NULL | | (struct page *) | +---+--------------------------+ ``` - [ ] partial 上有記憶體 這時需要從 partial 中找到可用的頁面,裝到 freelist 上。這步執行於 `___slab_alloc()` 和 `get_freelist()`。 下面二圖分別展現裝之前和之後的變化,可見原來在 partial 中的頁面 A,安裝後移到 cpu_slab $\to$ page。注意在 partial 上的頁面,frozen 也是 1,但 inuse 則反映真實的配置情況而不是等於 objects。 ``` kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | | |freelist | = NULL | | (void **) | | |page | = NULL | | (struct page *) | | | | | |partial ------|--->+-----------+ +-----------+ | | (struct page *) | |A next|---->|B next| | | | +-----------+ +-----------+ | | | |pobjectsA | = |pobjectsB | + objectsA | | | | | | | | | | |inuse < | |inuse < | | | | | objectsA | | objectsB | | | | |frozen = 1 | |frozen = 1 | | | | +-----------+ +-----------+ +---+--------------------------+ kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | | |freelist ------|----+ = page->freelist | | (void **) | | | | | v | |page | +--------+--------+--------+--------+ | | (struct page *) A | | | | | | | | +----------------------+ +--------+--------+--------+--------+ | | |freelist = NULL | | | |inuse = objects | | | |frozen = 1 | | | +----------------------+ | | | | |partial ------|--->+-----------+ | | (struct page *) | |B next| | | | +-----------+ | | | |pobjectsB | = objectsB | | | | | | | | |inuse < | | | | | objectsB | | | | |frozen = 1 | | | | +-----------+ +---+--------------------------+ ``` - [ ] node 上有記憶體 看完 freelist 和 partial,最後我們來看 cpu_slab $\to$ node 上有記憶體的情況。在該情況,我們需要從 node 上取出頁面,並將其裝入 freelist 中。該過程執行於 `new_slab_objects()` $\to$ `get_partial()` $\to$ `get_partial_node()`。 注意在某些情況下,不僅會將頁面裝入 freelist 中,還會取出多個頁面裝入 partial 中,避免下次配置時找不到可用的記憶體。下圖展示的頁面 B 就是從 node 轉移到 partial 上。此外,在 node 上的頁面中,frozen 為 0。 ``` kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | | |freelist | = NULL | | (void **) | | |page | = NULL | | (struct page *) | | | | | |partial | = NULL | | (struct page *) | | | | +---+--------------------------+ |node[MAX_NUMNODES] | | (struct kmem_cache_node*) | | +--------------------------+ | |nr_partial = 3 | | | (unsigned long) | | |partial ----|--->+-----------+ +-----------+ +-----------+ | | (struct list_head) | |A lru|---->|B lru|---->| lru| +---+--------------------------+ +-----------+ +-----------+ +-----------+ |inuse < | |inuse < | | objects | | objects | |frozen = 0 | |frozen = 0 | +-----------+ +-----------+ kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | | |freelist ------|----+ = page->freelist | | (void **) | | | | | v | |page | +--------+--------+--------+--------+ | | (struct page *) A | | | | | | | | +----------------------+ +--------+--------+--------+--------+ | | |freelist = NULL | | | |inuse = objects | | | |frozen = 1 | | | +----------------------+ | | | | |partial ------|--->+-----------+ | | (struct page *) | |B next| | | | +-----------+ | | | |pobjects | = objects | | | | | | | | |inuse < | | | | | objects | | | | |frozen = 1 | | | | +-----------+ | | | +---+--------------------------+ |node[MAX_NUMNODES] | | (struct kmem_cache_node*) | | +--------------------------+ | |nr_partial = 1 | | | (unsigned long) | | |partial ----|--->+-----------+ | | (struct list_head) | |C lru| +---+--------------------------+ +-----------+ ``` ### 釋放記憶體 釋放雖然是配置的另一面,但在細節與 SLUB 配置略有不同。由於 SLUB 的設計是個鏈結串列,所以無論需要釋放記憶體對應的頁面在哪個地方,都是在 freelist 鏈結串列的開頭插入一個物件。因此,單個物件的釋放沒有太大區別。區別在於釋放物件後,需要將對應的頁面移動到適當的位置。 SLUB 釋放記憶體階段需要在以下三個位置之間移動頁面: * cpu_slab $\to$ freelist * cpu_slab $\to$ partial * node $\to$ partial - [ ] 移動 freelist 該情況相對簡單,不涉及頁面的移動,只要移動 cpu_slab $\to$ freelist 即可。 ``` kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | obj | |freelist ------|-------------+.................. | | (void **) | | . | | | v v | |page | +--------+--------+--------+--------+ | | (struct page *) | | | | | | | | +----------------------+ +--------+--------+--------+--------+ | | |freelist = NULL | | | |inuse = objects | | | |frozen = 1 | | | +----------------------+ | | | | |partial | = NULL | | (struct page *) | +---+--------------------------+ ``` - [ ] 替換 cpu_slab->page 該情況發生於配置記憶體時,當 cpu_slab $\to$ page 無法滿足配置需求時,而將這個頁面退回到 node $\to$ partial。過程由 `deactivate_slab()` 實作。 ``` kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | | |freelist ------|----+ = page->freelist | | (void **) | | | | | v | |page | +--------+--------+--------+--------+ | | (struct page *) A | | | | | | | | +----------------------+ +--------+--------+--------+--------+ | | |freelist = NULL | | | |inuse = objects | | | |frozen = 1 | | | +----------------------+ | | | | |partial ------|--->+-----------+ | | (struct page *) | |B next| | | | +-----------+ | | | |pobjects | = objects | | | | | | | | |inuse < | | | | | objects | | | | |frozen = 1 | | | | +-----------+ | | | +---+--------------------------+ |node[MAX_NUMNODES] | | (struct kmem_cache_node*) | | +--------------------------+ | |nr_partial = 1 | | | (unsigned long) | | |partial ----|--->+-----------+ | | (struct list_head) | |C lru| +---+--------------------------+ +-----------+ kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | | |freelist | = NULL | | (void **) | | |page | = NULL | | (struct page *) | | | | | |partial ------|--->+-----------+ | | (struct page *) | |B next| | | | +-----------+ | | | |pobjects | = objects | | | | | | | | |inuse < | | | | | objects | | | | |frozen = 1 | | | | +-----------+ | | | +---+--------------------------+ |node[MAX_NUMNODES] | | (struct kmem_cache_node*) | | +--------------------------+ | |nr_partial = 1 | | | (unsigned long) | | |partial ----|--->+-----------+ +-----------+ | | (struct list_head) | |A lru|---->|C lru| +---+--------------------------+ +-----------+ +-----------+ |inuse < | | objects | |frozen = 0 | +-----------+ ``` - [ ] 解凍 cpu_slab $\to$ partial 當偵測到 cpu_slab $\to$ partial 上的物件數量足夠多時,系統會將鏈結串列上的所有頁面退回到 node $\to$ partial。注意該過程會將 cpu_slab $\to$ partial 清空。 ``` kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | | |freelist ------|----+ = page->freelist | | (void **) | | | | | v | |page | +--------+--------+--------+--------+ | | (struct page *) A | | | | | | | | +----------------------+ +--------+--------+--------+--------+ | | |freelist = NULL | | | |inuse = objects | | | |frozen = 1 | | | +----------------------+ | | | | |partial ------|--->+-----------+ +-----------+ | | (struct page *) | |B next|---->|C next| | | | +-----------+ +-----------+ | | | |pobjectsA | = |pobjectsB | + objectsA | | | | | | | | | | |inuse < | |inuse < | | | | | objectsA | | objectsB | | | | |frozen = 1 | |frozen = 1 | | | | +-----------+ +-----------+ | | | +---+--------------------------+ |node[MAX_NUMNODES] | | (struct kmem_cache_node*) | | +--------------------------+ | |nr_partial = 1 | | | (unsigned long) | | |partial ----|--->+-----------+ | | (struct list_head) | |D lru| +---+--------------------------+ +-----------+ kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | | |freelist ------|----+ = page->freelist | | (void **) | | | | | v | |page | +--------+--------+--------+--------+ | | (struct page *) A | | | | | | | | +----------------------+ +--------+--------+--------+--------+ | | |freelist = NULL | | | |inuse = objects | | | |frozen = 1 | | | +----------------------+ | | | | |partial | = NULL | | (struct page *) | | | | +---+--------------------------+ |node[MAX_NUMNODES] | | (struct kmem_cache_node*) | | +--------------------------+ | |nr_partial = 1 | | | (unsigned long) | | |partial ----|--->+-----------+ +-----------+ +-----------+ | | (struct list_head) | |B lru|---->|C lru|---->|D lru| +---+--------------------------+ +-----------+ +-----------+ +-----------+ |inuse < | |inuse < | | objects | | objects | |frozen = 0 | |frozen = 0 | +-----------+ +-----------+ ``` - [ ] 從 node $\to$ partial 取出一個頁面加入 cpu_slab $\to$ partial 特定狀況下,我們會將 node $\to$ partial 鏈結串列中的一個頁面放到 cpu_slab $\to$ partial 上。 ``` kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | | |freelist ------|----+ = page->freelist | | (void **) | | | | | v | |page | +--------+--------+--------+--------+ | | (struct page *) A | | | | | | | | +----------------------+ +--------+--------+--------+--------+ | | |freelist = NULL | | | |inuse = objects | | | |frozen = 1 | | | +----------------------+ | | | | |partial ------|--->+-----------+ | | (struct page *) | |B next| | | | +-----------+ | | | |pobjectsB | = objectsB | | | | | | | | |inuse < | | | | | objectsB | | | | |frozen = 1 | | | | +-----------+ | | | +---+--------------------------+ |node[MAX_NUMNODES] | | (struct kmem_cache_node*) | | +--------------------------+ | |nr_partial = 3 | | | (unsigned long) | | |partial ----|--->+-----------+ +-----------+ | | (struct list_head) | |C lru|---->|D lru| +---+--------------------------+ +-----------+ +-----------+ |inuse < | | objects | |frozen = 0 | +-----------+ kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | | |freelist ------|----+ = page->freelist | | (void **) | | | | | v | |page | +--------+--------+--------+--------+ | | (struct page *) A | | | | | | | | +----------------------+ +--------+--------+--------+--------+ | | |freelist = NULL | | | |inuse = objects | | | |frozen = 1 | | | +----------------------+ | | | | |partial ------|--->+-----------+ +-----------+ | | (struct page *) | |C next|---->|B next| | | | +-----------+ +-----------+ | | | |pobjectsC | = |pobjectsB | + objectsC | | | | | | | | | | |inuse < | |inuse < | | | | | objectsC | | objectsB | | | | |frozen = 1 | |frozen = 1 | | | | +-----------+ +-----------+ | | | +---+--------------------------+ |node[MAX_NUMNODES] | | (struct kmem_cache_node*) | | +--------------------------+ | |nr_partial = 1 | | | (unsigned long) | | |partial ----|--->+-----------+ | | (struct list_head) | |C lru| +---+--------------------------+ +-----------+ ``` - [ ] 將頁面加入 node $\to$ partial 當一個頁面的所有空間都被配置後,該頁面就不會出現在其他位置 (除非是 Linux 啟用 `CONFIG_SLUB_DEBUG` 選項)。當釋放頁面上的一個物件時,該頁面會重新出現在 node $\to$ partial 上。 為了清晰起見,下圖展示在 Linux 核心在啟用 `CONFIG_SLUB_DEBUG` 時,node->full 鏈結串列的開頭,以表示頁面的移動情況。 ``` kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | | |freelist ------|----+ = page->freelist | | (void **) | | | | | v | |page | +--------+--------+--------+--------+ | | (struct page *) A | | | | | | | | +----------------------+ +--------+--------+--------+--------+ | | |freelist = NULL | | | |inuse = objects | | | |frozen = 1 | | | +----------------------+ | | | | |partial ------|--->+-----------+ | | (struct page *) | |B next| | | | +-----------+ | | | |pobjectsB | = objectsB | | | | | | | | |inuse < | | | | | objectsB | | | | |frozen = 1 | | | | +-----------+ | | | +---+--------------------------+ |node[MAX_NUMNODES] | | (struct kmem_cache_node*) | | +--------------------------+ | |nr_partial = 3 | | | (unsigned long) | | |partial ----|--->+-----------+ +-----------+ | | (struct list_head) | |C lru|---->|D lru| | +--------------------------+ +-----------+ +-----------+ | |full ----|--->+-----------+ | | (struct list_head) | |E lru| +---+--------------------------+ +-----------+ kmem_cache +------------------------------+ |name (char *) | +------------------------------+ |cpu_slab | * per cpu variable pointer | (struct kmem_cache_cpu*) | | +--------------------------+ | |stat[NR_SLUB_STAT_ITEMS] | | | (unsigned) | | +--------------------------+ | |tid (unsigned long) | | | | | |freelist ------|----+ = page->freelist | | (void **) | | | | | v | |page | +--------+--------+--------+--------+ | | (struct page *) A | | | | | | | | +----------------------+ +--------+--------+--------+--------+ | | |freelist = NULL | | | |inuse = objects | | | |frozen = 1 | | | +----------------------+ | | | | |partial ------|--->+-----------+ | | (struct page *) | |B next| | | | +-----------+ | | | |pobjectsB | = objectsB | | | | | | | | |inuse < | | | | | objectsB | | | | |frozen = 1 | | | | +-----------+ | | | +---+--------------------------+ |node[MAX_NUMNODES] | | (struct kmem_cache_node*) | | +--------------------------+ | |nr_partial = 3 | | | (unsigned long) | | |partial ----|--->+-----------+ +-----------+ +-----------+ | | (struct list_head) | |E lru|---->|C lru|---->|D lru| | +--------------------------+ +-----------+ +-----------+ +-----------+ | |full | = NULL | | (struct list_head) | +---+--------------------------+ ``` ## SLUB 的安全強化機制 一些與 freelist 相關的核心選項如下: - [ ] `CONFIG_SLAB_FREELIST_HARDENED` 亦即安全強化的的 freelist,啟用此選項後,freelist 中的指標將通過與每個 cache 的隨機數 (`s->random`) 和指標儲存地址 (`ptr_addr`) 進行 XOR 運算,以增加攻擊者猜測的難度: ```c /* * Returns freelist pointer (ptr). With hardening, this is obfuscated * with an XOR of the address where the pointer is held and a per-cache * random number. */ static inline void *freelist_ptr(const struct kmem_cache *s, void *ptr, unsigned long ptr_addr) { #ifdef CONFIG_SLAB_FREELIST_HARDENED /* ... */ return (void *)((unsigned long)ptr ^ s->random ^ swab((unsigned long)kasan_reset_tag((void *)ptr_addr))); #else return ptr; #endif } ``` 若存在一個漏洞允許攻擊者覆蓋這些指標,為了成功攻擊,他們需要知道 `s->random` 和 `ptr_addr`(這有可能發生,但難度更大)。 - [ ] `CONFIG_SLAB_FREELIST_RANDOMIZATION` 對 freelist 進行隨機化,在 Linux v4.8 中引入,啟用後,freelist 中區塊的初始順序不再是連續的。亦即原本當 slab 建立時,所有區塊都是空閒的,且是 freelist 的一部分。但若若啟用 `CONFIG_SLAB_FREELIST_RANDOMIZATION` 選項,配置將不再按它們在 slab 中的順序 (第一個配置返回 slab 中的第一個區塊,依此類推),而是按隨機順序。這由每個 cache 隨機序列(`s->random_seq`)決定(在 cache 初始化時定義),並且 cache 中的每個 slab 都從這個序列中隨機選擇一個起點 (這樣不同 slab 中的配置不會從相同的偏移量開始)。 此舉減輕核心 heap overflow 的風險,在此之前,若易受攻擊的物件和目標物件一個接一個地配置,它們總是相鄰的,因此很容易被攻擊者所利用。然而,這可透過配置許多目標物件,然後釋放其中一個,接著配置一個易受攻擊的物件來輕易繞過。事實上,易受攻擊的物件將被目標物件包圍,然後我們可觸發 heap overflow,並覆蓋其中一個目標物件。Michael S 和 Vitaly Nikolenko 撰寫的文章〈[Linux kernel heap feng shui in 2022](https://duasynt.com/blog/linux-kernel-heap-feng-shui-2022)〉清楚解釋這點,feng shui 即「風水」的拼音。 ## 待整理 * [hardened_malloc](https://github.com/GrapheneOS/hardened_malloc) * [SlabDbg](https://github.com/NeatMonster/slabdbg): GDB plug-in that makes it easier to develop Linux kernel exploits targeting the SLUB allocator.