AI智能摘要
以实际dump为例,文章用trace32深入解析了Linux slab cache的内部逻辑,带你逐层解读kmalloc-64实例中的数据结构,从kmalloc_caches和kmem_cache到per-CPU区与节点node的内存组织和管理特性。
此摘要由AI分析文章内容生成,仅供参考。

我们之前一直在看各种的理论性的介绍,尽管网络上的文章写的都很好!但是总觉得缺少对slab的理想构图!所以我萌生出了从trace32去理解slab cache的方法,希望对大家理解slab内存的内部逻辑有一定的帮助

本文以我手里的一份dump来分析一下kmalloc-64作为案例

kmalloc_caches

v.v kmalloc_caches
kmalloc_caches = (
    (0x0, 0x0, 0xFFFFFF800B21E400, 0x0, 0x0, 0x0, 0xFFFFFF800B21E200, 0xFFFFFF800B21E300, 0xFFFFFF80
    (0x0, 0x0, 0xFFFFFF800B21E400, 0x0, 0x0, 0x0, 0xFFFFFF800B21E200, 0xFFFFFF800B21E300, 0xFFFFFF80
    (0x0, 0x0, 0xFFFFFF800B21ED00, 0x0, 0x0, 0x0, 0xFFFFFF800B21EB00, 0xFFFFFF800B21EC00, 0xFFFFFF80

  • 第一维:[0][1]……(不同 KMALLOC_TYPES,一般 0 是 normal)

  • 第二维:[0]..[N],每个元素是 struct kmem_cache *

所以点开第一个找到name为kmalloc-64的那一个

  kmalloc_caches = (
    (
      0x0,
      0x0,
      0xFFFFFF800B21E400,
      0x0,
      0x0,
      0x0,
      0xFFFFFF800B21E200 -> (
        cpu_slab = 0xFFFFFFC009FB2D30,
        flags = 1073745920,
        min_partial = 5,
        size = 64,
        object_size = 64,
        reciprocal_size = (m = 1, sh1 = 1, sh2 = 5),
        offset = 32,
        cpu_partial = 120,
        cpu_partial_slabs = 4,
        oo = (x = 64),
        min = (x = 64),
        allocflags = 0,
        refcount = -1,
        ctor = 0x0,
        inuse = 64,
        align = 64,
        red_left_pad = 0,
        name = 0xFFFFFFC00956482B -> "kmalloc-64",
        list = (next = 0xFFFFFF800B21E168, prev = 0xFFFFFF800B21E368),
        kobj = (name = 0xFFFFFFC00956482B -> "kmalloc-64", entry = (next = 0xFFFFFF800B21E180, prev
        random = 1024558225630158183,
        random_seq = 0xFFFFFF8045752100,
        kasan_info = (is_kmalloc = FALSE),
        useroffset = 0,
        usersize = 64,
        node = (0xFFFFFF800B21C080)),
      0xFFFFFF800B21E300,

所以kmalloc-64对应的地址是0xFFFFFF800B21E200,结构体类型为:struct kmem_cache

kmem_cache

  (struct kmem_cache *) kmalloc_caches[0][6] = 0xFFFFFF800B21E200 -> (
    (struct kmem_cache_cpu *) cpu_slab = 0xFFFFFFC009FB2D30,
    (slab_flags_t) flags = 1073745920,
    (unsigned long) min_partial = 5,
    (unsigned int) size = 64,
    (unsigned int) object_size = 64,
    (struct reciprocal_value) reciprocal_size = ((u32) m = 1, (u8) sh1 = 1, (u8) sh2 = 5),
    (unsigned int) offset = 32,
    (unsigned int) cpu_partial = 120,
    (unsigned int) cpu_partial_slabs = 4,
    (struct kmem_cache_order_objects) oo = ((unsigned int) x = 64),
    (struct kmem_cache_order_objects) min = ((unsigned int) x = 64),
    (gfp_t) allocflags = 0,
    (int) refcount = -1,
    (void (*)()) ctor = 0x0,
    (unsigned int) inuse = 64,
    (unsigned int) align = 64,
    (unsigned int) red_left_pad = 0,
    (const char *) name = 0xFFFFFFC00956482B -> "kmalloc-64",
    (struct list_head) list = ((struct list_head *) next = 0xFFFFFF800B21E168, (struct list_head *) prev = 0xFFFFFF800B21E368),
    (struct kobject) kobj = ((const char *) name = 0xFFFFFFC00956482B -> "kmalloc-64", (struct list_head) entry = ((struct list_head *) next = 0xFFFFFF800B21E180, (struct list_head *) prev =
    (unsigned long) random = 1024558225630158183,
    (unsigned int *) random_seq = 0xFFFFFF8045752100,
    (struct kasan_cache) kasan_info = ((bool) is_kmalloc = FALSE),
    (unsigned int) useroffset = 0,
    (unsigned int) usersize = 64,
    (struct kmem_cache_node * [1]) node = ([0] = 0xFFFFFF800B21C080))

cpu_slab

  (struct kmem_cache *) kmalloc_caches[0][6] = 0xFFFFFF800B21E200 -> (
    (struct kmem_cache_cpu *) cpu_slab = 0xFFFFFFC009FB2D30 -> (
      (void * *) freelist = ERROR:SPACEIDINVALID,
      (unsigned long) tid = ERROR:SPACEIDINVALID,
      (struct slab *) slab = ERROR:SPACEIDINVALID,
      (struct slab *) partial = ERROR:SPACEIDINVALID,
      (local_lock_t) lock = ()),

cpu_slab 里全是 ERROR:SPACEIDINVALID,是因为 读不到那块内存(per-cpu 区域),而不是结构体坏掉

  • cpu_slab 指向的是 per-CPU 内存alloc_percpu 出来的)

  • 每个 CPU 有一个自己的 struct kmem_cache_cpu

  • 你看到的地址 0xFFFFFFC009FB2D30 就是在 per-CPU 段 里的地址

而 TRACE32 要想读 per-CPU 区域,必须有正确的 MMU/地址空间配置或者在 dump 里包含那块内存。如果没有,就会出现类似:

  • ERROR:READERROR

  • ERROR:SPACEIDINVALID

node

    (struct kmem_cache_node * [1]) node = (
      [0] = 0xFFFFFF800B21C080 -> (
        (spinlock_t) list_lock = ((struct raw_spinlock) rlock = ((arch_spinlock_t) raw_lock = ((atomic_t) val = ((int) counter = 0), (u8) locked = 0, (u8) pending = 0, (u16) locked_pending =
        (unsigned long) nr_partial = 0,
        (struct list_head) partial = (
          (struct list_head *) next = 0xFFFFFF800B21C090,
          (struct list_head *) prev = 0xFFFFFF800B21C090),
        (atomic_long_t) nr_slabs = ((s64) counter = 3073),
        (atomic_long_t) total_objects = ((s64) counter = 196672),
        (struct list_head) full = (
          (struct list_head *) next = 0xFFFFFF800B21C0B0,
          (struct list_head *) prev = 0xFFFFFF800B21C0B0))))

1. 这段输出在说什么?

  • nr_partial = 0

  • partial.next = partial.prev = 0xFFFFFF800B21C090

说明:partial 这个链表是自环,也就是 空链表

同理:

  • full.next = full.prev = 0xFFFFFF800B21C0B0

full 也是自环 → 也是空链表

但同时:

  • nr_slabs = 3073

  • total_objects = 196672

说明:这个 node 上是有很多 slab 的,只不过它们没有挂在 partial / full 这两个链表上。

2. 为什么 full 链表是空的,但 nr_slabs 却是 3073?

这是 SLUB 的一个“坑点”:

  • kmem_cache_node.full 这个链表只在启用 SLUB debug(slub_debug 或相关配置)时才真正用来挂 full slabs

  • 普通配置下,full 基本是个“摆设”,不维护 slab 列表,所以你看到的就是自环。

换句话说:
在你现在的配置里,不能指望通过 node->full 找到所有 full slabs
nr_slabs / total_objects 这些计数器会更新,但 slab 实体不一定挂在 full 上。

加上前面看到的:

  • nr_partial = 0partial

  • full 也空

→ 结合 cpu_slab 又看不到(per-CPU 区域读不到),极大概率是所有 slab 都在各 CPU 的 kmem_cache_cpu.slab / partial 链里,没挂回 node 的 partial/full

然后我打开了slub_debug=FPZU后重新抓了一份dump

  (*((kmalloc_caches[0][6]))).node[0] = 0xFFFFFF800B21C240 -> (
    list_lock = (rlock = (raw_lock = (val = (counter = 0), locked = 0, pending = 0, locked_pending =
    nr_partial = 1693,
    partial = (
      next = 0xFFFFFFFE036A7188 -> (
        next = 0xFFFFFFFE073C9F08 -> (
          next = 0xFFFFFFFE02AE9508 -> (
            next = 0xFFFFFFFE00EBA788 -> (
              next = 0xFFFFFFFE03296708,
              prev = 0xFFFFFFFE02AE9508),
            prev = 0xFFFFFFFE073C9F08),
          prev = 0xFFFFFFFE036A7188),
        prev = 0xFFFFFF800B21C250),
      prev = 0xFFFFFFFE06877788),
    nr_slabs = (counter = 8094),
    total_objects = (counter = 258992),
    full = (
      next = 0xFFFFFFFE00E44008 -> (
        next = 0xFFFFFFFE00B50B88 -> (
          next = 0xFFFFFFFE02D69088,
          prev = 0xFFFFFFFE00E44008),
        prev = 0xFFFFFF800B21C270),
      prev = 0xFFFFFFFE002C8788))
  1. nr_partial = 1693partial 链表确实挂了很多 slab

  2. full.nextfull.prev 也不再自环,说明 full slab 也在链表里了

而挂载partial和full上面的next 和prev指针并不是slab的首地址,而是:

挂在链表上的 struct list_head 成员的地址
这个 list_head 嵌在 struct page 里(一般叫 slab_list 或有的版本直接用 lru)。

也就是说:

  • partial.next = 0xFFFFFFFE036A7188

代表的是:

有一个 struct page,它里面的某个 struct list_head 成员(比如 page->slab_list)的地址就是 0xFFFFFFFE036A7188

  • 我们只要知道这个成员在 struct page 里的偏移,就能反推回 struct page * 的地址

即:

page = (void *)list_head_addr - offsetof(struct page, slab_list)

page

查询list_head在page里的偏移

struct page {
    unsigned long flags;        /* Atomic flags, some possibly
                     * updated asynchronously */
    /*
     * Five words (20/40 bytes) are available in this union.
     * WARNING: bit 0 of the first word is used for PageTail(). That
     * means the other users of this union MUST NOT use the bit to
     * avoid collision and false-positive PageTail().
     */
    union {
        struct {    /* Page cache and anonymous pages */
            /**
             * @lru: Pageout list, eg. active_list protected by
             * lruvec->lru_lock.  Sometimes used as a generic list
             * by the page owner.
             */
            union {
                struct list_head lru;

                /* Or, for the Unevictable "LRU list" slot */
                struct {
                    /* Always even, to negate PageTail */
                    void *__filler;
                    /* Count page's or folio's mlocks */
                    unsigned int mlock_count;
                };

                /* Or, free page */
                struct list_head buddy_list;
                struct list_head pcp_list;
            };
            /* See page-flags.h for PAGE_MAPPING_FLAGS */
            struct address_space *mapping;
            pgoff_t index;        /* Our offset within mapping. */
            /**
             * @private: Mapping-private opaque data.
             * Usually used for buffer_heads if PagePrivate.
             * Used for swp_entry_t if PageSwapCache.
             * Indicates order in the buddy system if PageBuddy.
             */
            unsigned long private;
        };

lru 的偏移是0x8,所以当前partial->next =0xFFFFFFFE036A7188 对应的page的地址为0xFFFFFFFE036A7180

这里的链表顺序也和partial链表对的上

当 page 用作 slab 页时,struct page 里很多字段是通过 union 被 SLUB 重用的,根本不是“普通匿名页”的含义。

也就是说:

  • 对 slab 页来说:

    • mappingindexprivatepp__filler 这些字段

    • 实际上被当成 slab 的元数据(slab_cache 指针、freelist、对象计数等)来用

所以这些字段用“通用 page 视角”看起来乱,是正常的,并不是算错了

我们只需要用slab类型来解析就可以得到真正的slab结构体

  (struct slab *)0xFFFFFFFE036A7180 = 0xFFFFFFFE036A7180 -> (
    __page_flags = 4611686018427453952,
    slab_list = (
      next = 0xFFFFFFFE073C9F08 -> (
        next = 0xFFFFFFFE02AE9508 -> (
          next = 0xFFFFFFFE00EBA788,
          prev = 0xFFFFFFFE073C9F08),
        prev = 0xFFFFFFFE036A7188),
      prev = 0xFFFFFF800B21C250),
    callback_head = (next = 0xFFFFFFFE073C9F08, func = 0xFFFFFF800B21C250),
    next = 0xFFFFFFFE073C9F08,
    slabs = 186761808,
    slab_cache = 0xFFFFFF8045750700,
    freelist = 0xFFFFFF80DA9C6D40 -> ,
    counters = 2097175,
    inuse = 23,
    objects = 32,
    frozen = 0,
    __unused = 4294967295,
    __page_refcount = (counter = 1),
    memcg_data = 0)

这块 slab 页是什么状态?

关键字段:

  • slab_cache = 0xFFFFFF8045750700

👉 这是 kmalloc-64 的一个 slab 页。

  • objects = 32

👉 这个 slab 一共管理 32 个对象(64B object + 对齐 + metadata 后的结果),

不是单纯 “4K / 64 = 64”,SLUB 为自己的元数据留了空间。

  • inuse = 23

👉 当前有 23 个对象正在被分配出去,

剩余 free = 32 - 23 = 9 个空闲对象

  • frozen = 0

👉 这块 slab 没有被冻结在某个 CPU 的 kmem_cache_cpu

也就是正常挂在 kmem_cache_node.partial 的 partial 链里——

和你刚刚看到的 node[0].partial 是一致的。

  • slab_list

👉 这就是在 node 里看的那个链表节点:

  • prev = 0xFFFFFF800B21C250:就是 node->partial 的头结点

  • next = 0xFFFFFFFE073C9F08:指向 partial 链上的下一个 slab

也就是说:

已经成功从 node->partial 链表,走到了一个具体的 kmalloc-64 的 partial slab 页,而且看到了它有 32 个 object,其中 23 个在用。

freelist

我们要了解到的是freelist是指向的当前slab页中的第一个空闲slab object,所以我们来看

freelist = 0xFFFFFF80DA9C6D40 这个对应的内存是怎样的

正如我们设置的slub_debug=FPZU

区域

模式

含义

0x6B (ASCII 'k')

SLAB_POISON 用的 free object poison(0x6B)

表示这是一个 free object

0xC 重复

调试用填充,通常用于 redzone 或头部

防溢出检测

0x5A ('Z')

Redzone poison(0x5A)

用来检测越界写

这个freelist的指针指向的是slab object的freepointer指针的位置,

我们索要了解的是这个freepointer的位置在free时的位置是放在object部分的,所以我们现在看到的这张图0xFFFFFF80DA9C6D40处就是redzone的末尾和object的起始位置,所以前面就是填充的redzone区域,应该为0xB,查看内存推导结果也如我们所料

开启 slub_debug 且带 USLAB_STORE_USER)选项时,每个对象尾部会多出一个或两个 struct track

查看后面的FFFFFFC001386FC8,这个就是alloc track

查看后面的 FFFFFFC00138864C,这个就是free track

freelist 里的 slab object = 已经 free 的对象

里面的用户数据基本都被 0x6B/0x5A/0xFF 这些 poison 覆盖掉了,剩下的只是 redzone 和 track 等 debug 元数据。

“找一个 正在使用 的 kmalloc-64 对象,看看它的 payload + alloc/free track”

就必须换目标:

去找一个不在 freelist 里的 slot,也就是 allocated object。

allocated object

总结一下现在已经有的:

  • 对某个 slab页:

    • objects = 32

    • inuse = 23 → 有 23 个已分配、9 个 free

    • freelist 指向第一个 free 对象(你已经 dump 过,都是 0x6B poison)

  • 你知道:

    • PAGE_SIZE(4K)

    • kmem_cache->size(64)

    • kmem_cache->red_left_pad(要再看一下 kmalloc-64 的 struct)

  1. 算出这一页 slab 的起始地址(slab_base)

0xFFFFFF80DA9C6D40 & ~(0x1000-1) = 0xFFFFFF80DA9C6000
  1. 计算 kmem_cache的 size / red_left_pad

size = ((struct kmem_cache *)&0xFFFFFF80DA9C6000)->size

  kmalloc_caches[0][6] = 0xFFFFFF8045750700 -> (
    cpu_slab = 0xFFFFFFC00A0C2D30,
    flags = 2147556608,
    min_partial = 5,
    size = 256,
    object_size = 64,
    reciprocal_size = (m = 1, sh1 = 1, sh2 = 7),
    offset = 72,
    cpu_partial = 0,
    cpu_partial_slabs = 0,
    oo = (x = 65568),
    min = (x = 16),
    allocflags = 262144,
    refcount = -1,
    ctor = 0x0,
    inuse = 72,
    align = 64,
    red_left_pad = 64,
    name = 0xFFFFFFC0093624B8 -> "kmalloc-64",

对于这个 cache:

  • 每个 slot 步长size = 256 (0x100)

  • 用户真正能用的大小:object_size = 64

  • 左边红区 / padding:red_left_pad = 64 (0x40)

  • freelist 指针在 object 内部的偏移:offset = 72 (0x48)(后面遍历 freelist 时用)

所以这一块 slab 里,第 i 个 slot 的地址是:

slot[i] = slab_base + red_left_pad + i * size
        = 0xFFFFFF80DA9C6000 + 0x40 + i * 0x100

我们来验证一下前面那条 freelist 指向的 object 地址:

freelist = 0xFFFFFF80DA9C6D40

算它是第几个 slot:

  • slab_base + pad = 0xFFFFFF80DA9C6040

  • 差值:0x6D40 - 0x6040 = 0x0D00

  • 0x0D00 / 0x100 = 0x0D = 13

👉 也就是说:freelist 指向的是这一 slab 的第 13 号 slot(index = 13)

这个对吗?我们已知这个slab页一共32个,其中23个已经allocated!那我们推导出freelist指向的是地13号slot,那是不是说明我们分析的有问题?

其实不然!

SLUB 在一个 slab 里:

  • 对象物理布局:按固定步长排好(slot[i] = base + pad + i * size),这一点是确定的;

  • 对象分配/释放顺序:不保证任何顺序,只看 freelist 链表头是谁——

特别是有 SLAB_FREELIST_RANDOM 的话,free 的顺序还会被打乱。

也就是说:

  • 地址上的顺序:slot[0], slot[1], slot[2]...

  • 分配的顺序:slot[13] → slot[2] → slot[27] → ... 完全不必按地址来。

我们现在找一个已经分配的slab object分析一下

看到了熟悉的DEAD000000000122

这个正是slab的

查看slab的alloc track和free track

  (struct track *)0xFFFFFF80DA9C6290 = 0xFFFFFF80DA9C6290 -> (
    addr = 0xFFFFFFC00834EAAC,
    handle = 0x0,
    cpu = 0x0,
    pid = 0x2456,
    when = 0xFFFF7230)

  (struct track *)0xFFFFFF80DA9C62B0 = 0xFFFFFF80DA9C62B0 -> (
    addr = 0xFFFFFFC001388730,
    handle = 0x0,
    cpu = 0x0,
    pid = 0x0A,
    when = 0xFFFF7224)

Congratuations!完美符合!

总结

整篇文章主要就利用trace32来梳理slab内存的这些结构,最起码我在梳理的时候觉得以前一些不通的概念就想明白了,希望对您有所帮助!

另外,本篇文章涉及到的一些概念,请配合以下文章一起食用:

[linux内存管理] 第020篇 Linux内核slab内存的越界检查SLUB_DEBUG的原理剖析

[linux内存管理] 第030篇 深入理解 slab cache 内存分配全链路实现