SLUB 代码笔记

数据结构

主要包含两个缓存对象(CPU的kmem_cache_cpu和内存节点numa的kmem_cache_node),还有一个描述本身slab数据的kmem_cache

CPU缓存

struct kmem_cache_cpu {
    void **freelist;    // 指向第一个空闲对象。
    struct page *page;  // 从哪个页面分配
    int node;       /* numa id */
    unsigned int offset;    /* 对象中指向下一个对象的偏移量 */
    unsigned int objsize;   /* 对象大小 */
    ...
};
  • freelist 是一个空闲链表,freelist指向第一个对象,对象中包含下一个节点;
  • offset 对象内存中指向下一个对象的偏移量,object = this->freelist; next = object[offset];

numa节点缓存

struct kmem_cache_node {
    spinlock_t list_lock;   /* Protect partial list and nr_partial */
    unsigned long nr_partial; // 有多少个部分空闲链表
    struct list_head partial; // partial是struct page链表
    ...
};

kmem_cache

kmem_cache保存着一个slub分配器相关的数据,包括一个对象的大小,名称,构造函数,每个CPU上的缓存等。

/*
 * Slab cache management.
 */
struct kmem_cache {
    /* Used for retriving partial slabs etc */
    unsigned long flags;
    int size;       /* 包含原数据的对象大小 */
    int objsize;    /* 不包含原数据的对象大小,就是最原始的申请的对象的大小 */
    int offset;     /* 一个对象中下一个空闲对象的存放的位置 */
    struct kmem_cache_order_objects oo; // oo中包含一个数字:unsigned long x,这个数字包含两个内容,一个是order,一个是一个slab中能容纳多少个对象。order数据表示一次分配页内存时,分配页的个数(1<<order)。

    /*
     * Avoid an extra cache line for UP, SMP and for the node local to
     * struct kmem_cache.
     * 为了避免再重新刷新到cache中保存的缓存
     */
    struct kmem_cache_node local_node;

    /* Allocation and freeing of slabs */
    struct kmem_cache_order_objects max;
    struct kmem_cache_order_objects min;
    gfp_t allocflags;   /* gfp flags to use on each alloc */
    int refcount;       /* Refcount for slab cache destroy */
    void (*ctor)(void *);
    int inuse;      /* Offset to metadata */
    int align;      /* 对齐 */
    unsigned long min_partial;
    const char *name;   /* 展示用的名称 */
    struct list_head list;  /* slab cache列表 */

#ifdef CONFIG_NUMA
    /*
     * 从远端内存节点上分配的时候,碎片整理
     * 在创建cache的时候,函数kmem_cache_open会设置为1000
     #ifdef CONFIG_NUMA
        s->remote_node_defrag_ratio = 1000;
    #endif
     */
    int remote_node_defrag_ratio;
    struct kmem_cache_node *node[MAX_NUMNODES];
#endif
#ifdef CONFIG_SMP
    struct kmem_cache_cpu *cpu_slab[NR_CPUS]; // 每个CPU对应一个cpu slab缓存
#else
    struct kmem_cache_cpu cpu_slab;
#endif
};

实现

分配内存

kmalloc/__kmalloc

void *kmalloc(size_t size, gfp_t flags)

如果size > SLUB_MAX_SIZE(2 * PAGE_SIZE),就使用kmalloc_large分配内存,否则使用slab分配slab_allockmalloc_large使用页分配,与slub无关,不记录。

slab_alloc

如果kmem_cache_cpu.freelist不为空切numa node id与要分配的节点匹配,那么直接成功返回,否则从__slab_alloc分配。

NOTE: kmem_cache_cpu上有一个freelist,kmem_cache_cpu.page上也有一个freelist。从__slab_alloc中可以看到,会把page.freelist迁移到kmem_cache_cpu上。

__slab_alloc

  1. 加slab锁

    struct kmem_cache_cpu *c;
    slab_lock(c->page);
  2. 判断node id是否匹配,不匹配会deactive当前kmem_cache_cpu,然后重新分配新的slab

  3. load_freelist。从kmem_cache_cpu.page.freelist中获取对象。

    c->freelist = object[c->offset];
    c->page->inuse = c->page->objects;
    c->page->freelist = NULL;
    c->node = page_to_nid(c->page);
  4. another_slab。freelist中没有,就deactive当前kmem_cache_cpu,然后分配新的slab。
  5. new_slab。先尝试从kmem_cache_node中获取partial slab的page,重新设置kmem_cache_cpu.page=new page。获取不到尝试分配新的slab,然后设置kmem_cache_cpu = get_cpu_slab(s, smp_processor_id()),然后重新加锁新的页面。如果还不成功,就会报错,slab_out_of_memory。上面两次尝试如果任意一次成功,就会回到第二步load_freelist
  6. 如果new_slab也分配失败,就会slab_out_of_memory,内存分配失败。
  7. 上面的步骤任一步导致结束的时候,都会解slab锁。

上面步骤简单汇总一下大致就是:

slab_lock page
kmem_cache_cpu.page.freelist
new_page from kmem_cache_node(get_partial)
new_slab from kmem_cache(new_slab@mm/slub.c)
slab_out_of_memory
slab_unlock page

步骤中提到的deactive当前kmem_cache_cpu:
1. 将freelist从kmem_cache_cpu.freelist释放到kmem_cache_cpu.page.freelist,然后将kmem_cache_cpu.page设置为NULL,等于取消关联。代码中注释说这种情况很少会出现,因为走到这里时,kmem_cache_cpu.freelist和kmem_cache_cpu.page.freelist通常都是空的。
2. unfreeze_slab。将一页还到list中去,这个函数要求调用的时候对应的页加上了slab锁,返回之前会解锁。 // TODO

get_partial

获取一个partial页,然后解锁返回。

static struct page *get_partial(struct kmem_cache *s, gfp_t flags, int node)
{
    struct page *page;
    int searchnode = (node == -1) ? numa_node_id() : node; // 按照numa id来查找

    /*
     get_partial_node会遍历kmem_cache_node上的partial list,然后
     尝试lock_and_freeze_slab,成功则返回这一页
     lock_and_freeze_slab会slab_trylock(page),成功的话再freeze page:
     __SetPageSlubFrozen(page)
     */
    page = get_partial_node(get_node(s, searchnode));
    if (page || (flags & __GFP_THISNODE))
        return page;

    /*
     遍历node_zonelist, 找到一个有partial的page
     */
    return get_any_partial(s, flags);
}

什么时候是无法分配对象

__kmalloc
    slab_alloc  // 忽略大内存情况(大块内存>2*PAGE_SIZE)
        __slab_alloc
            new_slab // 当前page缓存上没有,node上没有partial page
                allocate_slab
                    先尝试分配2^s->oo个页面,分配失败的话再尝试2^s->min个页面
                    alloc_slab_page
                        alloc_pages_node // 分配物理页
                        // 这个函数在内存紧张的时候会唤醒kswapd回收内存
            slab_out_of_memory // 最后可能会用这个函数打印日志

释放内存

释放内存从最直观的理解上,应该与申请内存类似的顺序。比如,先尝试释放到kmem_cache_cpu,不行就释放到page上。如果page上都空了,是否考虑释放到node上,没有空,就放到partial page列表中。

kfree

根据释放的对象所在的页,判断当前页是否有slab标识,如果没有直接使用put_page释放(根据一个地址可以直接找到对应的page)。否则使用slab_free释放。

slab_free

如果要释放的内存所在的page与当前kmem_cache_cpu中的page是同一个,那么可以直接释放到kmem_cache_cpu中(还有个前提:kmem_cache_cpu.node >= 0)。否则调用__slab_free释放,里面会调用__free_page释放页到伙伴系统。

__slab_free

  1. 把对象放到page.freelist上。
  2. 如果page还有人使用(PageSlubFrozen标识为true),那么直接解锁返回;
  3. 如果page上还有对象没有释放(page.inuse != 0),说明是partial page,使用add_partial加到kmem_cache_node的partial list中,然后解锁返回;
  4. 走到这里,说明page.inuse==0,page上的节点全都释放了,也没有kmem_cache_cpu使用。那么根据一开始page.freelist信息,判断page是不是partial,如果是,将kmem_cache_node.nr_partial减1。最后就是discard_slab,尝试释放掉这一页。如果slab的释放策略是SLAB_DESTROY_BY_RCU,那么就把page放到rcu队列中,否则就调用__free_slab释放page。

初始化

kmem_cache_init

kmem_cache有一个全局数组:

struct kmem_cache kmalloc_caches[SLUB_PAGE_SHIFT];

SLUB_PAGE_SHIFT在4K页面的机器上是14。

初始化的时候还没有任何kmem_cache创建出来,第一个就是为kmem_cache_node创建的,大小就是sizeof(kmem_cache_node)。
然后按照系统的一些信息,在64位系统上大致创建出来的有大小分别是8,16,32,64,…,16384(2^14)的cache。另外还会额外创建大小为96和192的cache,估计是这两个大小的对象会经常使用,如果没有这两个,会使用稍大一点的128和256,会浪费不少内存。

创建kmem_cache的函数是create_kmalloc_cache

create_kmalloc_cache

这个函数会调用kmem_cache_open初始化一些kmem_cache的基本参数,包括对象大小,min_partial参数,然后初始化kmem_cache_node和kmem_cache_cpu。在CONFIG_NUMA生效的时候设置s->remote_node_defrag_ratio = 1000;
最后将初始化完成的kmem_cache加入到slab_caches链表中。

kmem_cache_open

该函数负责初始化kmem_cache的一些基本参数。

static int kmem_cache_open(struct kmem_cache *s, gfp_t gfpflags,
        const char *name, size_t size,
        size_t align, unsigned long flags,
        void (*ctor)(void *))
  • name是cache的名称;
  • size就是原始对象大小;
  • align是对齐方式;
  • ctor是对象的构造函数;
  • flag是分配标识, 在初始化的时候是GFP_NOWAIT
    其中对齐方式是固定的:
#define ARCH_KMALLOC_MINALIGN __alignof__(unsigned long long)

kmem_cache中对给定的对象大小做一个运算,选择一个合适的大小作为分配时的对象大小。计算的函数是calculate_sizes。这里面会对分配对象大小、align和inuse、oo做计算,理解这里的计算对里面kmem_cache中的成员变量很有帮助。
kmem_cache.min_partial是log2(size),并且在[5,10]范围内。
接下来分别初始化kmem_cache_node(init_kmem_cache_nodes)和kmem_cache_cpu(alloc_kmem_cache_cpus)。
init_kmem_cache_nodes为每个内存节点初始化一个kmem_cache_node,然后创建一个slab给它。因为刚开始初始化时slab还不能用,所以slub使用变量slab_state表示初始化时slub的状态。而kmem_cache_node也是一个slub缓存,所以在创建第一个slab缓存kmem_cache_node时,slab_state是DOWN,之后就改成PARTIAL,使用early_kmem_cache_node_alloc分配内存。之后的node就使用kmem_cache_alloc_node创建,即正常的使用slub创建对象。
alloc_kmem_cache_cpus为kmem_cache的每个CPU分配一个对应的kmem_cache_cpu对象。

/*
 * calculate_sizes计算order和slab对象数据的分布
 */
static int calculate_sizes(struct kmem_cache *s, int forced_order)
{
    unsigned long flags = s->flags;
    unsigned long size = s->objsize;
    unsigned long align = s->align;
    int order;

    /*
     * 用指针大小对齐。空闲指针直接放在了对象内存中,对象内存只能放
     * 在指针对齐的内存上。
     */
    size = ALIGN(size, sizeof(void *));

    /*
     * 现在就已经知道了一个对象使用的真正大小就是inuse。这也可能是空闲
     * 指针的偏移量,就是kmem_cache.offset
     */
    s->inuse = size;

    // 在初始化的时候,flags给的是GFP_NOWAIT,s->ctor构造函数也是NULL
    // SLAB_DESTROY_BY_RCU: 释放slab到RCU队列上
    // SLAB_POISON:比较神奇的名字,毒害的slab对象,这是调试用的
    if (((flags & (SLAB_DESTROY_BY_RCU | SLAB_POISON)) ||
        s->ctor)) {
        /*
         * 重新定位空闲指针,放到对象的后面去,以免覆盖对象内存。
         */
        s->offset = size;
        size += sizeof(void *);
    }

    /*
     * 重新计算对齐,可以选择与cache line对齐,最少按照sizeof(long long)对齐
     */
    align = calculate_alignment(flags, align, s->objsize);
    s->align = align;

    size = ALIGN(size, align);
    s->size = size;
    if (forced_order >= 0) // 初始化时传的是-1
        order = forced_order;
    else
        order = calculate_order(size); // 没有指定就计算一个

    if (order < 0)
        return 0;

    s->allocflags = 0;
    if (order)
        s->allocflags |= __GFP_COMP;

    if (s->flags & SLAB_CACHE_DMA)
        s->allocflags |= SLUB_DMA;

    if (s->flags & SLAB_RECLAIM_ACCOUNT)
        s->allocflags |= __GFP_RECLAIMABLE;

    /*
     * 计算一个slab中可以容纳多少对象
     * oo中会保存这个slab的order和一个slab中容纳的对象个数
     */
    s->oo = oo_make(order, size);
    s->min = oo_make(get_order(size), size);
    if (oo_objects(s->oo) > oo_objects(s->max))
        s->max = s->oo;

    return !!oo_objects(s->oo);

}

其实里面有一个计算order的函数还是比较牛的,它的主要目的是为了避免分配内存过程中造成的一些碎片浪费。代码中的注释说order 0是首选的,因为在页分配器中不会造成碎片。但是大于一页的对象就不能用0,因为会有很多空间空下来。如果有1/16的slab会浪费的话,就会选择一个大点的order。详细可以看slab_order的代码注释。

收缩

收缩是为了防止slub占用过多的内存,或者有很多空的slub node,应该释放掉。
slub提供了接口kmem_cache_shrink用来收缩某个缓存。这个接口的功能很简单,先把所有CPU缓存上的slab放到node上(flush_slab),然后遍历缓存的所有kmem_cache_node,每个node上释放空的slab,然后把partial slab从大到小排序重新放到node的slab链表上。

NOTE: 什么时候收缩内存?
内存不足,比如分配物理页失败的时候,会调用kswapd。

参考阅读

  1. Linux slab 分配器剖析
  2. Linux SLUB 分配器详解
  3. chinaunix论坛讨论内存管理
  4. slab为什么要进行着色处理
  5. 每个程序员都应该了解的 CPU 高速缓存
  6. linux内存源码分析 - SLAB分配器概述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值