
1. 從一次內存泄漏排查說起為什么需要關注 kmalloc_cache前陣子幫朋友看一個內核模塊的問題現(xiàn)象很典型模塊加載后跑一段時間/proc/meminfo里的Slab項持續(xù)上漲卸載模塊后也不見回落。用slabtop一看某個kmalloc-192的緩存對象數(shù)量只增不減。順著這條線索往下挖最后定位到模塊里一處kmalloc之后忘了kfree的分支。問題本身不復雜但排查過程中我重新把kmalloc_cache這套結構翻了一遍發(fā)現(xiàn)很多平時寫代碼時想當然的地方其實背后都有講究。這篇就接著上一部分 SLUB 分配器的內容專門聊kmalloc_cache這個結構。如果你寫過內核模塊、調過驅動、或者單純對 SLUB 怎么管理通用內存分配感興趣這篇應該能幫你把kmalloc到底從哪兒拿內存這件事徹底理清楚。我會從kmalloc_cache的組織方式講起拆開kmalloc_caches這個二維數(shù)組說清楚 size index 和 type 是怎么映射到具體緩存的再結合kmalloc的調用鏈看看一次分配到底走了哪些路徑。中間會穿插幾個我自己踩過的坑比如kmalloc大小和實際緩存不匹配導致的浪費、GFP標志選錯引發(fā)的連鎖反應。先把結論擺前面kmalloc_cache不是一張簡單的表而是一個按大小檔位 × 分配類型組織的二維緩存矩陣SLUB 通過它把任意kmalloc請求快速路由到最合適的 slab 緩存上。理解了這個矩陣的構造邏輯很多關于內存開銷、碎片、性能的疑問就都有答案了。2. kmalloc_caches 二維數(shù)組的構造邏輯2.1 為什么是二維而不是一維剛接觸這塊的時候我有個疑問既然kmalloc是按大小分配搞一個大小到緩存的一維數(shù)組不就行了為什么要弄成二維后來看代碼才明白多出來的那一維是分配類型kmalloc_type。內核里kmalloc的調用場景差異很大有的是普通內核對象分配有的是 DMA 相關有的要求內存可回收reclaimable有的來自cgroup受限上下文。這些場景對內存的物理屬性、回收策略、記賬方式要求都不一樣。如果所有請求都擠在同一個緩存里一個要求 DMA 能用的分配和一個普通分配混在一起物理頁屬性就沒法保證。所以 SLUB 的做法是同一檔大小按類型拆成多個獨立的緩存。這樣每個緩存內部的 slab 頁屬性一致分配時不用再做屬性篩選。這就是二維結構的根本原因——第一維是大小檔位第二維是分配類型。用生活化的類比這就像倉庫管理同樣是小件區(qū)但易碎品、危險品、普通貨物要分開放因為它們的存儲條件不同。你不能因為都是小件就堆一個貨架上。2.2 大小檔位的劃分規(guī)則kmalloc支持的大小不是連續(xù)的而是一組離散檔位。在常見的 64 位配置下檔位大致是這樣排的檔位序號對象大小字節(jié)典型用途08極小結構體、指針數(shù)組116小型描述符232常見小對象364中等小對象496非 2 的冪檔位5128常用檔位6192非 2 的冪檔位7256常用檔位8512中等對象91024較大對象102048大對象114096接近頁大小128192超過單頁這里有個細節(jié)值得說檔位里混進了 96 和 192 這種非 2 的冪。為什么因為實測發(fā)現(xiàn)內核里大量分配請求集中在這些尺寸附近如果只有 64 和 128那么一個 96 字節(jié)的請求會被塞進 128 的緩存浪費 32 字節(jié)而 192 的請求塞進 256 會浪費 64 字節(jié)。加上這兩個檔位后整體內存浪費明顯下降。這是典型的用一點管理復雜度換實際內存效率的取舍。注意檔位的具體數(shù)值和內核配置有關CONFIG_SLAB_MERGE_DEFAULT、架構位寬、是否開啟CONFIG_RANDOM_KMALLOC_CACHES都會影響最終結果??创a時不要死記數(shù)字要理解劃分思路。2.3 類型維度的實際含義第二維的類型在代碼里對應enum kmalloc_cache_type。常見的有這么幾類KMALLOC_NORMAL普通分配最常用。KMALLOC_RECLAIM可回收分配內存緊張時可以被回收。KMALLOC_DMA要求物理地址連續(xù)且落在 DMA 可尋址范圍。KMALLOC_CGROUP帶 cgroup 內存記賬的分配。每種類型對應一組獨立的緩存。也就是說kmalloc-128這個名字其實對應好幾個實際緩存分別服務于不同場景。你在slabtop里看到的kmalloc-128往往只是其中一類其他類型可能顯示為帶后綴的名字。這里有個容易踩的坑類型選錯不會報錯但會帶來隱性代價。比如你本可以用KMALLOC_NORMAL卻因為習慣寫了GFP_DMA那分配就會落到 DMA 類型的緩存里。DMA 緩存為了保證可尋址往往預留更保守長期跑下來內存占用會比普通緩存高。我見過一個驅動因為全程用GFP_DMA在非 DMA 場景下白白多占了幾十 MB。3. size index 與 type 如何映射到具體緩存3.1 從請求大小到 index 的換算當你調用kmalloc(size, flags)時第一步是把size換算成檔位序號。這個換算不是簡單的log2因為存在 96、192 這種非 2 的冪檔位。內核用的是查表加位運算結合的方式。大致邏輯是先判斷size落在哪個區(qū)間再用預計算的映射表拿到 index。對于 2 的冪檔位可以用fls找最高有效位快速定位對于 96、192 這種需要額外判斷。整個換算過程被設計得盡量短因為kmalloc是熱路徑每次分配都要走一遍。我實測過一個細節(jié)在 x86_64 上kmalloc(100, GFP_KERNEL)會落到 128 檔位而不是 96。因為 100 大于 96只能往上取到 128。所以如果你對內存敏感把結構體大小壓到剛好不超過某個檔位能省下可觀的浪費。比如把 100 字節(jié)的結構體優(yōu)化到 96 字節(jié)以內每個對象就省 32 字節(jié)十萬個對象就是 3 MB 多。3.2 type 的判定時機類型不是從size推出來的而是從flags里解析出來的。kmalloc內部會檢查flags中的__GFP_DMA、__GFP_RECLAIMABLE等位決定用哪個 type。這里有個順序問題類型判定在大小換算之前還是之后實際代碼里兩者是耦合的因為最終的緩存指針要通過kmalloc_caches[type][index]取。所以流程是先解析 flags 得到 type再把 size 換算成 index最后查二維數(shù)組。偽代碼大概長這樣static __always_inline struct kmem_cache * kmalloc_slab(size_t size, gfp_t flags) { unsigned int index; enum kmalloc_cache_type type; if (size 8) index 0; else if (size 16) index 1; /* ... 中間省略 ... */ else index 12; type kmalloc_type(flags, _RET_IP_); return kmalloc_caches[type][index]; }真實代碼比這復雜有KMALLOC_MIN_SIZE處理、有CONFIG_RANDOM_KMALLOC_CACHES的隨機化但主干邏輯就是這樣。3.3 緩存指針的緩存機制每次kmalloc都去查二維數(shù)組雖然數(shù)組在內存里訪問很快但仍然是熱路徑上的開銷。所以內核做了一層優(yōu)化把常用的緩存指針直接內聯(lián)到調用點附近或者用__builtin_constant_p在編譯期就把 size 是常量的情況算好。這就是為什么kmalloc經常被寫成宏或者__always_inline函數(shù)——編譯器看到kmalloc(64, GFP_KERNEL)這種常量參數(shù)能直接把 index 和 type 都算出來最終變成一次數(shù)組索引加一次 slab 分配調用。如果你傳的是變量 size那就只能運行時算。實操建議在性能敏感的代碼里盡量讓kmalloc的 size 是編譯期常量。我做過對比測試常量 size 的分配路徑比變量 size 短一截在高頻分配場景下差距能到百分之幾。4. kmalloc 調用鏈上的關鍵分支4.1 小對象走 slab大對象走頁分配kmalloc并不是所有請求都走kmalloc_cache。當 size 超過最大檔位通常是 8192 或更大取決于配置時會直接走頁分配器繞過 slab。這個分界點很關鍵。為什么要有這個分界因為 slab 的價值在于把小對象打包進頁里減少碎片和管理開銷。當對象本身就接近或超過一頁時打包沒意義了直接分配頁更高效。所以kmalloc內部有個判斷size 大于KMALLOC_MAX_CACHE_SIZE就走alloc_pages。這個閾值在不同架構上不一樣。32 位系統(tǒng)上可能是 819264 位可能更大。寫代碼時如果依賴這個邊界一定要用KMALLOC_MAX_CACHE_SIZE宏而不是硬編碼數(shù)字。4.2 分配失敗時的重試與降級kmalloc在緩存里拿不到對象時會觸發(fā) slab 的擴容向頁分配器要新頁切成對象掛到 freelist 上。這個過程涉及GFP標志的傳遞。這里有個我踩過的坑GFP_KERNEL在原子上下文里用會出問題。因為GFP_KERNEL允許睡眠等待內存而原子上下文比如中斷處理、持有自旋鎖時不能睡眠。如果你在中斷里寫了kmalloc(size, GFP_KERNEL)輕則警告重則死鎖。正確做法是用GFP_ATOMIC它不允許睡眠拿不到就返回 NULL。但GFP_ATOMIC也不是隨便用的。它成功率高但代價是可能動用緊急內存池長期大量使用會耗盡預留內存。所以原則是能睡眠的上下文用GFP_KERNEL不能睡眠的才用GFP_ATOMIC且要處理 NULL 返回。4.3 與 per-CPU 緩存的交互SLUB 的高性能很大程度來自 per-CPU 緩存。每個 CPU 有自己的 freelist分配時優(yōu)先從本地拿避免鎖競爭。kmalloc_cache對應的每個 slab 緩存都有自己的 per-CPU 結構。這意味著同一個kmalloc請求在不同 CPU 上可能拿到不同 slab 頁里的對象。這對大多數(shù)場景沒影響但如果你依賴對象地址的某種局部性比如假設連續(xù)分配的對象在內存上相鄰就會失望。我見過有人寫代碼假設兩次kmalloc的地址差是固定值結果在多核機器上跑出各種詭異問題。正確的做法是不要對kmalloc返回的地址做任何跨調用的連續(xù)性假設。需要連續(xù)內存就用kmalloc一次分配一整塊或者用vmalloc。5. 幾個容易混淆的相鄰概念5.1 kmalloc_cache 與 kmem_cache 的關系初學者容易把這兩個搞混。kmem_cache是 SLUB 里通用的緩存結構任何自定義緩存都是kmem_cache。而kmalloc_cache是專門為kmalloc服務的那批緩存它們是kmem_cache的特例。關系上kmalloc_caches[type][index]里存的就是struct kmem_cache *。也就是說kmalloc_cache本質上是預創(chuàng)建好的一批kmem_cache按大小和類型組織成矩陣。你自己調kmem_cache_create創(chuàng)建的緩存和kmalloc用的緩存在 SLUB 內部是同一套機制管理的。理解這一點后很多行為就通了kmalloc的緩存也會出現(xiàn)在/proc/slabinfo里也會被slabtop統(tǒng)計因為它們就是普通的 slab 緩存。5.2 kmalloc 與 kzalloc、kcalloc 的區(qū)別這三個經常一起出現(xiàn)區(qū)別在初始化kmalloc只分配不初始化內存里是臟數(shù)據。kzalloc分配并清零。kcalloc分配數(shù)組并清零內部會做乘法溢出檢查。從kmalloc_cache的角度看它們走的是同一套緩存區(qū)別只在分配后是否memset。所以kzalloc的額外開銷就是一次清零。對于大對象清零成本不可忽略對于小對象現(xiàn)代 CPU 的memset很快通常不是瓶頸。經驗如果對象馬上會被完整覆蓋用kmalloc就行別習慣性寫kzalloc。我在一個高頻路徑上把kzalloc改成kmalloc省下的清零時間在壓測里能看出來。5.3 與 vmalloc 的邊界kmalloc保證物理連續(xù)vmalloc只保證虛擬連續(xù)。kmalloc_cache管的是前者。當你要的 size 很大、物理連續(xù)內存難找時kmalloc會失敗這時候才考慮vmalloc。但vmalloc有代價需要建立頁表映射TLB 壓力大訪問速度可能不如kmalloc的內存。所以原則是能用kmalloc就用kmalloc實在要不到連續(xù)物理頁再用vmalloc。不要因為vmalloc能分配更大就無腦選它。6. 排查與觀測怎么看清 kmalloc_cache 的實際狀態(tài)6.1 用 slabinfo 看緩存分布/proc/slabinfo是最直接的窗口。里面每一行是一個緩存字段包括對象大小、活躍對象數(shù)、總對象數(shù)等。關注kmalloc-*開頭的行能看出各檔位的使用情況。我排查內存泄漏時的套路是隔一段時間抓兩次slabinfo對比active_objs的變化。如果某個kmalloc-*的活躍對象持續(xù)上漲且不回落基本就是泄漏。再結合代碼里該檔位對應的分配點范圍就縮小了。6.2 slabtop 的實時視角slabtop是slabinfo的動態(tài)版按對象數(shù)或內存占用排序。它的好處是能一眼看到誰在吃內存。我一般按Slab大小排序看排前面的幾個緩存是不是符合預期。有個細節(jié)slabtop顯示的kmalloc-*可能不包含所有類型。因為類型維度的緩存名字可能帶后綴或者被合并顯示。要精確統(tǒng)計還是得看slabinfo原始數(shù)據。6.3 用 tracepoint 追蹤單次分配如果泄漏點很隱蔽靜態(tài)統(tǒng)計不夠用可以上 tracepoint。kmalloc相關的 tracepoint 能記錄每次分配的調用地址、size、返回指針。配合perf或ftrace能精確定位到是哪行代碼在反復分配。這個手段比較重一般只在前面幾步定位不到時才用。但它確實有效——我有個同事就是靠 tracepoint 抓到一個在錯誤分支里反復kmalloc卻沒釋放的 bug。7. 寫代碼時的幾條實用原則7.1 讓 size 貼近檔位上限前面提過kmalloc按檔位分配實際占用是檔位大小而非請求大小。所以結構體設計時盡量讓sizeof落在檔位內且接近上限。比如你的結構體是 130 字節(jié)會落到 192 檔位浪費 62 字節(jié)如果能壓到 128 以內就省下 64 字節(jié)。壓縮手段包括調整字段順序減少 padding、用位域、把不常用的字段拆出去單獨分配。這些優(yōu)化在對象數(shù)量大時收益明顯。7.2 flags 要精確不要圖省事全用GFP_KERNEL或全用GFP_ATOMIC。根據上下文選上下文推薦 flags說明進程上下文可睡眠GFP_KERNEL最常用中斷/軟中斷GFP_ATOMIC不可睡眠持有自旋鎖GFP_ATOMIC不可睡眠DMA 場景GFP_KERNEL | GFP_DMA按需加 DMA可回收對象GFP_KERNEL | __GFP_RECLAIMABLE內存緊張時可回收選錯 flags 的代價前面說過要么死鎖要么浪費內存要么記賬錯誤。7.3 釋放要及時且路徑唯一kmalloc的配對kfree一定要在所有路徑上都覆蓋。我見過太多 bug 是正常路徑釋放了錯誤路徑忘了。建議用goto統(tǒng)一錯誤處理或者用__free之類的清理屬性新內核支持減少遺漏。小技巧如果編譯器支持__attribute__((cleanup))可以封裝一個自動釋放的包裝讓kfree在作用域結束時自動執(zhí)行。這在復雜函數(shù)里能顯著降低泄漏風險。7.4 別對地址做連續(xù)性假設前面強調過per-CPU 緩存讓分配地址不可預測。任何依賴兩次分配相鄰的代碼都是脆弱的。需要連續(xù)內存就一次分配夠或者用專門的數(shù)據結構。8. 一個完整的觀測實例假設你懷疑某個模塊有kmalloc泄漏可以按這個流程走加載模塊記錄初始slabinfo中相關kmalloc-*的active_objs。跑業(yè)務邏輯觸發(fā)模塊的主要路徑。再次抓取slabinfo對比active_objs。如果某檔位持續(xù)上漲用slabtop確認再用 tracepoint 定位具體調用點。找到分配點后檢查所有分支的釋放路徑。這個流程我在多個項目里用過基本能覆蓋大部分kmalloc相關問題。關鍵是要有基線對比的意識單次快照說明不了問題。9. 關于 kmalloc_cache 的一點個人體會把kmalloc_cache這套結構吃透之后我寫內核代碼時對內存的手感明顯好了。以前kmalloc就是隨手一寫現(xiàn)在會下意識想這個 size 會落到哪個檔位flags 選對了嗎釋放路徑全不全這些思考不費多少時間但能避免很多后期排查的麻煩。SLUB 的設計哲學其實很樸素用結構化的緩存矩陣把千變萬化的分配請求收斂到有限的管理單元上。kmalloc_cache就是這個哲學的集中體現(xiàn)。理解了它再去看kmem_cache_create、kmem_cache_alloc這些接口會發(fā)現(xiàn)它們和kmalloc是同一套邏輯的不同入口。最后分享一個我常用的檢查習慣每次寫完涉及kmalloc的模塊加載后先跑一遍slabtop看看有沒有意外的緩存增長。這個動作花不了幾秒但經常能在早期發(fā)現(xiàn)苗頭。內存問題越早發(fā)現(xiàn)越好查等到線上跑出問題時現(xiàn)場往往已經很難復現(xiàn)了。