
1. 从 kmem_cache 到 kmalloc_cache 的认知跨越很多人在啃 Linux 内存管理源码时第一次看到kmalloc_caches这个数组都会愣一下——明明上一节刚把kmem_cache的结构和创建流程捋清楚怎么突然又冒出来一个kmalloc_cache它跟kmem_cache到底什么关系是继承、是包装、还是干脆就是同一个东西换了个马甲我当初也是卡在这个点上好久。后来把slab_common.c、slub.c和slab.h来回翻了几遍才慢慢品出味道来kmalloc_cache本质上就是kmem_cache的一个“特化实例”它专门服务于kmalloc系列接口把“按大小分级”这件事做到了极致。你可以把它理解成 SLUB 分配器给内核开发者准备的一套“标准尺码表”——你要 8 字节、16 字节、32 字节……一直到 8MB它都提前把对应的缓存建好了你直接拿就行不用每次自己kmem_cache_create。这一节我们就专门把kmalloc_cache这个结构从里到外拆一遍。它长什么样、怎么被初始化的、和kmem_cache的字段有什么异同、kmalloc_caches这个二维数组又是怎么组织的、KMALLOC_NORMAL、KMALLOC_RECLAIM、KMALLOC_DMA这几个类型到底在什么场景下用——这些都会讲到。如果你正在读mm/slub.c或者准备做内核内存相关的性能调优这篇内容应该能帮你省下不少来回翻代码的时间。2. kmalloc_cache 结构的整体设计与字段拆解2.1 它到底是不是一个独立结构体先给一个明确的结论在主流的内核版本里kmalloc_cache并不是一个全新的、独立定义的结构体类型。你在include/linux/slab.h里搜struct kmalloc_cache是搜不到的。它实际上是struct kmem_cache的一个“逻辑子集”或者说“使用约定”。那为什么大家嘴里都在说kmalloc_cache因为内核代码里确实有kmalloc_caches这个全局数组而且kmalloc的实现路径中会频繁出现kmalloc_caches[type][index]这样的写法。久而久之大家就把“用于 kmalloc 的那一批 kmem_cache 实例”统称为 kmalloc_cache。/* include/linux/slab.h 中的关键声明 */ extern struct kmem_cache * kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH 1];这行声明信息量很大。它是一个二维数组第一维是NR_KMALLOC_TYPES第二维是KMALLOC_SHIFT_HIGH 1。换句话说内核为每一种 kmalloc 类型都准备了一组按 2 的幂次递增的缓存实例。2.2 NR_KMALLOC_TYPES 到底有几种类型NR_KMALLOC_TYPES的取值直接决定了这个二维数组第一维的大小。在较新的内核中它通常定义为 4 或者 3取决于是否开启了CONFIG_ZONE_DMA和CONFIG_MEMCG_KMEM等配置。enum kmalloc_cache_type { KMALLOC_NORMAL 0, #ifndef CONFIG_ZONE_DMA KMALLOC_DMA KMALLOC_NORMAL, #endif #ifdef CONFIG_MEMCG_KMEM KMALLOC_CGROUP, #else KMALLOC_CGROUP KMALLOC_NORMAL, #endif KMALLOC_RECLAIM, #ifdef CONFIG_ZONE_DMA KMALLOC_DMA, #endif NR_KMALLOC_TYPES };这段枚举定义非常值得逐行读。它用条件编译把不同类型的缓存“折叠”到一起目的是在不需要某些类型时不浪费数组空间同时保证索引逻辑统一。KMALLOC_NORMAL最常用的普通内存分配对应ZONE_NORMAL绝大多数kmalloc调用走的就是这条路径。KMALLOC_DMA给需要 DMA 一致性的设备用的保证分配出来的物理内存在 DMA 可寻址范围内。如果没有配置CONFIG_ZONE_DMA它就等于KMALLOC_NORMAL。KMALLOC_CGROUP用于 memory cgroup 的 kmem 计费场景。没开CONFIG_MEMCG_KMEM时同样折叠为KMALLOC_NORMAL。KMALLOC_RECLAIM这是后来加入的类型专门用于可回收内存的分配常见于文件系统元数据等可以承受回收压力的场景。注意不同内核版本这个枚举的顺序和成员可能有差异读代码时一定要以你手头那份源码为准不要凭记忆套。2.3 第二维 KMALLOC_SHIFT_HIGH 的含义第二维的大小是KMALLOC_SHIFT_HIGH 1这个KMALLOC_SHIFT_HIGH决定了 kmalloc 能支持的最大分配尺寸。#ifdef CONFIG_SLUB #define KMALLOC_SHIFT_HIGH ((MAX_ORDER PAGE_SHIFT - 1) 25 ? \ (MAX_ORDER PAGE_SHIFT - 1) : 25) #else #define KMALLOC_SHIFT_HIGH (PAGE_SHIFT 1) #endif在 SLUB 下这个值通常等于MAX_ORDER PAGE_SHIFT - 1但被限制不超过 25。假设MAX_ORDER是 11PAGE_SHIFT是 12那么11 12 - 1 22所以KMALLOC_SHIFT_HIGH就是 22对应最大 4MB 的分配。如果某些架构MAX_ORDER更大算出来超过 25就截断到 25也就是 32MB。数组第二维有KMALLOC_SHIFT_HIGH 1个元素索引从 0 到KMALLOC_SHIFT_HIGH。索引i对应的缓存负责分配大小在2^(i-1)1到2^i之间的请求。索引 0 比较特殊它对应的是 96 字节以下的小分配具体后面会讲。3. kmalloc_caches 数组的初始化全过程3.1 create_kmalloc_caches 的调用时机kmalloc_caches这个数组不是静态初始化的它需要在 SLUB 子系统起来之后通过create_kmalloc_caches函数动态创建。这个函数的调用点通常在kmem_cache_init里而kmem_cache_init又是在内核启动的start_kernel-mm_init-kmem_cache_init这条路径上被调用的。void __init create_kmalloc_caches(slab_flags_t flags) { int i; enum kmalloc_cache_type type; for (type KMALLOC_NORMAL; type NR_KMALLOC_TYPES; type) { for (i KMALLOC_SHIFT_LOW; i KMALLOC_SHIFT_HIGH; i) { if (!kmalloc_caches[type][i]) new_kmalloc_cache(i, type, flags); if (KMALLOC_MIN_SIZE 32 i 6 !kmalloc_caches[type][1]) new_kmalloc_cache(1, type, flags); if (KMALLOC_MIN_SIZE 64 i 7 !kmalloc_caches[type][2]) new_kmalloc_cache(2, type, flags); } } }这段代码有几个细节特别容易看漏。第一外层循环遍历所有 kmalloc 类型内层循环从KMALLOC_SHIFT_LOW开始。KMALLOC_SHIFT_LOW通常是 3也就是从 8 字节开始。但注意索引 0、1、2 并不在这个循环里直接创建而是通过后面两个if条件“补建”的。第二为什么要补建索引 1 和索引 2因为KMALLOC_MIN_SIZE可能是 8、16、32 或 64取决于架构和配置。如果最小分配尺寸是 8那么索引 3 对应 8 字节就够了索引 1 和 2 本来不需要。但内核为了兼容某些特殊路径比如kmalloc传入极小 size 时的对齐要求仍然会创建 96 字节和 192 字节的缓存分别挂在索引 1 和索引 2 上。第三new_kmalloc_cache内部会调用kmem_cache_create把创建出来的kmem_cache指针存回kmalloc_caches[type][i]。同时还会设置一些标志位比如SLAB_KMALLOC用来标识这个缓存是给 kmalloc 用的。3.2 new_kmalloc_cache 的命名规则static void __init new_kmalloc_cache(int idx, enum kmalloc_cache_type type, slab_flags_t flags) { if (type KMALLOC_RECLAIM) { flags | SLAB_RECLAIM_ACCOUNT; } kmalloc_caches[type][idx] create_kmalloc_cache( kmalloc_info[idx].name[type], kmalloc_info[idx].size, flags, 0, kmalloc_info[idx].size); }这里引出了另一个关键数据结构kmalloc_info。它是一个数组每个元素包含一个名字和大小。名字的格式类似kmalloc-8、kmalloc-16、kmalloc-32……一直到kmalloc-8k、kmalloc-4M等。你在/proc/slabinfo里看到的那些kmalloc-*条目就是从这里来的。kmalloc_info的定义大致如下const struct kmalloc_info_struct kmalloc_info[] __initconst { {NULL, 0}, {kmalloc-96, 96}, {kmalloc-192, 192}, {kmalloc-8, 8}, {kmalloc-16, 16}, {kmalloc-32, 32}, {kmalloc-64, 64}, {kmalloc-128, 128}, {kmalloc-256, 256}, {kmalloc-512, 512}, {kmalloc-1k, 1024}, {kmalloc-2k, 2048}, {kmalloc-4k, 4096}, {kmalloc-8k, 8192}, {kmalloc-16k, 16384}, {kmalloc-32k, 32768}, {kmalloc-64k, 65536}, {kmalloc-128k, 131072}, {kmalloc-256k, 262144}, {kmalloc-512k, 524288}, {kmalloc-1M, 1048576}, {kmalloc-2M, 2097152}, {kmalloc-4M, 4194304}, {kmalloc-8M, 8388608}, {kmalloc-16M, 16777216}, {kmalloc-32M, 33554432}, };注意索引 0 的名字是 NULL、大小是 0它是个占位符实际不会被使用。索引 1 是 96 字节索引 2 是 192 字节从索引 3 开始才是 8、16、32 这样的 2 的幂次。这个“错位”设计是 kmalloc 分级的一个经典细节很多人第一次看都会觉得别扭。3.3 为什么 96 和 192 这两个“非幂次”尺寸存在这个问题我被问过不止一次。核心原因在于如果只有 2 的幂次缓存那么分配 65 字节的请求会落到 128 字节的缓存里内部碎片率高达 49%。而实际内核中大量结构体的大小集中在 64 到 256 之间比如很多struct file、struct socket相关的辅助结构。加入 96 和 192 两个档位后65 字节的请求可以落到 96 字节缓存碎片率降到 32% 左右129 字节的请求落到 192 字节也比落到 256 字节划算。这个设计是经验值驱动的不是拍脑袋定的。内核社区曾经统计过实际分配尺寸的分布发现 96 和 192 这两个档位能显著降低整体内存浪费。所以你在看kmalloc_info时不要觉得这两个数字“破坏规律”它们恰恰是规律之外的优化。4. kmalloc 分配路径中如何选中正确的缓存4.1 kmalloc_index 的映射逻辑当你调用kmalloc(size, flags)时内核首先要做的就是根据size算出应该用哪个索引。这个工作由kmalloc_index完成。static __always_inline unsigned int kmalloc_index(size_t size) { if (!size) return 0; if (size KMALLOC_MIN_SIZE) return KMALLOC_SHIFT_LOW; if (KMALLOC_MIN_SIZE 32 size 64 size 96) return 1; if (KMALLOC_MIN_SIZE 64 size 128 size 192) return 2; if (size 8) return 3; if (size 16) return 4; if (size 32) return 5; if (size 64) return 6; if (size 128) return 7; if (size 256) return 8; if (size 512) return 9; if (size 1024) return 10; if (size 2048) return 11; if (size 4096) return 12; if (size 8192) return 13; if (size 16384) return 14; if (size 32768) return 15; if (size 65536) return 16; if (size 131072) return 17; if (size 262144) return 18; if (size 524288) return 19; if (size 1048576) return 20; if (size 2097152) return 21; if (size 4194304) return 22; if (size 8388608) return 23; if (size 16777216) return 24; if (size 33554432) return 25; return -1; }这段代码是kmalloc性能的关键路径之一。它被声明为__always_inline而且全部是简单的比较和返回编译器优化后会变成一串条件跳转几乎没有函数调用开销。注意几个边界size 0返回 0但索引 0 对应的缓存是 NULL所以实际kmalloc(0)会走特殊路径通常返回ZERO_SIZE_PTR。size KMALLOC_MIN_SIZE直接返回KMALLOC_SHIFT_LOW也就是最小缓存的索引。96 和 192 的判断有条件编译保护只有当KMALLOC_MIN_SIZE足够小的时候才生效。4.2 kmalloc_type 如何决定第一维选完索引还不够还要决定用哪种类型。这个由kmalloc_type完成。static __always_inline enum kmalloc_cache_type kmalloc_type(gfp_t flags, unsigned long caller) { int is_dma 0; int type; if (IS_ENABLED(CONFIG_ZONE_DMA) (flags GFP_DMA)) is_dma 1; if (is_dma) return KMALLOC_DMA; if (IS_ENABLED(CONFIG_MEMCG_KMEM) (flags __GFP_ACCOUNT)) return KMALLOC_CGROUP; if (flags __GFP_RECLAIMABLE) return KMALLOC_RECLAIM; return KMALLOC_NORMAL; }逻辑很直白先看是不是 DMA 请求再看是不是要计费到 cgroup再看是不是可回收最后兜底到 NORMAL。这个顺序不能乱因为一个请求可能同时带多个标志优先级高的类型要先匹配。实操心得如果你在调试时发现某个kmalloc调用落到了意料之外的缓存第一件事就是打印flags然后对照这个函数逐条排查。我遇到过__GFP_RECLAIMABLE被意外设置导致缓存选错的情况最后追到一个驱动里手动加了不该加的标志。4.3 slab_alloc 的最终调用链选好类型和索引后kmalloc最终会调用slab_alloc传入kmalloc_caches[type][index]作为缓存指针。static __always_inline void *kmalloc(size_t size, gfp_t flags) { if (__builtin_constant_p(size)) { unsigned int index kmalloc_index(size); if (!index) return ZERO_SIZE_PTR; return kmalloc_trace(kmalloc_caches[kmalloc_type(flags, _RET_IP_)][index], flags, size); } return __kmalloc(size, flags); }当size是编译期常量时kmalloc_index和kmalloc_type都能在编译期算出来整个kmalloc就退化成一次数组索引加一次slab_alloc调用效率极高。这也是为什么内核代码里大量使用kmalloc(sizeof(struct xxx), GFP_KERNEL)这种写法——编译器能帮你把索引算好。如果size是运行时变量就走__kmalloc里面会再算一次索引和类型多几次比较但整体仍然是 O(1) 的。5. 不同类型 kmalloc_cache 的适用场景与选择建议5.1 KMALLOC_NORMAL 的绝对主导地位日常驱动开发中你写的kmalloc(size, GFP_KERNEL)几乎全部落在KMALLOC_NORMAL上。它对应ZONE_NORMAL在 64 位系统上就是普通可寻址内存没有特殊限制。这个类型的缓存数量最多使用最频繁也是/proc/slabinfo里kmalloc-*条目中 active 数最高的那一批。做内存占用分析时KMALLOC_NORMAL的统计值基本能代表整个 kmalloc 子系统的整体情况。5.2 KMALLOC_DMA 的使用边界KMALLOC_DMA只在CONFIG_ZONE_DMA开启时才是独立类型。它服务于那些需要物理地址在特定范围内的设备比如某些老式 ISA 设备或者特定 SoC 上的外设。void *buf kmalloc(size, GFP_KERNEL | GFP_DMA);这行代码会强制走KMALLOC_DMA缓存。需要注意的是DMA 区域通常比 NORMAL 区域小得多滥用GFP_DMA会导致 DMA 区域快速耗尽进而影响真正需要它的设备。注意在支持ZONE_DMA32的 64 位系统上很多设备其实用GFP_DMA32就够了不需要GFP_DMA。GFP_DMA32不会切换到KMALLOC_DMA类型仍然走 NORMAL只是分配器会在ZONE_DMA32里取页。这个区别很容易搞混。5.3 KMALLOC_CGROUP 与内存计费开启CONFIG_MEMCG_KMEM后带__GFP_ACCOUNT标志的分配会落到KMALLOC_CGROUP。这个类型的缓存存在的意义是当 cgroup 内存回收时可以精确知道哪些 slab 对象属于哪个 cgroup从而做针对性的回收。void *buf kmalloc(size, GFP_KERNEL | __GFP_ACCOUNT);如果你在写一个需要被 cgroup 统计的子系统比如网络协议栈里的某些缓冲区就应该带上这个标志。否则这些内存不会被计入 cgroup 的 kmem 用量可能导致容器内存超限却查不到原因。5.4 KMALLOC_RECLAIM 的回收语义KMALLOC_RECLAIM是相对较新的类型它对应SLAB_RECLAIM_ACCOUNT标志。带__GFP_RECLAIMABLE的分配会落到这里。这个类型的典型用户是文件系统的元数据缓存比如某些 inode 相关的辅助结构。这些内存可以在系统内存紧张时被回收回收后重新从磁盘或上层缓存构建。把它们单独归类可以让回收器快速定位可回收的 slab而不必扫描全部缓存。void *buf kmalloc(size, GFP_KERNEL | __GFP_RECLAIMABLE);实操心得不要为了“省内存”而随意给分配加__GFP_RECLAIMABLE。只有当你确认这块内存可以安全丢弃并重建时才能加否则回收后出现悬空指针排查起来非常痛苦。我见过一个案例某模块给一个生命周期很长的结构体加了可回收标志结果在内存压力下被回收后续访问直接 crash。6. 调试与观测如何查看 kmalloc_cache 的实际状态6.1 /proc/slabinfo 的解读/proc/slabinfo是最直接的观测入口。你需要 root 权限才能看到完整内容。sudo cat /proc/slabinfo | grep kmalloc输出大致如下kmalloc-8k 120 120 8192 4 8 : tunables ... kmalloc-4k 2048 2560 4096 8 8 : tunables ... kmalloc-2k 1024 1280 2048 8 4 : tunables ... kmalloc-1k 3072 3584 1024 8 4 : tunables ... kmalloc-512 4096 4608 512 8 4 : tunables ... kmalloc-256 8192 9216 256 8 4 : tunables ... kmalloc-192 1024 1152 192 16 4 : tunables ... kmalloc-128 2048 2304 128 16 4 : tunables ... kmalloc-96 1536 1728 96 16 4 : tunables ... kmalloc-64 16384 18432 64 32 4 : tunables ... kmalloc-32 32768 36864 32 64 4 : tunables ... kmalloc-16 65536 73728 16 128 4 : tunables ... kmalloc-8 131072 147456 8 256 4 : tunables ...各列含义列名含义name缓存名称对应 kmalloc_info 里的名字active_objs当前活跃对象数num_objs总对象数objsize每个对象大小objperslab每个 slab 里的对象数pagesperslab每个 slab 占用的页数通过对比active_objs和num_objs你能看出缓存的利用率。如果某个缓存的num_objs远大于active_objs说明有空闲 slab 可以回收。6.2 用 slabinfo 工具做趋势分析slabinfo工具来自linux-tools或slabinfo包可以按指定间隔采样输出趋势数据。sudo slabinfo -X kmalloc-64 -N 5 -I 1000这条命令会每秒采样一次kmalloc-64共采 5 次输出 active 对象数的变化。做内存泄漏排查时这个工具比单次看/proc/slabinfo有用得多——如果某个缓存的 active 数持续上涨且不回落基本可以锁定泄漏方向。6.3 tracepoint 观测 kmalloc 调用内核提供了kmalloctracepoint可以精确追踪每一次分配。sudo perf trace -e kmalloc --filtersize 1024 sleep 10或者用 ftracecd /sys/kernel/debug/tracing echo 1 events/kmem/kmalloc/enable echo 1 tracing_on sleep 5 echo 0 tracing_on cat trace | head -50tracepoint 输出会包含调用地址、请求大小、返回指针等信息。结合kmalloc_index的逻辑你可以验证某个 size 是否落到了预期的缓存。注意在高频分配场景下开启 kmalloc tracepoint 会产生大量数据可能影响系统性能。建议只在问题复现窗口内短时间开启并配合 filter 缩小范围。7. 常见问题与排查技巧实录7.1 为什么 kmalloc 返回的地址不是按请求大小对齐的这是新手最容易困惑的点之一。你请求 100 字节拿到的地址可能只按 8 字节或 16 字节对齐而不是 128 字节。原因在于kmalloc 只保证返回的内存块大小足够不保证按请求大小对齐。实际对齐取决于缓存的align字段通常由架构的ARCH_KMALLOC_MINALIGN决定。如果你需要特定对齐比如 64 字节对齐用于 DMA 描述符应该用kmem_cache_create创建带SLAB_HWCACHE_ALIGN的专用缓存或者用dma_alloc_coherent这类接口。7.2 kmalloc 大块内存失败但系统还有空闲内存当请求大小超过KMALLOC_MAX_CACHE_SIZE通常是 8KB 或 4MB取决于配置时kmalloc会退化为kmalloc_large直接走页分配器。这时候失败原因就不是 slab 缓存的问题而是伙伴系统找不到连续物理页。排查思路确认请求大小是否超过了KMALLOC_MAX_CACHE_SIZE。查看/proc/buddyinfo确认对应 order 的空闲页块是否充足。检查是否有内存碎片问题必要时触发内存规整。cat /proc/buddyinfo如果高 order 的计数长期为 0说明碎片严重大块分配容易失败。7.3 某个 kmalloc 缓存 active 数只涨不跌这是典型的内存泄漏信号。排查步骤用slabinfo工具持续采样确认是哪个缓存泄漏。开启对应缓存的 tracepoint抓取分配和释放的配对情况。如果 tracepoint 数据量太大可以用kmemleak做粗粒度扫描。echo scan /sys/kernel/debug/kmemleak cat /sys/kernel/debug/kmemleakkmemleak会输出疑似泄漏的分配栈能直接定位到代码位置。不过它本身有性能开销生产环境慎用。7.4 常见问题速查表现象可能原因排查手段kmalloc 返回 NULL内存不足或 size 超限查 buddyinfo、确认 size 上限缓存 active 数持续上涨内存泄漏slabinfo 趋势 kmemleak分配落到意外缓存flags 设置错误打印 flags 对照 kmalloc_typeDMA 分配失败DMA 区域耗尽查 /proc/zoneinfo 的 DMA 区域回收后访问 crash误加 __GFP_RECLAIMABLE检查分配标志和对象生命周期性能下降缓存碎片或锁竞争perf 采样 slabinfo 利用率7.5 一个真实的排查案例之前遇到过一个场景某模块在压力测试下kmalloc-256的 active 数每小时涨几千但模块本身看起来释放逻辑没问题。后来用 tracepoint 抓了一段时间发现有一批分配来自一个错误处理路径那条路径在特定条件下会提前返回跳过了释放。这种问题光看代码很难发现因为正常路径和错误路径的释放逻辑是分开写的容易漏。修复方式是在错误路径上补上kfree同时把分配和释放改成 goto 链式结构保证任何分支都不会漏。这个经验告诉我排查 kmalloc 泄漏时不要只盯着正常流程错误处理和提前返回的分支往往是重灾区。8. 从 kmalloc_cache 延伸出的性能调优思路8.1 减少跨缓存分配每次kmalloc都要经过kmalloc_index和kmalloc_type两次判断虽然都是 O(1)但在超高频路径上仍有优化空间。一个常见做法是在模块初始化时预先创建专用kmem_cache把热路径上的分配固定到那个缓存上绕过 kmalloc 的分级逻辑。static struct kmem_cache *my_cache; static int __init my_init(void) { my_cache kmem_cache_create(my_struct, sizeof(struct my_struct), 0, SLAB_HWCACHE_ALIGN, NULL); if (!my_cache) return -ENOMEM; return 0; } static void *my_alloc(gfp_t flags) { return kmem_cache_alloc(my_cache, flags); }这样做的另一个好处是/proc/slabinfo里会单独出现my_struct条目观测和统计都更清晰。8.2 合理利用 KMALLOC_RECLAIM 降低内存压力对于确实可回收的缓存数据带上__GFP_RECLAIMABLE能让回收器在内存紧张时优先处理这些 slab。但前提是你必须确保回收后能正确重建且没有外部指针引用。一个判断标准如果这块内存丢了之后系统能通过重新读取磁盘或重新计算得到那它就可以标记为可回收。反之如果它是某个活跃对象的唯一状态就不能标记。8.3 关注 KMALLOC_MIN_SIZE 对碎片的影响KMALLOC_MIN_SIZE在不同架构上可能是 8、16、32 或 64。这个值决定了最小缓存的粒度。如果你的模块大量分配小于这个值的内存实际都会落到最小缓存上造成内部碎片。比如在KMALLOC_MIN_SIZE为 64 的架构上你频繁kmalloc(8)每次都会占用 64 字节碎片率 87.5%。这种情况下要么合并小分配要么用专用缓存。8.4 监控 kmalloc-* 缓存的整体健康度在生产环境建议把/proc/slabinfo里kmalloc-*的 active 数和总对象数纳入监控。设置合理的告警阈值比如某个缓存的 active 数在 10 分钟内增长超过 50% 且不回落就触发告警。这样能在内存问题演变成 OOM 之前提前介入。我个人在实际操作中的体会是kmalloc_cache 这套机制看起来只是“按大小分级”这么简单但真正把它吃透之后你会发现内核在碎片控制、类型隔离、回收策略上的很多设计都是环环相扣的。尤其是 96 和 192 这两个非幂次档位的存在以及 KMALLOC_RECLAIM 类型的引入都是实际运行数据倒逼出来的优化。读这部分代码时不要只记结论多想想“如果不这样设计会怎样”理解会深很多。