• SLUB分配一个object的流程分析


    SLUB分配一个object的流程分析 

      上篇文章已经说了性能瓶颈显示为slub,所以这篇文章来看看

    转载:

    在上一节 我们清晰的知道了当调用kmem_cache_create之后系统会为我们分配一个名为slub_test的一个slab。这时候只是分配了kmem_cache,kmem_cache_cpu,kmem_cache_node结构,同时设置针对此object需要多少个page之类。

    我们这节将分析当申请一个object的时候,应该是如何的分配。还是之前的例子,继续来分析当调用kmem_cache_alloc函数之后,代码的关键流程。

    test= kmem_cache_alloc(slub_test, GFP_KERNEL);
    if(test!= NULL){
        printk("alloc object success!\n");
        ret = 0;
    }

    通过kmem_cache_alloc函数最终会调用到slab_alloc函数

    static __always_inline void *slab_alloc(struct kmem_cache *s,
            gfp_t gfpflags, unsigned long addr)
    {
        return slab_alloc_node(s, gfpflags, NUMA_NO_NODE, addr);
    }
     
    void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)
    {
        void *ret = slab_alloc(s, gfpflags, _RET_IP_);
     
        trace_kmem_cache_alloc(_RET_IP_, ret, s->object_size,
                    s->size, gfpflags);
     
        return ret;
    }
    • 参数s:就是我们创建好的slab
    • gfpflasg: 就是分配内存时的一些掩码,比如我们kmalloc经常使用的是GFP_KERNEL
    static __always_inline void *slab_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node, unsigned long addr)
    {
        void *object;
        struct kmem_cache_cpu *c;
        struct page *page;
        unsigned long tid;
     
    redo:
     
        do {
            tid = this_cpu_read(s->cpu_slab->tid);
            c = raw_cpu_ptr(s->cpu_slab);
        } while (IS_ENABLED(CONFIG_PREEMPT) &&
             unlikely(tid != READ_ONCE(c->tid)));
     
        object = c->freelist;
        page = c->page;
        if (unlikely(!object || !node_match(page, node))) {
            object = __slab_alloc(s, gfpflags, node, addr, c);
            stat(s, ALLOC_SLOWPATH);
        } else {
            void *next_object = get_freepointer_safe(s, 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;
            }
            prefetch_freepointer(s, next_object);
            stat(s, ALLOC_FASTPATH);
        }
     
        if (unlikely(gfpflags & __GFP_ZERO) && object)
            memset(object, 0, s->object_size);
     
        slab_post_alloc_hook(s, gfpflags, 1, &object);
     
        return object;
    }
    • 这个函数分为快车道和慢车道。快车道就是当前cpu上的kmem_cache_cpu里的freelist有可用的object,有的话直接分配此object。慢车道就是:当前cpu的freelist中没有可用的object。我们第一次申请object则进入的就是慢车道、
    • 快车道很简单:直接从kmem_cache_cpu的freelist中获取一个object返回即可。慢车道就比较麻烦,需要申请page,然后根据page大小设置freelist的指针等。重点关注下慢车道。
    • 确保当前在同一个cpu上操作,在开启抢占的情况下。
    • 获取当前cpu的freelist链表,以及page。当第一次分配object的时候这两值都为null,则就进入到__slab_alloc函数中。设置状态为ALLOC_SLOWPATH慢速分配。
    • 如果是快速分配,则通过get_freepointer_safe下一个object的指针next_object
    • 通过this_cpu_cmpxchg_double函数重新设置freelist的指针,以及tid
    • 如果flag存在__GFP_ZERO,则将此object清为0即可。

    我们现在总结下分配一个object需要经历的4中选择:

    1. 先从kmem_cache_cpu→ freelist中分配,如果freelist为null
    2. 接着去kmem_cache_cpu→partital链表中分配,如果此链表为null
    3. 接着去kmem_cache_node→partital链表分配,如果此链表为null
    4. 这就需要重新分配一个slab了。

    接下来分为四个步骤去分析各个情况下的分配object

    从kmem_cache_cpu→freelist中分配

    这种分配就是我们前面提到的快车道分配,操作很简单,直接获取freelist所指的object,然后计算下一个object。重新设置freelist和tid的值即可。

     

    从kmem_cache_cpu→partital中分配

    • 第一步将kmem_cache_cpu->partital赋值给kmem_cache_cpu→page节点
    • 第二步kmem_cache_cpu→partial = kmem_cache_cpu→page->next,这样一来partital就指向下一个page
    • 对应的代码如下
    #define slub_percpu_partial(c)      ((c)->partial)
     
    #define slub_set_percpu_partial(c, p)       \
    ({                      \
        slub_percpu_partial(c) = (p)->next;  \
    })
     
     
    if (slub_percpu_partial(c)) {
            page = c->page = slub_percpu_partial(c);
            slub_set_percpu_partial(c, page);
            stat(s, CPU_PARTIAL_ALLOC);
            goto redo;
    }
    • 将page中的freelist设置给kmem_cache_cpu的freelist
    • 将page→freelist设置为NULL
    • 然后和快速车道一样,设置下一个freelist的指针,以及tid,返回当前的object
    • 代码如下:
    static inline void *get_freelist(struct kmem_cache *s, struct page *page)
    {
        struct page new;
        unsigned long counters;
        void *freelist;
     
        do {
            freelist = page->freelist;
            counters = page->counters;
     
            new.counters = counters;
            VM_BUG_ON(!new.frozen);
     
            new.inuse = page->objects;
            new.frozen = freelist != NULL;
     
        } while (!__cmpxchg_double_slab(s, page,
            freelist, counters,
            NULL, new.counters,
            "get_freelist"));
     
        return freelist;
    }
     
    load_freelist:
        VM_BUG_ON(!c->page->frozen);
        c->freelist = get_freepointer(s, freelist);
        c->tid = next_tid(c->tid);
        return freelist;

    至此从kmem_cache_cpu的partial链表中获取object完毕了。

     

    从kmem_cache_node→partital中分配

    • 当kmem_cache_cpu的freelist和partital链表都没有可用的object的时候,就去kmem_cache_node去寻找可用的object
    • 将kmem_cache_node中的page→freelist设置为null,然后将此page从lru链表去remove掉
    • 将remove掉的page设置到kmem_cache_cpu的page中
    • 设置kmem_cache_cpu的freelist到当前从kmem_cache_node remove的freelist中去
    • 涉及的代码如下:
    static void *get_partial_node(struct kmem_cache *s, struct kmem_cache_node *n,
                    struct kmem_cache_cpu *c, gfp_t flags)
    {
        struct page *page, *page2;
        void *object = NULL;
        unsigned int available = 0;
        int objects;
     
        /*
         * Racy check. If we mistakenly see no partial slabs then we
         * just allocate an empty slab. If we mistakenly try to get a
         * partial slab and there is none available then get_partials()
         * will return NULL.
         */
        if (!n || !n->nr_partial)
            return NULL;
     
        spin_lock(&n->list_lock);
        list_for_each_entry_safe(page, page2, &n->partial, lru) {
            void *t;
     
            if (!pfmemalloc_match(page, flags))
                continue;
     
            t = acquire_slab(s, n, page, object == NULL, &objects);
            if (!t)
                break;
     
            available += objects;
            if (!object) {
                c->page = page;
                stat(s, ALLOC_FROM_PARTIAL);
                object = t;
            } else {
                put_cpu_partial(s, page, 0);
                stat(s, CPU_PARTIAL_NODE);
            }
            if (!kmem_cache_has_cpu_partial(s)
                || available > slub_cpu_partial(s) / 2)
                break;
     
        }
        spin_unlock(&n->list_lock);
        return object;
    } 

    重新分配一个slab

    终于经过了千方百计的救援,依旧没有找到可用的slab,则就通过new_slab函数重新分配一个新的slab。

    static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
    {
        struct page *page;
        struct kmem_cache_order_objects oo = s->oo;
        gfp_t alloc_gfp;
        void *start, *p;
        int idx, order;
        bool shuffle;
     
        flags &= gfp_allowed_mask;
     
        if (gfpflags_allow_blocking(flags))
            local_irq_enable();
     
        flags |= s->allocflags;
     
        /*
         * Let the initial higher-order allocation fail under memory pressure
         * so we fall-back to the minimum order allocation.
         */
        alloc_gfp = (flags | __GFP_NOWARN | __GFP_NORETRY) & ~__GFP_NOFAIL;
        if ((alloc_gfp & __GFP_DIRECT_RECLAIM) && oo_order(oo) > oo_order(s->min))
            alloc_gfp = (alloc_gfp | __GFP_NOMEMALLOC) & ~(__GFP_RECLAIM|__GFP_NOFAIL);
     
        page = alloc_slab_page(s, alloc_gfp, node, oo);
        if (unlikely(!page)) {
            oo = s->min;
            alloc_gfp = flags;
            /*
             * Allocation may have failed due to fragmentation.
             * Try a lower order alloc if possible
             */
            page = alloc_slab_page(s, alloc_gfp, node, oo);
            if (unlikely(!page))
                goto out;
            stat(s, ORDER_FALLBACK);
        }
     
        page->objects = oo_objects(oo);
     
        order = compound_order(page);
        page->slab_cache = s;
        start = page_address(page);
     
        if (unlikely(s->flags & SLAB_POISON))
            memset(start, POISON_INUSE, PAGE_SIZE << order);
     
        shuffle = shuffle_freelist(s, page);
     
        if (!shuffle) {
            for_each_object_idx(p, idx, s, start, page->objects) {
                setup_object(s, page, p);
                if (likely(idx < page->objects))
                    set_freepointer(s, p, p + s->size);
                else
                    set_freepointer(s, p, NULL);
            }
            page->freelist = fixup_red_left(s, start);
        }
     
        page->inuse = page->objects;
        page->frozen = 1;
     
        return page;
    }
    • 通过设置flag去申请page,同时要根据object的需要的order去申请page
    • 调用此函数alloc_slab_page去申请一页,至于怎么申请的我们在后面的buddy内容里详细描述
    • 假如现在申请失败了,看看他还是不放弃,还要在尝试一次,去更低的order去申请page,如果再次失败,则宣布退出。如果申请成功,我们就拿到这一页page
    • 获取到slab的object数,以及设置当前page对应的kmem_cache,获取到page的开始地址,然后如果开机SLAB_POISON flasg,则设置申请的page内容初始化为0x5a, 用于debug使用
    • 如果开启了随机的的freelist,随机的freelist的意思就是下一个object的地址是随机的。则会进到shuffle_freelist设置各个object的地址,形成一个单链表
    • 如果没有开启的,则会通过外面的一个for循环,设置下一个object的地址,下一个object的地址就等于curr+object_size
    • page->inuse = page→objects; 就是我们在第一节的时候说刚开始创建的object insue=objects的
    • 返回当前申请好的page
    static inline void *new_slab_objects(struct kmem_cache *s, gfp_t flags,
                int node, struct kmem_cache_cpu **pc)
    {
        void *freelist;
        struct kmem_cache_cpu *c = *pc;
        struct page *page;
     
        WARN_ON_ONCE(s->ctor && (flags & __GFP_ZERO));
     
        freelist = get_partial(s, flags, node, c);
     
        if (freelist)
            return freelist;
     
        page = new_slab(s, flags, node);
        if (page) {
            c = raw_cpu_ptr(s->cpu_slab);
            if (c->page)
                flush_slab(s, c);
     
            /*
             * No other reference to the page yet so we can
             * muck around with it freely without cmpxchg
             */
            freelist = page->freelist;
            page->freelist = NULL;
     
            stat(s, ALLOC_SLAB);
            c->page = page;
            *pc = c;
        } else
            freelist = NULL;
     
        return freelist;
    }
    • 将申请好的page的freelist给返回去给kmem_cache_cpu的freelist,设置当前page的freelist为NULL
    • 然后将当前page设置给kmem_cache_cpu的page
    • 至此当申请一个page的时候 各个情况就说明完了

    ps:

     this_cpu_cmpxchg_double(pcp1, pcp2, oval1, oval2, nval1, nval2)   

           类似于cmpxchg_double,能够避免禁止中断.

    __slab_alloc()的分配稍后分析,现在看一下else分支的动作。其先经get_freepointer_safe()取得slab中空闲对象地址,接着使用this_cpu_cmpxchg_double()原子指令操作取得该空闲对象,如果获取成功将使用prefetch_freepointer()刷新数据,否则将经note_cmpxchg_failure()记录日志后重回redo标签再次尝试分配。这里面的关键是this_cpu_cmpxchg_double()原子指令操作。该原子操作主要做了三件事情:1)重定向首指针指向当前CPU的空间;2)判断tid和freelist未被修改;3)如果未被修改,也就是相等,确信此次slab分配未被CPU迁移,接着将新的tid和freelist数据覆盖过去以更新。

    具体将this_cpu_cmpxchg_double()的功能展开用C语言表述就是:

    if
    ((__this_cpu_ptr(s->cpu_slab->freelist) == object) &&
    (__this_cpu_ptr(s->cpu_slab->tid) == tid))
    
    {
    
        __this_cpu_ptr(s->cpu_slab->freelist)
    = next_object;
    
        __this_cpu_ptr(s->cpu_slab->tid) =
    next_tid(tid);
    
        return true;
    
    }
    
    else
    
    {
    
        return false;
    
    }

    这里使用原子操作,其通过单指令方式实现完以上功能免除了加锁解锁操作,且完全避免了多核的情况下CPU迁移锁资源等待所带来的性能开销,极大地提升了效率。这在通常的程序开发中消除性能瓶颈也是极佳的手段。

     

      _slab_alloc()是slab申请的慢路径,这是由于freelist是空的或者需要执行调试任务。

      该函数会先行local_irq_save()禁止本地处理器的中断并且记住它们之前的状态。如果配置CONFIG_PREEMPT了,为了避免因调度切换到不同的CPU,该函数会重新通过this_cpu_ptr()获取CPU域的指针;如果c->page为空,也就是cpu local slab不存在就经由new_slab分支新分配一个。

      当c->page不为空的情况下,会经node_match()判断页面与节点是否匹配,如果节点不匹配就通过deactivate_slab()去激活cpu本地slab;再然后通过pfmemalloc_match()判断当前页面属性是否为pfmemalloc,如果不是则同样去激活。

      接着会再次检查空闲对象指针freelist是否为空,避免在禁止本地处理器中断前因发生了CPU迁移或者中断,导致本地的空闲对象指针不为空。如果不为空的情况下,将会跳转至load_freelist,这里将会把对象从空闲队列中取出,并更新数据信息,然后恢复中断使能,返回对象地址。如果为空,将会更新慢路径申请对象的统计信息,并通过get_freelist()从页面中获取空闲队列。if (!freelist)表示获取空闲队列失败,此时则需要创建新的slab,否则更新统计信息进入load_freelist分支取得对象并返回。

      最后看一下该函数的new_slab分支的实现,首先会if (c->partial)判断partial是否为空,不为空则从partial中取出,然后跳转回redo重试分配。如果partial为空,意味着当前所有的slab都已经满负荷使用,

    那么则需使用new_slab_objects()创建新的slab。如果创建失败,那么将if (!(gfpflags &__GFP_NOWARN) && printk_ratelimit())判断申请页面是否配置为无告警,并且送往控制台的消息数量在临界值内,

      则调用slab_out_of_memory()记录日志后使能中断并返回NULL表示申请失败。

    否则将会if (likely(!kmem_cache_debug(s) && pfmemalloc_match(page,gfpflags)))判断是否未开启调试且页面属性匹配pfmemalloc,是则跳转至load_freelist分支进行slab对象分配;

    否则将会经if (kmem_cache_debug(s) && !alloc_debug_processing(s, page,freelist, addr)) 判断,

      若开启调试并且调试初始化失败,则返回创建新的slab。如果未开启调试或page调试初始化失败,都将会deactivate_slab()去激活该page,使能中断并返回

     

    参考:https://www.jeanleo.com/2018/09/07/%e3%80%90linux%e5%86%85%e5%ad%98%e6%ba%90%e7%a0%81%e5%88%86%e6%9e%90%e3%80%91slub%e5%88%86%e9%85%8d%e7%ae%97%e6%b3%95%ef%bc%884%ef%bc%89/

     参考:http://www.wowotech.net/memory_management/426.html

  • 相关阅读:
    从底层谈WebGIS 原理设计与实现(六):WebGIS中地图瓦片在Canvas上的拼接显示原理
    从底层谈WebGIS 原理设计与实现(五):WebGIS中通过行列号来换算出多种瓦片的URL 之在线地图
    从底层谈WebGIS 原理设计与实现(四):WebGIS中通过行列号来换算出多种瓦片的URL 之离线地图
    从底层谈WebGIS 原理设计与实现(三):WebGIS前端地图显示之根据地理范围换算出瓦片行列号的原理(核心)
    从底层谈WebGIS 原理设计与实现(二):探究本质,WebGIS前端地图显示之地图比例尺换算原理
    [leetcode]Rotate List
    [leetcode]Remove Element
    [leetcode]Binary Tree Level Order Traversal II
    [leetcode]Populating Next Right Pointers in Each Node II
    [leetcode]Construct Binary Tree from Inorder and Postorder Traversal
  • 原文地址:https://www.cnblogs.com/codestack/p/16107533.html
Copyright © 2020-2023  润新知