Linux内存管理7——深入理解 slab cache 内存分配全链路实现

news2024/12/22 23:56:10

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);
          ...........   
}
#ifndef CONFIG_ARCH_TASK_STRUCT_ALLOCATOR
static struct kmem_cache *task_struct_cachep;
static inline struct task_struct *alloc_task_struct_node(int node)
{
    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 的快速分配路径

        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。大家读到这里一定会感觉非常懵,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 中分配对象

        (1)如果一个 slab 被一个 cpu 缓存之后,那么这个 cpu 在该 slab 看来就是本地 cpu,当本地 cpu 释放对象回这个 slab 的时候会释放回 kmem_cache_cpu->freelist 链表中。

        (2)如果其他 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;
}

        如果当前 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 中的空闲对象按照随机的顺序串联起来

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,指向对象内存真正的起始位置,如上图中所示。 

struct kmem_cache {
    unsigned int red_left_pad;    /* Left redzone padding size */
}
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 中的 freelist 指针就会指向这第一个已经被初始化好的空闲对象。

page->freelist = start;

        随后通过 start + kmem_cache->size 顺序获取下一个空闲对象的起始地址,重复上述初始化对象过程。直到 slab 中的空闲对象全部串联在 freelist 中,freelist 中的最后一个空闲对象 freepointer 指向 null。一般来说,都会使用顺序的初始化方式来初始化 freelist, 但出于安全因素的考虑,防止被攻击,会配置 CONFIG_SLAB_FREELIST_RANDOM 选项,这样就会使 slab 中的空闲对象以随机的方式串联在 freelist 中,无法预测。

        在我们明白了 slab freelist 的顺序初始化方式之后,随机的初始化方式其实就很好理解了。随机初始化和顺序初始化唯一不同的点在于,获取空闲对象起始地址的方式不同:

        (1)顺序初始化的方式是直接获取 slab 中第一个空闲对象的地址,然后通过 start + kmem_cache->size 按照顺序一个一个地获取后面对象地址

        (2)随机初始化的方式则是通过随机的方式获取 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。如下图所示:

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/618505.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

iperf3使用

目录 写在前面&#xff1a;带宽和吞吐量安装使用测试TCP吞吐量测试UDP吞吐量测试上下行带宽&#xff08;TCP双向传输&#xff09;测试多线程TCP吞吐量测试上下行带宽&#xff08;UDP双向传输&#xff09;测试多线程UDP吞吐量 iperf3常用参数通用参数server端参数client端参数 i…

一种星载系统软件定义平台的设计与实现.v3

摘要 针对星载综合射频开放式系统架构&#xff0c;为了在软件综合层面上实现波形应用软件与具体平台的解耦&#xff0c;设计并实现了一种基于软件通信架构&#xff08;Software Communication Architecture, SCA&#xff09;的软件平台及其环境工具。通过解决星载平台软件的分…

linuxOPS基础_linux自有服务systemctl

自有服务概述 ​ 服务是一些特定的进程&#xff0c;自有服务就是系统开机后就自动运行的一些进程&#xff0c;一旦客户发出请求&#xff0c;这些进程就自动为他们提供服务&#xff0c;windows系统中&#xff0c;把这些自动运行的进程&#xff0c;称为"服务" ​ 举例…

总结888

学习目标&#xff1a; 月目标&#xff1a;6月&#xff08;线性代数强化9讲2遍&#xff0c;背诵15篇短文&#xff0c;考研核心词过三遍&#xff09; 周目标&#xff1a;线性代数强化1讲&#xff0c;英语背3篇文章并回诵&#xff0c;检测 每日必复习&#xff08;5分钟&#xff…

Java 基础第八章: 接口、内部类、包装类

参考资料 &#xff1a;康师傅的视频课 方法 、 有继承的代码块的加载顺序&#xff1a;先执行父类的静态代码块、子类的静态代码块&#xff1b;然后&#xff0c;执行父类的普通代码块和构造器 子类的的普通代码块和构造器&#xff1b; 总结&#xff1a;由父到子&#xff0c;静…

【Web服务器】Nginx之Rewrite与location的用法

文章目录 前言一、正则表达式1. Nginx 的正则表达式2. 正则表达的优势3. Nginx 使用正则的作用 二、location 的概念1. location 和 rewrite 区别2. location 匹配的分类3. location 常用的匹配规则3.1 location 匹配优先级3.2 location 匹配的实例3.3 实际网站规则定义第一个必…

深度学习应用篇-计算机视觉-图像分类[2]:LeNet、AlexNet、VGG、GoogleNet、DarkNet模型结构、实现、模型特点详细介绍

【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化算法、卷积模型、序列模型、预训练模型、对抗神经网络等 专栏详细介绍&#xff1a;【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化…

RabbitMQ - 发布确认

RabbitMQ - 发布确认 发布确认逻辑发布确认的策略单个确认发布批量确认发布异步确认发布 发布确认逻辑 生产者将信道设置成 confirm 模式&#xff0c;一旦信道进入 confirm 模式&#xff0c;所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始)&#xff0c;一旦消…

什么时候 MySQL 查询会变慢?

前面几篇文章和小伙伴们聊的基本上都是从索引的角度去优化 MySQL 查询&#xff0c;然而&#xff0c;索引创建的好&#xff0c;并不意味着查询就一定快&#xff0c;影响查询效率的因素特别多&#xff0c;今天我们就来聊一聊这些可能影响到查询的因素。 1. 查询流程 开始今天的…

欢迎来到新世界

&#xff08;1&#xff09; 我去年对技术的发展是比较灰心的&#xff1a; 云原生&#xff1a;技术一直动荡&#xff0c;SOA->Servless、Docker->WASM、GitOpsCICDDevOps云计算&#xff1a;在中国从公有云走向了私有云&#xff0c;乃至金融云、国资云、政务云等等N种云Saa…

圆满收官!飞桨黑客松第四期高手云集,四大赛道开源贡献持续升级

2023年2月20日PaddlePaddle Hackathon 飞桨黑客马拉松&#xff08;以下简称为“飞桨黑客松”&#xff09;第四期活动发布后&#xff0c;开发者们反响热烈&#xff0c;围绕四大赛道展开了激烈角逐&#xff0c;超过2000位社区开发者参与到飞桨黑客松中&#xff0c;完成800余次任务…

直播教学签到功能(互动功能接收端JS-SDK)

功能概述 本模块主要用于接收和处理讲师、助教和管理员等用户发起的签到操作。 初始化及销毁 在实例化该模块并进行使用之前&#xff0c;需要对SDK进行初始化配置&#xff0c;详细见参考文档。 在线文件引入方式 // script 标签引入&#xff0c;根据版本号引入JS版本。 <…

ChatGPT 和 Bing Chat两者之间的比较,看完你就懂了

目录 一、ChatGPT 1.1 介绍 1.2 特点 1.3 使用场景 二、 Bing Chat 2.1 介绍 2.2 功能特点 2.3 使用场景 三、对比 一、ChatGPT 1.1 介绍 ChatGPT是一款基于人工智能技术的语言模型应用&#xff0c;由美国人工智能研究实验室OpenAI在2022年11月30日推出。该模型是一种…

【深度学习】跌倒识别 Yolov5(带数据集和源码)从0到1,内含很多数据处理的坑点和技巧,收获满满

文章目录 前言1. 数据集1.1 数据初探1.2 数据处理1.3 训练前验证图片1.4 翻车教训和进阶知识 2. 训练3.效果展示 前言 又要到做跌倒识别了。 主流方案有两种&#xff1a; 1.基于关键点的识别&#xff0c;然后做业务判断&#xff0c;判断跌倒&#xff0c;用openpose可以做到。…

干货分享 | CloudQuery 数据保护能力之动态数据脱敏!

在企业数字化转型的过程中&#xff0c;尤其随着互联网、云计算、大数据等信息技术与通信技术的迅猛发展&#xff0c;海量数据在各种信息系统上被存储和处理&#xff0c;其中包含大量有价值的敏感数据&#xff0c;这意味着数据泄露的风险也不断增加。 数据泄露可能由各种因素引…

【项目】实现web服务器

目录 1.需要实现的项目需求&#xff08;web服务器的工作原理&#xff09; 2.实现过程&#xff1a; 1.编写套接字 2.多线程的代码和任务类 3.文件描述符的处理方法的框架 4.读取请求 4.1.读取请求行 4.2.读取请求报头 4.3.分析请求行和报头 请求行的方法、URI、版本…

桌面图标删不掉?试试这几个解决办法!

案例&#xff1a;我想对电脑桌面上的应用进行删除&#xff0c;但是我怎么删也删不掉应用的图标&#xff1f;有人知道这是怎么回事吗&#xff1f;怎样才能成功删除桌面图标&#xff1f;求一个解决办法&#xff01; 有时候我们可能会遇到桌面图标无法删除的困扰&#xff0c;桌面…

【已解决】Macbook pro/Macbook air 电脑过热问题(附软件下载地址)

问题&#xff1a; 今天早上一上班打开我的macbook air&#xff0c;刚开机了十来分钟&#xff0c;就觉得左上角位置特别的热&#xff0c;耳朵凑近风扇处&#xff0c;基本听不到风扇的声音&#xff0c;风扇的转速太慢&#xff0c;导致cpu温度堆积造成温度升高。 解决办法&#…

【JS】1705- 重学 JavaScript API - Fullscreen API

❝ 前期回顾&#xff1a; 1. Page Visibility API 2. Broadcast Channel API 3. Beacon API 4. Resize Observer API 5. Clipboard API 6. Fetch API 7. Performance API 8. WebStorage API 9. WebSockets API ❞ 本文中&#xff0c;我们将探索 Fullscreen API 的概念、使用方法…

SpringBoot+Vue 的简历招聘系统

文章目录 1、效果演示2、 前言介绍3、主要技术4 **系统设计**4.1 系统体系结构4.2开发流程设计4.3 数据库设计原则4.4 数据表 5 **系统详细设计**5.1管理员功能模块5.2用户功能模块5.3前台首页功能模块 6、源码获取 1、效果演示 2、 前言介绍 随着科学技术的飞速发展&#xff…