关于后端:深入理解-slab-cache-内存分配全链路实现

45次阅读

共计 27757 个字符,预计需要花费 70 分钟才能阅读完成。

本文源码局部基于内核 5.4 版本探讨

在通过上篇文章《从内核源码看 slab 内存池的创立初始化流程》的介绍之后,咱们最终失去上面这幅 slab cache 的残缺架构图:

本文笔者将带大家持续从内核源码的角度持续拆解 slab cache 的实现细节,接下来笔者会基于下面这幅 slab cache 残缺架构图,具体介绍一下 slab cache 是如何进行内存调配的。

1. slab cache 如何分配内存

当咱们应用 fork() 零碎调用创立过程的时候,内核须要为过程创立 task_struct 构造,struct task_struct 是内核中的外围数据结构,当然也会有专属的 slab cache 来进行治理,task_struct 专属的 slab cache 为 task_struct_cachep。

上面笔者就以内核从 task_struct_cachep 中申请 task_struct 对象为例,为大家分析 slab cache 分配内存的整个源码实现。

内核通过定义在文件 /kernel/fork.c 中的 dup_task_struct 函数来为过程申请
task_struct 构造并初始化。

static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{
          ........... 
    struct task_struct *tsk;
    // 从 task_struct 对象专属的 slab cache 中申请 task_struct 对象
    tsk = alloc_task_struct_node(node);
          ...........   
}

// task_struct 对象专属的 slab cache
static struct kmem_cache *task_struct_cachep;

static inline struct task_struct *alloc_task_struct_node(int node)
{
    // 利用 task_struct_cachep 动态分配 task_struct 对象
    return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);
}

内核中通过 kmem_cache_alloc_node 函数要求 slab cache 从指定的 NUMA 节点中调配对象。

// 定义在文件:/mm/slub.c
void *kmem_cache_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node)
{void *ret = slab_alloc_node(s, gfpflags, node, _RET_IP_);
    return ret;
}

static __always_inline void *slab_alloc_node(struct kmem_cache *s,
        gfp_t gfpflags, int node, unsigned long addr)
{
    // 用于指向调配胜利的对象
    void *object;
    // slab cache 在以后 cpu 下的本地 cpu 缓存
    struct kmem_cache_cpu *c;
    // object 所在的内存页
    struct page *page;
    // 以后 cpu 编号
    unsigned long tid;

redo:
    // slab cache 首先尝试从以后 cpu 本地缓存 kmem_cache_cpu 中获取闲暇对象
    // 这里的 do..while 循环是要保障获取到的 cpu 本地缓存 c 是属于执行过程的以后 cpu
    // 因为过程可能因为抢占或者中断的起因被调度到其余 cpu 上执行,所需须要确保两者的 tid 是否统一
    do {
        // 获取执行以后过程的 cpu 中的 tid 字段
        tid = this_cpu_read(s->cpu_slab->tid);
        // 获取 cpu 本地缓存 cpu_slab
        c = raw_cpu_ptr(s->cpu_slab);
        // 如果开启了 CONFIG_PREEMPT 示意容许优先级更高的过程抢占以后 cpu
        // 如果产生抢占,以后过程可能被从新调度到其余 cpu 上运行,所以须要查看此时运行以后过程的 cpu tid 是否与方才获取的 cpu 本地缓存统一
        // 如果两者的 tid 字段不统一,阐明过程曾经被调度到其余 cpu 上了,须要再次获取正确的 cpu 本地缓存
    } while (IS_ENABLED(CONFIG_PREEMPT) &&
         unlikely(tid != READ_ONCE(c->tid)));

    // 从 slab cache 的 cpu 本地缓存 kmem_cache_cpu 中获取缓存的 slub 闲暇对象列表
    // 这里的 freelist 指向本地 cpu 缓存的 slub 中第一个闲暇对象
    object = c->freelist;
    // 获取本地 cpu 缓存的 slub,这里用 page 示意,如果是复合页,这里指向复合页的首页 head page
    page = c->page;
    if (unlikely(!object || !node_match(page, node))) {
        // 如果 slab cache 的 cpu 本地缓存中曾经没有闲暇对象了
        // 或者 cpu 本地缓存中的 slub 并不属于咱们指定的 NUMA 节点
        // 那么咱们就须要进入慢速门路中调配对象:
        // 1. 查看 kmem_cache_cpu 的 partial 列表中是否有闲暇的 slub
        // 2. 查看 kmem_cache_node 的 partial 列表中是否有闲暇的 slub
        // 3. 如果都没有,则只能从新到搭档零碎中去申请内存页
        object = __slab_alloc(s, gfpflags, node, addr, c);
        // 统计 slab cache 的状态信息,记录本次调配走的是慢速门路 slow path
        stat(s, ALLOC_SLOWPATH);
    } else {
        // 走到该分支示意,slab cache 的 cpu 本地缓存中还有闲暇对象,间接调配
        // 疾速门路 fast path 下调配胜利,从以后闲暇对象中获取下一个闲暇对象指针 next_object        
        void *next_object = get_freepointer_safe(s, object);
        // 更新 kmem_cache_cpu 构造中的 freelist 指向 next_object
        if (unlikely(!this_cpu_cmpxchg_double(
                s->cpu_slab->freelist, s->cpu_slab->tid,
                object, tid,
                next_object, next_tid(tid)))) {note_cmpxchg_failure("slab_alloc", s, tid);
            goto redo;
        }
        // cpu 预取 next_object 的 freepointer 到 cpu 高速缓存,放慢下一次调配对象的速度
        prefetch_freepointer(s, next_object);
        stat(s, ALLOC_FASTPATH);
    }

    // 如果 gfpflags 掩码中设置了  __GFP_ZERO,则须要将对象所占的内存初始化为零值
    if (unlikely(slab_want_init_on_alloc(gfpflags, s)) && object)
        memset(object, 0, s->object_size);
    // 返回调配好的对象
    return object;
}

2. slab cache 的疾速调配门路

正如笔者在前边文章《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》中的“7. slab 内存调配原理”大节里介绍的原理,slab cache 在最开始会进入 fastpath 调配对象,也就是说首先会从 cpu 本地缓存 kmem_cache_cpu->freelist 中获取对象。

在获取 kmem_cache_cpu 构造的时候须要保障这个 cpu 本地缓存是属于以后执行过程的 cpu。

在开启了 CONFIG_PREEMPT 的状况下,内核是容许优先级更高的过程抢占以后 cpu 的,当产生 cpu 抢占之后,过程会被内核从新调度到其余 cpu 上执行,这样一来,过程在被抢占之前获取到的 kmem_cache_cpu 就与以后执行过程 cpu 的 kmem_cache_cpu 不统一了。

内核在 slab_alloc_node 函数开始的中央通过在 do..while 循环中一直判断两者的 tid 是否统一来保障这一点。

随后内核会通过 kmem_cache_cpu->freelist 来获取 cpu 缓存 slab 中的第一个闲暇对象。

如果以后 cpu 缓存 slab 是空的(没有闲暇对象可供调配)或者该 slab 所在的 NUMA 节点并不是咱们指定的。那么就会通过 __slab_alloc 进入到慢速调配门路 slowpath 中。

如果以后 cpu 缓存 slab 有闲暇的对象并且 slab 所在的 NUMA 节点正是咱们指定的,那么将以后 kmem_cache_cpu->freelist 指向的第一个闲暇对象从 slab 中拿出,并调配进来。

随后通过 get_freepointer_safe 获取以后调配对象的 freepointer 指针(指向其下一个闲暇对象),而后将 kmem_cache_cpu->freelist 更新为 freepointer(指向的下一个闲暇对象)。

// slub 中的闲暇对象中均保留了下一个闲暇对象的指针 free_pointer
// free_pointor  在 object 中的地位由 kmem_cache 构造的 offset 指定
static inline void *get_freepointer_safe(struct kmem_cache *s, void *object)
{
    // freepointer 在 object 内存区域的起始地址
    unsigned long freepointer_addr;
    // 指向下一个闲暇对象的 free_pontier
    void *p;
    // free_pointer 位于 object 起始地址的 offset 偏移处
    freepointer_addr = (unsigned long)object + s->offset;
    // 获取 free_pointer 指向的地址(下一个闲暇对象)probe_kernel_read(&p, (void **)freepointer_addr, sizeof(p));
    // 返回下一个闲暇对象地址
    return freelist_ptr(s, p, freepointer_addr);
}

3. slab cache 的慢速调配门路

static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
              unsigned long addr, struct kmem_cache_cpu *c)
{
    void *p;
    unsigned long flags;
    // 敞开 cpu 中断,避免并发拜访
    local_irq_save(flags);
#ifdef CONFIG_PREEMPT
    // 当开启了 CONFIG_PREEMPT,示意容许其余过程抢占以后 cpu
    // 运行过程的以后 cpu 可能会被其余优先级更高的过程抢占,以后过程可能会被调度到其余 cpu 上
    // 所以这里须要从新获取 slab cache 的 cpu 本地缓存
    c = this_cpu_ptr(s->cpu_slab);
#endif
    // 进入 slab cache 的慢速调配门路
    p = ___slab_alloc(s, gfpflags, node, addr, c);
    // 复原 cpu 中断
    local_irq_restore(flags);
    return p;
}

内核为了避免 slab cache 在慢速门路下的并发平安问题,在进入 slowpath 之前会把中断敞开掉,并从新获取 cpu 本地缓存。这样做的目标是为了避免再敞开中断之前,过程被抢占,调度到其余 cpu 上。

static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
              unsigned long addr, struct kmem_cache_cpu *c)
{
    // 指向 slub 中可供调配的第一个闲暇对象
    void *freelist;
    // 闲暇对象所在的 slub(用 page 示意)struct page *page;
    // 从 slab cache 的本地 cpu 缓存中获取缓存的 slub
    page = c->page;
    if (!page)
        // 如果缓存的 slub 中的对象曾经被全副调配进来,没有闲暇对象了
        // 那么就会跳转到 new_slab 分支进行降级解决走慢速调配门路
        goto new_slab;
redo:

    // 这里须要再次查看 slab cache 本地 cpu 缓存中的 freelist 是否有闲暇对象
    // 因为以后过程可能被中断,当从新调度之后,其余过程可能曾经开释了一些对象到缓存 slab 中
    // freelist 可能此时就不为空了,所以须要再次尝试一下
    freelist = c->freelist;
    if (freelist)
        // 从 cpu 本地缓存中的 slub 中间接调配对象
        goto load_freelist;

    // 本地 cpu 缓存的 slub 用 page 构造来示意,这里是查看 page 构造的 freelist 是否还有闲暇对象
    // c->freelist 示意的是本地 cpu 缓存的闲暇对象列表,刚咱们曾经查看过了
    // 当初咱们查看的 page->freelist,它示意由其余 cpu 所开释的闲暇对象列表
    // 因为此时有可能其余 cpu 又开释了一些对象到 slub 中这时 slub 对应的  page->freelist 不为空,能够间接调配
    freelist = get_freelist(s, page);
    // 留神这里的 freelist 曾经变为 page->freelist,并不是 c->freelist;
    if (!freelist) {
        // 此时 cpu 本地缓存的 slub 里的闲暇对象曾经全副耗尽
        // slub 从 cpu 本地缓存中脱离,进入 new_slab 分支走慢速调配门路
        c->page = NULL;
        stat(s, DEACTIVATE_BYPASS);
        goto new_slab;
    }

    stat(s, ALLOC_REFILL);

load_freelist:
    // 被 slab cache 的 cpu 本地缓存的 slub 所属的 page 必须是 frozen 解冻状态,只容许本地 cpu 从中调配对象
    VM_BUG_ON(!c->page->frozen);
    // kmem_cache_cpu 中的 freelist 指向被 cpu 缓存 slub 中第一个闲暇对象
    // 因为第一个闲暇对象马上要被调配进来,所以这里须要获取下一个闲暇对象更新 freelist
    c->freelist = get_freepointer(s, freelist);
    // 更新 slab cache 的 cpu 本地缓存调配对象时的全局 transaction id
    // 每当调配完一次对象,kmem_cache_cpu 中的 tid 都须要扭转
    c->tid = next_tid(c->tid);
    // 返回第一个闲暇对象
    return freelist;

new_slab:
     ......... 进入 slowpath 调配对象 ..........

}

在 slab cache 进入慢速门路之前,内核还须要再次查看本地 cpu 缓存的 slab 的存储容量,确保其真的没有闲暇对象了。

如果本地 cpu 缓存的 slab 为空(kmem_cache_cpu->page == null),间接跳转到 new_slab 分支进入 slow path。

如果本地 cpu 缓存的 slab 不为空,那么须要再次查看 slab 中是否有闲暇对象,这么做的目标是因为以后过程可能被中断,当从新调度之后,其余过程可能曾经开释了一些对象到缓存 slab 中了,所以在进入 slowpath 之前还是有必要再次检查一下 kmem_cache_cpu->freelist。

如果碰巧,其余过程在以后过程被中断之后,曾经开释了一些对象回缓存 slab 中了,那么就间接跳转至 load_freelist 分支,走 fastpath 门路,间接从缓存 slab (kmem_cache_cpu->freelist) 中调配对象,防止进入 slowpath。

load_freelist:
    // 更新 freelist,指向下一个闲暇对象
    c->freelist = get_freepointer(s, freelist);
    // 更新 tid
    c->tid = next_tid(c->tid);
    // 返回第一个闲暇对象
    return freelist;

如果 kmem_cache_cpu->freelist 还是为空,则须要再次查看 slab 自身的 freelist 是否空,留神这里指的是 struct page 构造中的 freelist。

struct page {
           // 指向内存页中第一个闲暇对象
           void *freelist;     /* first free object */
           // 该 slab 是否在对应 slab cache 的本地 CPU 缓存中
           // frozen = 1 示意缓存再本地 cpu 缓存中
           unsigned frozen:1;
}

大家读到这里肯定会感觉十分懵,kmem_cache_cpu 构造中有一个 freelist,page 构造也有一个 freelist,懵逼的是这两个 freelist 均是指向 slab 中第一个闲暇对象,它俩之间有什么差异吗?

事实上,这一块确实比较复杂,逻辑比拟绕,所以笔者有必要具体的为大家阐明一下,以解决大家心中的困惑。

首先,在 slab cache 的整个架构体系中确实存在两个 freelist:

  • 一个是 page->freelist,因为 slab 在内核中是应用 struct page 构造来示意的,所以 page->freelist 只是单纯的站在 slab 的视角来示意 slab 中的闲暇对象列表,这里不思考 slab 在 slab cache 架构中的地位。
  • 另一个是 kmem_cache_cpu->freelist,特指 slab 被 slab cache 的本地 cpu 缓存之后,slab 中的闲暇对象链表。这里能够了解为 slab 中被 cpu 缓存的闲暇对象。当 slab 被晋升为 cpu 缓存之后,page->freeelist 间接赋值给 kmem_cache_cpu->freelist,而后 page->freeelist 置空。slab->frozen 设置为 1,示意 slab 被解冻在以后 cpu 的本地缓存中。

而 slab 一旦被以后 cpu 缓存,它的状态就变为了解冻状态(slab->frozen = 1),处于解冻状态下的 slab,以后 cpu 能够从该 slab 中调配或者开释对象,然而其余 cpu 只能开释对象到该 slab 中,不能从该 slab 中调配对象

  • 如果一个 slab 被一个 cpu 缓存之后,那么这个 cpu 在该 slab 看来就是本地 cpu,当本地 cpu 开释对象回这个 slab 的时候会开释回 kmem_cache_cpu->freelist 链表中
  • 如果其余 cpu 想要开释对象回该 slab 时,其余 cpu 只能将对象开释回该 slab 的 page->freelist 中。

什么意思呢?笔者来举一个具体的例子为大家具体阐明。

如下图所示,cpu1 在本地缓存了 slab1,cpu2 在本地缓存了 slab2,过程先从 slab1 中获取了一个对象,失常状况下如果过程始终在 cpu1 上运行的话,当过程开释该对象回 slab1 中时,会间接开释回 kmem_cache_cpu1->freelist 链表中。

但如果过程在 slab1 中获取完对象之后,被调度到了 cpu2 上运行,这时过程想要开释对象回 slab1 中时,就不能走疾速门路了,因为 cpu2 本地缓存的是 slab2,所以 cpu2 只能将对象开释至 slab1->freelist 中。

这种状况下,在 slab1 的外部视角里,就有了两个 freelist 链表,它们的共同之处都是用于组织 slab1 中的闲暇对象,然而 kmem_cache_cpu1->freelist 链表中组织的是缓存再 cpu1 本地的闲暇对象,slab1->freelist 链表组织的是由其余 cpu 开释的闲暇对象。

明确了这些,让咱们再次回到 ___slab_alloc 函数的开始处,首先内核会在 slab cache 的本地 cpu 缓存 kmem_cache_cpu->freelist 中查找是否有闲暇对象,如果这里没有,内核会持续到 page->freelist 中查看是否有其余 cpu 开释的闲暇对象

如果两个 freelist 链表都没有闲暇对象了,那就证实 slab cache 在以后 cpu 本地缓存中的 slab 曾经为空了,将该 slab 从以后 cpu 本地缓存中脱离冻结,程序跳转到 new_slab 分支进入慢速调配门路。

// 查看 page->freelist 中是否有其余 cpu 开释的闲暇对象
static inline void *get_freelist(struct kmem_cache *s, struct page *page)
{
    // 用于寄存要更新的 page 属性值
    struct page new;
    unsigned long counters;
    void *freelist;

    do {
        // 获取 page 构造的 freelist,当其余 cpu 向 page 开释对象时 freelist 指向被开释的闲暇对象
        // 当 page 被 slab cache 的 cpu 本地缓存时,freelist 置为 null
        freelist = page->freelist;
        counters = page->counters;

        new.counters = counters;
        VM_BUG_ON(!new.frozen);
        // 更新 inuse 字段,示意 page 中的对象 objects 全副被调配进来了
        new.inuse = page->objects;
        // 如果 freelist != null,示意其余 cpu 又开释了一些对象到 page 中(slub)。// 则 page->frozen = 1 , slub 仍然解冻在 cpu 本地缓存中
        // 如果 freelist == null, 则 page->frozen = 0,slub 从 cpu 本地缓存中脱离冻结
        new.frozen = freelist != NULL;
        // 最初 cas 原子更新 page 构造中的相应属性
        // 这里须要留神的是,当 page 被 slab cache 本地 cpu 缓存时,page -> freelist 须要置空。// 因为在本地 cpu 缓存场景下 page -> freelist 指向其余 cpu 开释的闲暇对象列表
        // kmem_cache_cpu->freelist 指向的是被本地 cpu 缓存的闲暇对象列表
        // 这两个列表中的闲暇对象独特组成了 slub 中的闲暇对象
    } while (!__cmpxchg_double_slab(s, page,
        freelist, counters,
        NULL, new.counters,
        "get_freelist"));

    return freelist;
}

3.1 从本地 cpu 缓存 partial 列表中调配

内核通过在 redo 分支的查看,当初曾经确认了 slab cache 在以后 cpu 本地缓存的 slab 曾经没有任何可供调配的闲暇对象了。

上面内核正式进入到 slowpath 开始调配对象,首先内核会到本地 cpu 缓存的 partial 列表中去查看是否有一个 slab 能够调配对象。这里内核会从 partial 列表中的头结点开始遍历直到找到一个能够满足调配的 slab 进去。

随后内核会将该 slab 从 partial 列表中摘下,间接晋升为新的本地 cpu 缓存,这样一来 slab cache 的本地 cpu 缓存就被更新了,内核通过 kmem_cache_cpu->freelist 指针将缓存 slab 中的第一个闲暇对象调配进来,随后更新 kmem_cache_cpu->freelist 指向 slab 中的下一个闲暇对象。

static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
              unsigned long addr, struct kmem_cache_cpu *c)
{
          ............ 查看本地 cpu 缓存是否为空 ...........
redo:
          ............ 再次确认 kmem_cache_cpu->freelist 中是否有闲暇对象 ...........
          ............ 再次确认 page->freelist 中是否有闲暇对象 ...........

load_freelist:
          ............ 回到 fastpath 间接从 freelist 中调配对象 ...........
new_slab:
    // 查看 kmem_cache_cpu->partial 链表中是否有 slab 可供调配对象
    if (slub_percpu_partial(c)) {
        // 获取 cpu 本地缓存 kmem_cache_cpu 的 partial 列表中的第一个 slub(用 page 示意)// 并将这个 slub 晋升为 cpu 本地缓存中的 slub,赋值给 c->page
        page = c->page = slub_percpu_partial(c);
        // 将 partial 列表中第一个 slub(c->page)从 partial 列表中摘下
        // 并将列表中的下一个 slub 更新为 partial 列表的头结点
        slub_set_percpu_partial(c, page);
        // 更新状态信息,记录本次调配是从 kmem_cache_cpu 的 partial 列表中调配
        stat(s, CPU_PARTIAL_ALLOC);
        // 从新回到 redo 分支,这下就能够从 page->freelist 中获取对象了
        // 并且在 load_freelist 分支中将  page->freelist 更新到 c->freelist 中,page->freelist 设置为 null
        // 此时 slab cache 中的 cpu 本地缓存 kmem_cache_cpu 的 freelist 以及 page 就变为了 partial 列表中的 slub
        goto redo;
    }

    // 流程走到这里示意 slab cache 中的 cpu 本地缓存 partial 列表中也没有 slub 了
    // 须要近一步降级到 numa node cache —— kmem_cache_node 中的 partial 列表去查找
    // 如果还是没有,就只能去搭档零碎中申请新的 slub,而后调配对象
    // 该函数为 slab cache 在慢速门路下调配对象的外围逻辑
    freelist = new_slab_objects(s, gfpflags, node, &c);

    if (unlikely(!freelist)) {
        // 如果搭档零碎中无奈调配 slub 所需的 page,那么就提醒内存不足,调配失败,返回 null
        slab_out_of_memory(s, gfpflags, node);
        return NULL;
    }

    page = c->page;
    if (likely(!kmem_cache_debug(s) && pfmemalloc_match(page, gfpflags)))
        // 此时从 kmem_cache_node->partial 列表中获取的 slub 
        // 或者从搭档零碎中从新申请的 slub 曾经被晋升为本地 cpu 缓存了 kmem_cache_cpu->page
        // 这里须要跳转到 load_freelist 分支,从本地 cpu 缓存 slub 中获取第一个对象返回
        goto load_freelist;
 
}

内核对 kmem_cache_cpu->partial 链表的相干操作:

// 定义在文件 /include/linux/slub_def.h 中
#ifdef CONFIG_SLUB_CPU_PARTIAL
// 获取 slab cache 本地 cpu 缓存的 partial 列表
#define slub_percpu_partial(c)      ((c)->partial)
// 将 partial 列表中第一个 slub 摘下,晋升为 cpu 本地缓存,用于后续疾速调配对象
#define slub_set_percpu_partial(c, p)       \
({                      \
    slub_percpu_partial(c) = (p)->next; \
})

如果 slab cache 本地 cpu 缓存 kmem_cache_cpu->partial 链表也是空的,接下来内核就只能到对应 NUMA 节点缓存中去调配对象了。

3.2 从 NUMA 节点缓存中调配

// slab cache 慢速门路下调配对象外围逻辑
static inline void *new_slab_objects(struct kmem_cache *s, gfp_t flags,
            int node, struct kmem_cache_cpu **pc)
{
    // 从 numa node cache 中获取到的闲暇对象列表
    void *freelist;
    // slab cache 本地 cpu 缓存
    struct kmem_cache_cpu *c = *pc;
    // 调配对象所在的内存页
    struct page *page;
    // 尝试从指定的 node 节点缓存 kmem_cache_node 中的 partial 列表获取能够调配闲暇对象的 slub
    // 如果指定 numa 节点的内存不足,则会依据 cpu 拜访间隔的远近,进行跨 numa 节点调配
    freelist = get_partial(s, flags, node, c);

    if (freelist)
        // 返回 numa cache 中缓存的闲暇对象列表
        return freelist;
    // 流程走到这里阐明 numa cache 里缓存的 slub 也用尽了,无奈找到能够调配对象的 slub 了
    // 只能向底层搭档零碎从新申请内存页(slub),而后从新的 slub 中调配对象
    page = new_slab(s, flags, node);
    // 将新申请的内存页 page(slub),缓存到 slab cache 的本地 cpu 缓存中
    if (page) {
        // 获取 slab cache 的本地 cpu 缓存
        c = raw_cpu_ptr(s->cpu_slab);
        // 刷新本地 cpu 缓存,将旧的 slub 缓存与 cpu 本地缓存解绑
        if (c->page)
            flush_slab(s, c);

        // 将新申请的 slub 与 cpu 本地缓存绑定,page->freelist 赋值给 kmem_cache_cpu->freelist
        freelist = page->freelist;
        // 绑定之后  page->freelist 置空
        // 当初新的 slub 中的闲暇对象就曾经缓存再了 slab cache 的本地 cpu 缓存中,后续就间接从这里调配了
        page->freelist = NULL;

        stat(s, ALLOC_SLAB);
        // 将新申请的 slub 对应的 page 赋值给 kmem_cache_cpu->page
        c->page = page;
        *pc = c;
    }
    // 返回闲暇对象列表
    return freelist;
}

内核首先会在 get_partial 函数中找到咱们指定的 NUMA 节点缓存构造 kmem_cache_node,而后开始遍历 kmem_cache_node->partial 链表直到找到一个可供调配对象的 slab。而后将这个 slab 晋升为 slab cache 的本地 cpu 缓存,并从 kmem_cache_node->partial 链表中顺次填充 slab 到 kmem_cache_cpu->partial。

如果咱们指定的 NUMA 节点 kmem_cache_node->partial 链表也是空的,随后内核就会跨 NUMA 节点进行查找,依照拜访间隔由近到远,开始查找其余 NUMA 节点 kmem_cache_node->partial 链表。

如果还是不行,最初就只能通过 new_slab 函数到搭档零碎中从新申请一个 slab,并将这个 slab 晋升为本地 cpu 缓存。

3.2.1 从 NUMA 节点缓存 partial 链表中查找

static void *get_partial(struct kmem_cache *s, gfp_t flags, int node,
        struct kmem_cache_cpu *c)
{
    // 从指定 node 的 kmem_cache_node 缓存中的 partial 列表中获取到的对象
    void *object;
    // 行将要所搜寻的 kmem_cache_node 缓存对应 numa node
    int searchnode = node;
    // 如果咱们指定的 numa node 曾经没有闲暇内存了,则选取拜访间隔最近的 numa node 进行跨节点内存调配
    if (node == NUMA_NO_NODE)
        searchnode = numa_mem_id();
    else if (!node_present_pages(node))
        searchnode = node_to_mem_node(node);

    // 从 searchnode 的 kmem_cache_node 缓存中的 partial 列表中获取对象
    object = get_partial_node(s, get_node(s, searchnode), c, flags);
    if (object || node != NUMA_NO_NODE)
        return object;
    // 如果 searchnode 对象的 kmem_cache_node 缓存中的 partial 列表是空的,没有任何可供调配的 slub
    // 那么持续依照拜访间隔,遍历 searchnode 之后的 numa node,进行跨节点内存调配
    return get_any_partial(s, flags, c);
}

get_partial 函数的次要内容是选取适合的 NUMA 节点缓存,优先应用咱们指定的 NUMA 节点,如果指定的 NUMA 节点中没有足够的内存,内核就会跨 NUMA 节点依照拜访间隔的远近,选取一个适合的 NUMA 节点。

而后通过 get_partial_node 在选取的 NUMA 节点缓存 kmem_cache_node->partial 链表中查找 slab。

/*
 * Try to allocate a partial slab from a specific node.
 */
static void *get_partial_node(struct kmem_cache *s, struct kmem_cache_node *n,
                struct kmem_cache_cpu *c, gfp_t flags)
{
    // 接下来就会挨个遍历 kmem_cache_node 的 partial 列表中的 slub
    // 这两个变量用于长期存储遍历的 slub
    struct page *page, *page2;
    // 用于指向从 partial 列表 slub 中申请到的对象
    void *object = NULL;
    // 用于记录 slab cache 本地 cpu 缓存 kmem_cache_cpu 中所缓存的闲暇对象总数(包含 partial 列表)// 后续会向 kmem_cache_cpu 中填充 slub
    unsigned int available = 0;
    // 长期记录遍历到的 slub 中蕴含的残余闲暇对象个数
    int objects;

    spin_lock(&n->list_lock);
    // 开始挨个遍历 kmem_cache_node 的 partial 列表,获取 slub 用于调配对象以及填充 kmem_cache_cpu
    list_for_each_entry_safe(page, page2, &n->partial, slab_list) {
        void *t;
        // page 示意以后遍历到的 slub,这里会从该 slub 中获取闲暇对象赋值给 t
        // 并将 slub 从 kmem_cache_node 的 partial 列表上摘下
        t = acquire_slab(s, n, page, object == NULL, &objects);
        // 如果 t 是空的,阐明 partial 列表上曾经没有可供调配对象的 slub 了
        // slub 都满了,退出循环,进入搭档零碎从新申请 slub
        if (!t)            
            break;
        // objects 示意以后 slub 中蕴含的残余闲暇对象个数
        // available 用于统计目前遍历的 slub 中所有闲暇对象个数
        // 前面会依据 available 的值来判断是否持续填充 kmem_cache_cpu
        available += objects;
        if (!object) {
            // 第一次循环会走到这里,第一次循环次要是满足以后对象调配的需要
            // 将 partila 列表中第一个 slub 缓存进 kmem_cache_cpu 中
            c->page = page;
            stat(s, ALLOC_FROM_PARTIAL);
            object = t;
        } else {
            // 第二次以及前面的循环就会走到这里,目标是从 kmem_cache_node 的 partial 列表中
            // 摘下 slub,而后填充进 kmem_cache_cpu 的 partial 列表里
            put_cpu_partial(s, page, 0);
            stat(s, CPU_PARTIAL_NODE);
        }
        // 这里是用于判断是否持续填充 kmem_cache_cpu 中的 partial 列表
        // kmem_cache_has_cpu_partial 用于判断 slab cache 是否配置了 cpu 缓存的 partial 列表
        // 配置了 CONFIG_SLUB_CPU_PARTIAL 选项意味着开启 kmem_cache_cpu 中的 partial 列表,没有配置的话,cpu 缓存中就不会有 partial 列表
        // kmem_cache_cpu 中缓存被填充之后的闲暇对象个数(包含 partial 列表)不能超过 (kmem_cache 构造中 cpu_partial 指定的个数 / 2)
        if (!kmem_cache_has_cpu_partial(s)
            || available > slub_cpu_partial(s) / 2)
            // kmem_cache_cpu 曾经填充斥了,就退出循环,进行填充
            break;

    }
  
    spin_unlock(&n->list_lock);
    return object;
}

get_partial_node 函数通过遍历 NUMA 节点缓存构造 kmem_cache_node->partial 链表次要做两件事件:

  1. 将第一个遍历到的 slab 从 partial 链表中摘下,晋升为本地 cpu 缓存 kmem_cache_cpu->page。
  2. 持续遍历 partial 链表,前面遍历到的 slab 会填充进本地 cpu 缓存 kmem_cache_cpu->partial 链表中,直到以后 cpu 缓存的所有闲暇对象数目 available(既包含 kmem_cache_cpu->page 中的闲暇对象也包含 kmem_cache_cpu->partial 链表中的闲暇对象)超过了 kmem_cache->cpu_partial / 2 的限度。

当初 slab cache 的本地 cpu 缓存曾经被填充好了,随后内核会从 kmem_cache_cpu->freelist 中调配一个闲暇对象进去给过程应用。

3.2.2 从 NUMA 节点缓存 partial 链表中将 slab 摘下

// 从 kmem_cache_node 的 partial 列表中摘下一个 slub 调配对象
// 随后将摘下的 slub 放入 cpu 本地缓存 kmem_cache_cpu 中缓存,后续调配对象间接就会 cpu 缓存中调配
static inline void *acquire_slab(struct kmem_cache *s,
        struct kmem_cache_node *n, struct page *page,
        int mode, int *objects)
{
    void *freelist;
    unsigned long counters;
    struct page new;

    lockdep_assert_held(&n->list_lock);
    // page 示意行将从 kmem_cache_node 的 partial 列表摘下的 slub
    // 获取 slub  中的闲暇对象列表 freelist
    freelist = page->freelist;
    counters = page->counters;
    new.counters = counters;
    // objects 寄存该 slub 中还剩多少闲暇对象
    *objects = new.objects - new.inuse;
    // mode = true 示意将 slub 摘下之后填充到 kmem_cache_cpu 缓存中
    // mode = false 示意将 slub 摘下之后填充到 kmem_cache_cpu 缓存的 partial 列表中
    if (mode) {
        new.inuse = page->objects;
        new.freelist = NULL;
    } else {new.freelist = freelist;}
    // slub 放入 kmem_cache_cpu 之后须要解冻,其余 cpu 不能从这里调配对象,只能开释对象
    new.frozen = 1;
    // 更新 slub(page 示意)中的 freelist 和 counters
    if (!__cmpxchg_double_slab(s, page,
            freelist, counters,
            new.freelist, new.counters,
            "acquire_slab"))
        return NULL;
    // 将 slub(page 示意)从 kmem_cache_node 的 partial 列表上摘下
    remove_partial(n, page);
    // 返回 slub 中的闲暇对象列表
    return freelist;
}

3.3 从搭档零碎中从新申请 slab

假如 slab cache 以后的架构如上图所示,本地 cpu 缓存 kmem_cache_cpu->page 为空,kmem_cache_cpu->partial 为空,kmem_cache_node->partial 链表也为空,比方 slab cache 在刚刚被创立进去的时候就是这个架构。

在这种状况下,内核就须要通过 new_slab 函数到搭档零碎中申请一个新的 slab,填充到 slab cache 的本地 cpu 缓存 kmem_cache_cpu->page 中。

static struct page *new_slab(struct kmem_cache *s, gfp_t flags, int node)
{
    return allocate_slab(s,
        flags & (GFP_RECLAIM_MASK | GFP_CONSTRAINT_MASK), node);
}

static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
    // 用于指向从搭档零碎中申请到的内存页
    struct page *page;
    // kmem_cache 构造的中的 kmem_cache_order_objects oo,示意该 slub 须要多少个内存页,以及可能包容多少个对象
    // kmem_cache_order_objects 的高 16 位示意须要的内存页个数,低 16 位示意可能包容的对象个数
    struct kmem_cache_order_objects oo = s->oo;
    // 管制向搭档零碎申请内存的行为规范掩码
    gfp_t alloc_gfp;
    void *start, *p, *next;
    int idx;
    bool shuffle;
    // 向搭档零碎申请 oo 中规定的内存页
    page = alloc_slab_page(s, alloc_gfp, node, oo);
    if (unlikely(!page)) {
        // 如果搭档零碎无奈满足失常状况下 oo 指定的内存页个数
        // 那么这里再次尝试用 min 中指定的内存页个数向搭档零碎申请内存页
        // min 示意当内存不足或者内存碎片的起因无奈满足内存调配时,至多要保障包容一个对象所应用内存页个数
        oo = s->min;
        alloc_gfp = flags;
        // 再次向搭档零碎申请包容一个对象所须要的内存页(降级)page = alloc_slab_page(s, alloc_gfp, node, oo);
        if (unlikely(!page))
            // 如果内存还是有余,则走到 out 分支间接返回 null
            goto out;
        stat(s, ORDER_FALLBACK);
    }
    // 初始化 slub 对应的 struct page 构造中的属性
    // 获取 slub 能够包容的对象个数
    page->objects = oo_objects(oo);
    // 将 slub cache  与 page 构造关联
    page->slab_cache = s;
    // 将 PG_slab 标识设置到 struct page 的 flag 属性中
    // 示意该内存页 page 被 slub 所治理
    __SetPageSlab(page);
    // 用 0xFC 填充 slub 中的内存,用于内核对内存拜访越界查看
    kasan_poison_slab(page);
    // 获取内存页对应的虚拟内存地址
    start = page_address(page);
    // 在配置了 CONFIG_SLAB_FREELIST_RANDOM 选项的状况下
    // 会在 slub 的闲暇对象中以随机的程序初始化 freelist 列表
    // 返回值 shuffle = true 示意随机初始化 freelist,shuffle = false 示意依照失常的程序初始化 freelist    
    shuffle = shuffle_freelist(s, page);
    // shuffle = false 则依照失常的程序来初始化 freelist
    if (!shuffle) {
        // 获取 slub 第一个闲暇对象的真正起始地址
        // slub 可能配置了 SLAB_RED_ZONE,这样会在 slub 对象内存空间两侧填充 red zone,避免内存拜访越界
        // 这里须要跳过 red zone 获取真正寄存对象的内存地址
        start = fixup_red_left(s, start);
        // 填充对象的内存区域以及初始化闲暇对象
        start = setup_object(s, page, start);
        // 用 slub 中的第一个闲暇对象作为 freelist 的头结点,而不是随机的一个闲暇对象
        page->freelist = start;
        // 从 slub 中的第一个闲暇对象开始,依照失常的程序通过对象的 freepointer 串联起 freelist
        for (idx = 0, p = start; idx < page->objects - 1; idx++) {
            // 获取下一个对象的内存地址
            next = p + s->size;
            // 填充下一个对象的内存区域以及初始化
            next = setup_object(s, page, next);
            // 通过 p 的 freepointer 指针指向 next, 设置 p 的下一个闲暇对象为 next
            set_freepointer(s, p, next);
            // 通过循环遍历,就把 slub 中的闲暇对象依照失常程序串联在 freelist 中了
            p = next;
        }
        // freelist 中的尾结点的 freepointer 设置为 null
        set_freepointer(s, p, NULL);
    }
    // slub 的初始状态 inuse 的值为所有闲暇对象个数
    page->inuse = page->objects;
    // slub 被创立进去之后,须要放入 cpu 本地缓存 kmem_cache_cpu 中
    page->frozen = 1;

out:
    if (!page)
        return NULL;
    // 更新 page 所在 numa 节点在 slab cache 中的缓存 kmem_cache_node 构造中的相干计数
    // kmem_cache_node 中蕴含的 slub 个数加 1,蕴含的总对象个数加 page->objects
    inc_slabs_node(s, page_to_nid(page), page->objects);
    return page;
}

内核在向搭档零碎申请 slab 之前,须要晓得一个 slab 具体须要多少个物理内存页,而这些信息定义在 struct kmem_cache 构造中的 oo 属性中:

struct kmem_cache {
    // 其中低 16 位示意一个 slab 中所蕴含的对象总数,高 16 位示意一个 slab 所占有的内存页个数。struct kmem_cache_order_objects oo;
}

通过 oo 的高 16 位获取 slab 须要的物理内存页数,而后调用 alloc_pages 或者 __alloc_pages_node 向搭档零碎申请。

static inline struct page *alloc_slab_page(struct kmem_cache *s,
        gfp_t flags, int node, struct kmem_cache_order_objects oo)
{
    struct page *page;
    unsigned int order = oo_order(oo);

    if (node == NUMA_NO_NODE)
        page = alloc_pages(flags, order);
    else
        page = __alloc_pages_node(node, flags, order);

    return page;
}

对于 alloc_pages 函数调配物理内存页的具体过程,感兴趣的读者能够回看下《深刻了解 Linux 物理内存调配全链路实现》

如果以后 NUMA 节点中的闲暇内存不足,或者因为内存碎片的起因导致搭档零碎无奈满足 slab 所须要的内存页个数,导致调配失败。

那么内核会降级采纳 kmem_cache->min 指定的尺寸,向搭档零碎申请只包容一个对象所须要的最小内存页个数。

struct kmem_cache {
    // 当依照 oo 的尺寸为 slab 申请内存时,如果内存缓和,会采纳 min 的尺寸为 slab 申请内存,能够包容一个对象即可。struct kmem_cache_order_objects min;
}

如果搭档零碎依然无奈满足,那么就只能跨 NUMA 节点调配了。如果胜利地向搭档零碎申请到了 slab 所须要的内存页 page。紧接着就会初始化 page 构造中与 slab 相干的属性。

通过 kasan_poison_slab 函数将 slab 中的内存用 0xFC 填充,用于 kasan 对于内存越界相干的查看。

// 定义在文件:/mm/kasan/kasan.h
#define KASAN_KMALLOC_REDZONE   0xFC  /* redzone inside slub object */

// 定义在文件:/mm/kasan/common.c
void kasan_poison_slab(struct page *page)
{
    unsigned long i;
    // slub 可能蕴含多个内存页 page,挨个遍历这些 page
    // 革除这些 page->flag 中的内存越界查看标记
    // 示意当拜访到这些内存页的时候长期禁止内存越界查看
    for (i = 0; i < compound_nr(page); i++)
        page_kasan_tag_reset(page + i);
    // 用 0xFC 填充这些内存页的内存,用于内存拜访越界查看
    kasan_poison_shadow(page_address(page), page_size(page),
            KASAN_KMALLOC_REDZONE);
}

最初会初始化 slab 中的 freelist 链表,将内存页中的闲暇内存块通过 page->freelist 链表组织起来。

如果内核开启了 CONFIG_SLAB_FREELIST_RANDOM 选项,那么就会通过
shuffle_freelist 函数将内存页中闲暇的内存块依照随机的程序串联在 page->freelist 中。

如果没有开启,则会在 if (!shuffle) 分支中,依照失常的程序初始化 page->freelist。

最初通过 inc_slabs_node 更新 NUMA 节点缓存 kmem_cache_node 构造中的相干计数。

struct kmem_cache_node {
    // slab 的个数
    atomic_long_t nr_slabs;
    // 该 node 节点中缓存的所有 slab 中蕴含的对象总和
    atomic_long_t total_objects;
};
static inline void inc_slabs_node(struct kmem_cache *s, int node, int objects)
{
    // 获取 page 所在 numa node 再 slab cache 中的缓存
    struct kmem_cache_node *n = get_node(s, node);

    if (likely(n)) {
        // kmem_cache_node 中的 slab 计数加 1
        atomic_long_inc(&n->nr_slabs);
        // kmem_cache_node 中蕴含的总对象计数加 objects
        atomic_long_add(objects, &n->total_objects);
    }
}

4. 初始化 slab freelist 链表

内核在对 slab 中的 freelist 链表初始化的时候,会有两种形式,一种是依照内存地址的程序,一个一个的通过对象 freepointer 指针程序串联所有闲暇对象。

另外一种则是通过随机的形式,随机获取闲暇对象,而后通过对象的 freepointer 指针将 slab 中的闲暇对象依照随机的程序串联起来。

思考到程序初始化 freelist 比拟直观,为了不便大家的了解,笔者先为大家介绍程序初始化的形式。

static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
    // 获取 slab 的起始内存地址
    start = page_address(page);
    // shuffle_freelist 随机初始化 freelist 链表,返回 false 示意须要程序初始化 freelist
    shuffle = shuffle_freelist(s, page);
    // shuffle = false 则依照失常的程序来初始化 freelist
    if (!shuffle) {
        // 获取 slub 第一个闲暇对象的真正起始地址
        // slub 可能配置了 SLAB_RED_ZONE,这样会在 slub 对象内存空间两侧填充 red zone,避免内存拜访越界
        // 这里须要跳过 red zone 获取真正寄存对象的内存地址
        start = fixup_red_left(s, start);
        // 填充对象的内存区域以及初始化闲暇对象
        start = setup_object(s, page, start);
        // 用 slub 中的第一个闲暇对象作为 freelist 的头结点,而不是随机的一个闲暇对象
        page->freelist = start;
        // 从 slub 中的第一个闲暇对象开始,依照失常的程序通过对象的 freepointer 串联起 freelist
        for (idx = 0, p = start; idx < page->objects - 1; idx++) {
            // 获取下一个对象的内存地址
            next = p + s->size;
            // 填充下一个对象的内存区域以及初始化
            next = setup_object(s, page, next);
            // 通过 p 的 freepointer 指针指向 next, 设置 p 的下一个闲暇对象为 next
            set_freepointer(s, p, next);
            // 通过循环遍历,就把 slub 中的闲暇对象依照失常程序串联在 freelist 中了
            p = next;
        }
        // freelist 中的尾结点的 freepointer 设置为 null
        set_freepointer(s, p, NULL);
    }
}

内核在程序初始化 slab 中的 freelist 之前,首先须要晓得 slab 的起始内存地址 start,然而思考到 slab 如果配置了 SLAB_RED_ZONE 的状况,那么在 slab 对象左右两侧,内核均会插入两段 red zone,为了避免内存拜访越界。

所以在这种状况下,咱们通过 page_address 获取到的只是 slab 的起始内存地址,正是 slab 中第一个闲暇对象的左侧 red zone 的起始地位。

所以咱们须要通过 fixup_red_left 办法来修改 start 地位,使其越过 slab 对象左侧的 red zone,指向对象内存真正的起始地位,如上图中所示。

void *fixup_red_left(struct kmem_cache *s, void *p)
{
    // 如果 slub 配置了 SLAB_RED_ZONE,则意味着须要再 slub 对象内存空间两侧填充 red zone,避免内存拜访越界
    // 这里须要跳过填充的 red zone 获取真正的闲暇对象起始地址
    if (kmem_cache_debug(s) && s->flags & SLAB_RED_ZONE)
        p += s->red_left_pad;
    // 如果没有配置 red zone,则间接返回对象的起始地址
    return p;
}

当咱们确定了对象的起始地位之后,对象所在的内存块也就确定了,随后调用 setup_object 函数来初始化内存块,这里会依照 slab 对象的内存布局进行填充相应的区域。

slab 对象具体的内存布局介绍,能够回看下笔者之前的文章《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》中的“5. 从一个简略的内存页开始聊 slab”大节。

当初始化完对象的内存区域之后,slab 中的 freelist 指针就会指向这第一个曾经被初始化好的闲暇对象。

page->freelist = start;

随后通过 start + kmem_cache->size 程序获取下一个闲暇对象的起始地址,反复上述初始化对象过程。直到 slab 中的闲暇对象全副串联在 freelist 中,freelist 中的最初一个闲暇对象 freepointer 指向 null。

一般来说,都会应用程序的初始化形式来初始化 freelist,但出于平安因素的思考,避免被攻打,会配置 CONFIG_SLAB_FREELIST_RANDOM 选项,这样就会使 slab 中的闲暇对象以随机的形式串联在 freelist 中,无奈预测。

在咱们明确了 slab freelist 的程序初始化形式之后,随机的初始化形式其实就很好了解了。

随机初始化和程序初始化 惟一不同 的点在于,获取闲暇对象起始地址的形式不同:

  • 程序初始化的形式是间接获取 slab 中第一个闲暇对象的地址,而后通过 start + kmem_cache->size 依照程序一个一个地获取前面对象地址。
  • 随机初始化的形式则是通过随机的形式获取 slab 中闲暇对象,也就是说 freelist 中的头结点可能是 slab 中的第一个对象,也可能是第三个对象。后续也是通过这种随机的形式来获取下一个随机的闲暇对象。
// 返回值为 true 示意随机的初始化 freelist,false 示意采纳第一个闲暇对象初始化 freelist
static bool shuffle_freelist(struct kmem_cache *s, struct page *page)
{
    // 指向第一个闲暇对象
    void *start;
    void *cur;
    void *next;
    unsigned long idx, pos, page_limit, freelist_count;
    // 如果没有配置 CONFIG_SLAB_FREELIST_RANDOM 选项或者 slub 包容的对象个数小于 2
    // 则无需对 freelist 进行随机初始化
    if (page->objects < 2 || !s->random_seq)
        return false;
    // 获取 slub 中能够包容的对象个数
    freelist_count = oo_objects(s->oo);
    // 获取用于随机初始化 freelist 的随机地位
    pos = get_random_int() % freelist_count;
    page_limit = page->objects * s->size;
    // 获取 slub 第一个闲暇对象的真正起始地址
    // slub 可能配置了 SLAB_RED_ZONE,这样会在 slub 中对象内存空间两侧填充 red zone,避免内存拜访越界
    // 这里须要跳过 red zone 获取真正寄存对象的内存地址
    start = fixup_red_left(s, page_address(page));

   // 依据随机地位 pos 获取第一个随机对象的间隔 start 的偏移 idx
   // 返回第一个随机对象的内存地址 cur = start + idx
    cur = next_freelist_entry(s, page, &pos, start, page_limit,
                freelist_count);
    // 填充对象的内存区域以及初始化闲暇对象
    cur = setup_object(s, page, cur);
    // 第一个随机对象作为 freelist 的头结点
    page->freelist = cur;
    // 以 cur 为头结点随机初始化 freelist(每一个闲暇对象都是随机的)for (idx = 1; idx < page->objects; idx++) {
        // 随机获取下一个闲暇对象
        next = next_freelist_entry(s, page, &pos, start, page_limit,
            freelist_count);
        // 填充对象的内存区域以及初始化闲暇对象
        next = setup_object(s, page, next);
        // 设置 cur 的下一个闲暇对象为 next
        // next 对象的指针就是 freepointer,寄存于 cur 对象的 s->offset 偏移处
        set_freepointer(s, cur, next);
        // 通过循环遍历,就把 slub 中的闲暇对象随机的串联在 freelist 中了
        cur = next;
    }
    // freelist 中的尾结点的 freepointer 设置为 null
    set_freepointer(s, cur, NULL);
    // 示意随机初始化 freelist
    return true;
}

5. slab 对象的初始化

内核依照 kmem_cache->size 指定的尺寸,将物理内存页中的内存划分成一个一个的小内存块,每一个小内存块即是 slab 对象占用的内存区域。setup_object 函数用于初始化这些内存区域,并对 slab 对象进行内存布局。

static void *setup_object(struct kmem_cache *s, struct page *page,
                void *object)
{
    // 初始化对象的内存区域,填充相干的字节,比方填充 red zone,以及 poison 对象
    setup_object_debug(s, page, object);
    object = kasan_init_slab_obj(s, object);
    // 如果 kmem_cache 中设置了对象的构造函数 ctor,则用构造函数初始化对象
    if (unlikely(s->ctor)) {kasan_unpoison_object_data(s, object);
        // 应用用户指定的构造函数初始化对象
        s->ctor(object);
        // 在对象内存区域的结尾用 0xFC 填充一段 KASAN_SHADOW_SCALE_SIZE 大小的区域
        // 用于对内存拜访越界的查看
        kasan_poison_object_data(s, object);
    }
    return object;
}
// 定义在文件:/mm/kasan/kasan.h
#define KASAN_KMALLOC_REDZONE   0xFC  /* redzone inside slub object */
#define KASAN_SHADOW_SCALE_SIZE (1UL << KASAN_SHADOW_SCALE_SHIFT)
// 定义在文件:/arch/x86/include/asm/kasan.h
#define KASAN_SHADOW_SCALE_SHIFT 3

void kasan_poison_object_data(struct kmem_cache *cache, void *object)
{
    // 在对象内存区域的结尾用 0xFC 填充一段 KASAN_SHADOW_SCALE_SIZE 大小的区域
    // 用于对内存拜访越界的查看
    kasan_poison_shadow(object,
            round_up(cache->object_size, KASAN_SHADOW_SCALE_SIZE),
            KASAN_KMALLOC_REDZONE);
}

对于 slab 对象内存布局的外围逻辑封装在 setup_object_debug 函数中:

// 定义在文件:/include/linux/poison.h
#define SLUB_RED_INACTIVE    0xbb

static void setup_object_debug(struct kmem_cache *s, struct page *page,
                                void *object)
{
    // SLAB_STORE_USER:存储最近拜访该对象的 owner 信息,不便 bug 追踪
    // SLAB_RED_ZONE:在 slub 中对象内存区域的前后填充别离填充一段 red zone 区域,避免内存拜访越界
    // __OBJECT_POISON:在对象内存区域中填充一些特定的字符,示意对象特定的状态。比方:未被调配状态
    if (!(s->flags & (SLAB_STORE_USER|SLAB_RED_ZONE|__OBJECT_POISON)))
        return;
    // 初始化对象内存,比方填充 red zone,以及 poison
    init_object(s, object, SLUB_RED_INACTIVE);
    // 设置 SLAB_STORE_USER 起作用,初始化拜访对象的所有者相干信息
    init_tracking(s, object);
}

init_object 函数次要针对 slab 对象的内存区域进行布局,这里包含对 red zone 的填充,以及 POISON 对象的 object size 区域。

// 定义在文件:/include/linux/poison.h
#define SLUB_RED_INACTIVE   0xbb

// 定义在文件:/include/linux/poison.h
#define POISON_FREE    0x6b    /* for use-after-free poisoning */
#define    POISON_END    0xa5    /* end-byte of poisoning */

static void init_object(struct kmem_cache *s, void *object, u8 val)
{// p 为真正存储对象的内存区域起始地址(不蕴含填充的 red zone)
    u8 *p = object;
    // red zone 位于真正存储对象内存区域 object size 的左右两侧,别离有一段 red zone
    if (s->flags & SLAB_RED_ZONE)
        // 首先应用 0xbb 填充对象左侧的 red zone
        // 左侧 red zone 区域为对象的起始地址到  s->red_left_pad 的长度
        memset(p - s->red_left_pad, val, s->red_left_pad);

    if (s->flags & __OBJECT_POISON) {
        // 将对象的内容用 0x6b 填充,示意该对象在 slub 中还未被应用
        memset(p, POISON_FREE, s->object_size - 1);
        // 对象的最初一个字节用 0xa5 填充,示意 POISON 的开端
        p[s->object_size - 1] = POISON_END;
    }

    // 在对象内存区域 object size 的右侧持续用 0xbb 填充右侧 red zone
    // 右侧 red zone 的地位为:对象实在内存区域的开端开始一个字长的区域
    // s->object_size 示意对象自身的内存占用,s->inuse 示意对象在 slub 管理体系下的实在内存占用(蕴含填充字节数)// 通常会在对象内存区域开端处填充一个字长大小的 red zone 区域
    // 对象右侧 red zone 区域前面紧跟着的就是 freepointer
    if (s->flags & SLAB_RED_ZONE)
        memset(p + s->object_size, val, s->inuse - s->object_size);
}

内核首先会用 0xbb 来填充对象左侧 red zone,长度为 kmem_cache-> red_left_pad。

随后内核会用 0x6b 填充 object size 内存区域,并用 0xa5 填充该区域的最初一个字节。object size 内存区域正是真正存储对象的区域。

最初用 0xbb 来填充对象右侧 red zone,右侧 red zone 的起始地址为:p + s->object_size,长度为:s->inuse – s->object_size。如下图所示:

总结

本文咱们基于 slab cache 的残缺的架构,近一步深刻到内核源码中具体介绍了 slab cache 对于内存调配的残缺流程:

咱们能够看到 slab cache 内存调配的整个流程分为 fastpath 疾速门路和 slowpath 慢速门路。

其中在 fastpath 门路下,内核会间接从 slab cache 的本地 cpu 缓存中获取内存块,这是最快的一种形式。

在本地 cpu 缓存没有足够的内存块可供调配的时候,内核就进入到了 slowpath 门路,而 slowpath 下又分为多种状况:

  1. 从本地 cpu 缓存 partial 列表中调配
  2. 从 NUMA 节点缓存中调配,其中波及到了对本地 cpu 缓存的填充。
  3. 从搭档零碎中从新申请 slab

最初咱们介绍了 slab 所在内存页的具体初始化流程,其中包含了对 slab freelist 链表的初始化,以及 slab 对象的初始化。

好了本文的内容到这里就完结了,感激大家的收看,咱们下篇文章见~~~

正文完
 0