zoukankan      html  css  js  c++  java
  • linux slub分配器浅析

    在《linux内存管理浅析》中提到内核管理自己使用的内存时,使用了SLAB对象池。SLAB确实是比较复杂,所以一直以来都没有深入看一看。
    不过现在,linux内核中,SLAB已经被它的简化版--SLUB所代替。最近抽时间看了一下SLUB的代码,略记一些自己的理解。
    尽管SLUB是在内核里面实现的,用户态的对象池其实也可以借鉴这样的做法。

    SLUB的总体思想还是跟SLAB类似,对象池里面的内存都是以“大块”为单位来进行分配与回收的。然后每个“大块”又按对象的大小被分割成“小块”,使用者对于对象的分配与回收都是以“小块”为单位来进行的。

    SLUB的结构如下图:


    另外,kmem_cache还有以下一些参数(后面会解释到):
    int size;         /* 每个对象占用的空间 */
    int objsize;      /* 对象的大小 */
    int offset;       /* 对象所占用的空间中,存放next指针的偏移 */
    int refcount;     /* 引用计数 */
    int inuse;        /* 对象除next指针外所占用的大小 */
    int align;        /* 对齐字节数 */
    void (*ctor)(void *); /* 构造函数 */
    unsigned long min_partial; /* kmem_cache_node中保存的最小page数 */
    struct kmem_cache_order_objects oo; /* 首选的page分配策略(分配多少个连续页面,能划分成多少个对象) */
    struct kmem_cache_order_objects min; /* 次选的page分配策略 */
    (另外还有一些成员,或支持了一些选项、或支持了DEBUG、或是为周期性的内存回收服务。这里就不列举了。)

     

    大体结构

    kmem_cache是对象池管理器,每一个kmem_cache管理一种类型的对象。所有的管理器通过双向链表由表头slab_caches串连起来。
    这一点跟之前的SLAB是一样的。

    kmem_cache的内存管理工作是通过成员kmem_cache_node来完成的。在NUMA环境下(非均质存储结构),一个kmem_cache维护了一组kmem_cache_node,分别对应每一个内存节点。kmem_cache_node只负责管理对应内存节点下的内存。(如果不是 NUMA环境,那么kmem_cache_node只有一个。)

    而实际的内存还是靠page结构来管理的,kmem_cache_node通过partial指针串连起一组page(nr_partial代表链表长度),它们就代表了对象池里面的内存。page结构不仅代表了内存,结构里面还有一些union变量用来记录其对应内存的对象分配情况(仅当page被加入到SLUB分配器后有效,其他情况下这些变量有另外的解释)。
    原先的SLAB则要复杂一些,SLAB里面的page仅仅是管理内存,不维护“对象”的概念。而是由额外的SLAB控制结构(slab)来管理对象,并通过slab结构的一些指针数组来划定对象的边界。

    前面说过,对象池里面的内存是以“大块”为单位来进行分配与回收的,page就是这样的大块。page内部被划分成若干个小块,每一块用于容纳一个对象。这些对象是以链表的形式来存储的,page->freelist就是链表头,只有未被分配的对象才会放在链表中。对象的next指针存放在其偏移量为kmem_cache->offset的位置。(见上面的图)
    而在SLAB中,“大块”则是提供控制信息的slab结构。page结构只表示内存,它仅是slab所引用的资源。

    每一个page并不只代表一个页面,而是2^order个连续的页面,这里的order值是由kmem_cache里面的oo或min来确定的。分配页面时,首先尝试使用oo里面的order值,分配较合适大小的连续页面(这个值是在kmem_cache创建的时候计算出来的,使用这个值时需要分配一定的连续页面,以使得内存分割成“小块”后剩余的边角废料较少)。如果分配不成功(运行时间长了,内存碎片多了,分配大量连续页面就不容易了),则使用min里面的order值,分配满足对象大小的最少量的连续页面(这个值也是创建kmem_cache时计算出来的)。

    kmem_cache_node通过partial指针串连的一组page,这些page必须是没被占满的(一个page被划分成page->objects个对象大小的空间,这些空间中有page->inuse个已经被使用。如果page->objects==page->inuse,则page为full)。如果一个page为full,则它会被从链表中移除。而如果page是free的(page->inuse==0),一般情况下它也会被释放,除非这里的nr_partial(链表长度)小于kmem_cache里面的min_partial。(既然是池,就应该有一定的存量,min_partial就代表最低存量。这个值也是在创建kmem_cache时计算出来的,对象的size较大时,会得到较大的min_partial值。因为较大的size值在分配page时会需要更多连续页面,而分配连续页面不如单个的页面容易,所以应该多缓存一些。)
    而原先的SLAB则有三个链表,分别维护“full”、“partial”、“free”的slab。“free”和“partial”在SLUB里面合而为一,成了前面的partial链表。而“full”的page就不维护了。其实也不需要维护,因为page已经full了,不能再满足对象的分配,只能响应对象的回收。而在对象回收时,通过对象的地址就能得到对应的page结构(page结构的地址是与内存地址相对应的,见《linux内存管理浅析》)。维护full的page可以便于查看分配器的状态,所以在DEBUG模式下,kmem_cache_node里面还是会提供一个full链表。

    分配与释放

    对象的分配与释放并不是直接在kmem_cache_node上面操作的,而是在kmem_cache_cpu上。一个kmem_cache维护了一组kmem_cache_cpu,分别对应系统中的每一个CPU。kmem_cache_cpu相当于为每一个CPU提供了一个分配缓存,以避免CPU总是去kmem_cache_node上面做操作,而产生竞争。并且kmem_cache_cpu能让被它缓存的对象固定在一个CPU上,从而提高CPU的cache命中率。kmem_cache_cpu只提供了一个page的缓存。

    原先的SLAB是为每个CPU提供了一个array_cache结构来缓存对象。对象在array_cache结构中的组织形式跟它在slab中的组织形式是不一样的,这也就增加了复杂性。而SLUB则都是通过page结构来组织对象的,组织形式都一样。

    进行对象分配的时候,首先尝试在kmem_cache_cpu上去分配。如果分配不成功,再去kmem_cache_node上move一个page到kmem_cache_cpu上面来。分配不成功的原因有两个:kmem_cache_cpu上的page已经full了、或者现在需要分配的node跟kmem_cache_cpu上缓存page对应的node不相同。对于page已full的情况,page被从kmem_cache_cpu上移除掉(或者DEBUG模式下,被移动到对应kmem_cache_node的full链表上);而如果是node不匹配的情况,则kmem_cache_cpu上缓存page会先被move回到其对应kmem_cache_node的partial链表上(再进一步,如果page是free的,且partial链表的长度已经不小于min_partial了,则page被释放)。
    反过来,释放对象的时候,通过对象的地址能找到它所对应的page的地址,将对象放归该page即可。但是里面也有一些特殊逻辑,如果page正被kmem_cache_cpu缓存,就没有什么需要额外处理的了;否则,在将对象放归page时,需要对page加锁(因为其他CPU也可能正在该page上分配或释放对象)。另外,如果对象在回收之前该page是full的,则对象释放后该page就成partial的了,它还应该被添加到对应的kmem_cache_node的partial链表中。而如果对象回收之后该page成了free的,则它应该被释放掉。
    对象的释放还有一个细节,既然对象会放回到对应的page上去,那如果这个page正在被其他的CPU cache呢(其他CPU的kmem_cache_cpu正指使用这个page)?其实没关系,kmem_cache_cpu和page各自有一个freelist指针,当page被一个CPU cache时,page的freelist上的所有对象全部移动到kmem_cache_cpu的freelist上面去(其实就是一个指针赋值),page的freelist变成NULL。而释放的时候是释放到page的freelist上去。两个freelist互不影响。但是这个地方貌似有个问题,如果一个被cache的page的freelist由于对象的释放而变成非NULL,那么这个page就可能再被cache到其他CPU的kmem_cache_cpu上面去,于是多个kmem_cache_cpu可能cache同一个page。这将导致一个CPU内部的缓存可能cache到其他CPU上的对象(因为CPU缓存跟对象并不是对齐的),从而一个CPU上的对象写操作可能引起另一个CPU的缓存失效。

    在kmem_cache被创建的时候,SLUB会根据各种各样的信息,计算出对象池的合理布局(见上面的图)。objsize是对象本身的大小;这个大小经过对齐处理以后就成了inuse;紧贴inuse的后面可能会存放对象的next指针(由offset来标记),从而将对象实际占用的大小扩大到size。
    其实,这里的offset并不总是指向inuse后面的位置(否则offset就可以用inuse来代替了)。offset有两种可能的取值,一是 inuse、一是0。这里的offset指定了next指针的位置,而next是为了将对象串连在空闲链表中。那么,需要用到next指针的时候,对象必定是空闲的,对象里面的空间是未被使用的。于是正好,对象里的第一个字长的空间就拿来当next指针好了,此时offset就是0。但是在一些特殊情况下,对象里面的空间不能被复用作next指针,比如对象提供了构造函数ctor,那么对象的空间是被构造过的。此时,offset就等于inuse,next指针只好存放在对象的空间之后。

    关于kmem_cache_cpu

    前面在讲对象分配与释放的时候,着重讲的是过程。下面再细细分析一下kmem_cache_cpu的作用。

    如果没有kmem_cache_cpu,那么分配对象的过程就应该是:
    1、从对应的kmem_cache_node的partial链表里面选择一个page;
    2、从选中的page的freelist链表里面选择一个对象;
    这就是最基本的分配过程。

    但是这个过程有个不好的地方,kmem_cache_node的partial链表是全局的、page的freelist链表也是全局的:
    1、第一步访问partial链表的时候,需要上锁;
    2、第二步访问page->freelist链表的时候,也需要上锁;
    3、对象可能刚在CPU1上被释放,又马上被CPU2分配走。不利于CPU cache;

    引入kmem_cache_cpu就是对这一问题的优化。每个CPU各自对应一个kmem_cache_cpu实例,用于缓存第一步中选中的那个page。这样一来,第一步就不需要上锁了;而page中的对象在一段时间内也将趋于在同一个CPU上使用,有利于CPU cache。
    而kmem_cache_cpu中的freelist则是为了避免第二步的上锁。

    假设没有kmem_cache_cpu->freelist,而page->freelist初始时有1、2、3、4,四个对象。考虑如下事件序列:
    1、page被CPU1所cache,然后1、2被分配;
    2、由于在CPU1上请求的node id与该page不匹配,page被放回kmem_cache_node的partial链表,那么此时page->freelist还剩3和4两个对象;
    3、page又被CPU2所cache(page在上一步已经被放回partial链表了)。
    此时,page->freelist就有可能被CPU1和CPU2两个CPU所访问,当对象1或2被释放时(这两个对象已经分配给了CPU1),CPU1会访问page->freelist;而显然CPU2分配对象时也要会访问page->freelist。

    所以为了避免上锁,kmem_cache_cpu要维护自己的freelist,把page->freelist下的对象都接管过来。
    这样一来,CPU1就只跟page->freelist打交道,CPU2跟kmem_cache_cpu的freelist打交道,就不需要上锁了。

     

    关于page同时被多CPU使用

    前面还提到,从属于同一个page的对象可能cache到不同CPU上,从而可能对CPU的缓存造成一定的影响。不过这似乎也是没办法的事情。

    首先,初始状态下,page加入到slub之后,从属于该page的对象都是空闲的,都存在于page->freelist中;
    然后,这个page可能被某个CPU的kmem_cache_cpu所cache,假设是CPU-0,那么这个kmem_cache_cpu将得到属于该page的所有对象。 page->freelist将为空;
    接下来,这个page下的一部分对象可能在CPU-0上被分配出去;
    再接着,可能由于NUMA的node不匹配,这个page从CPU-0的kmem_cache_cpu上面脱离下来。这时page->freelist将保存着那些未被分配出去的对象(而其他的对象已经在CPU-0上被分配出去了);

    这时,从属于该page的一部分对象正在CPU-0上被使用着,另一部分对象存在于page->freelist中。
    那么,现在就有两个选择:
    1、不将这个page放回partial list,阻止其他CPU使用这个page;
    2、将这个page放回partial list,允许其他CPU使用这个page;

    对于第一种做法,可以避免属于同一个page的对象被cache到不同CPU。但是这个page必须等到CPU-0再次cache它以后才能被继续使用;或者等待CPU-0所使用的从属于这个page的对象全都被释放,然后这个page才能被放回partial list或者直接被释放掉。
    这样一来,一个page尽管拥有空闲的对象,却可能在一定时间内处于不可用状态(极端情况是永远不可用)。这样实现的系统似乎不太可控……

    而现在的slub选择了第二种做法,将page放回partial list,于是page马上就能被其他CPU使用起来。那么,由此引发的从属于同一个page的对象被cache到不同CPU的问题,也就是没办法的事情……

     

    vs slab

    相比SLAB,SLUB还有一个比较有意思的特性。当创建新的对象池时,如果发现原先已经创建的某个kmem_cache的size刚好等于或略大于新的size,则新的kmem_cache不会被创建,而是复用这个大小差不多kmem_cache。所以kmem_cache里面还维护了一个refcount(引用计数),表示它被复用的次数。

    另外,SLUB也去掉了SLAB中很有意思的一个特性,Coloring(着色)。
    什么是着色呢?一个内存“大块”,在按对象大小划分成“小块”的时候,可能并不是那么刚好,还会空余一些边边角角。着色就是利用这些边边角角来做文章,使得“小块”的起始地址并不总是等于“大块”内的0地址,而是在0地址与空余大小之间浮动。这样就使得同一种类型的各个对象,其地址的低几位存在更多的变化。
    为什么要这样做呢?这是考虑到了CPU的cache。在学习操作系统原理的时候我们都听说过,为提高CPU对内存的访存效率,CPU提供了cache。于是就有了从内存到cache之间的映射。当CPU指令要求访问一个内存地址的时候,CPU会先看看这个地址是否已经被缓存了。
    内存到cache的映射是怎么实现的呢?或者说CPU怎么知道某个内存地址有没有被缓存呢?
    一种极端的设计是“全相连映射”,任何内存地址都可以映射到任何的cache位置上。那么CPU拿到一个地址时,它可能被缓存的cache位置就太多了,需要维护一个庞大的映射关系表,并且花费大量的查询时间,才能确定一个地址是否被缓存。这是不太可取的。
    于是,cache的映射总是会有这样的限制,一个内存地址只可以被映射到某些个cache位置上。而一般情况下,内存地址的低几位又决定了内存被cache的位置(如:cache_location = address % cache_size)。
    好了,回到SLAB的着色,着色可以使同一类型的对象其低几位地址相同的概率减小,从而使得这些对象在cache中映射冲突的概率降低。
    这有什么用呢?其实同一种类型的很多对象被放在一起使用的情况是很多的,比如数组、链表、vector、等等情况。当我们在遍历这些对象集合的时候,如果每一个对象都能被CPU缓存住,那么这段遍历代码的处理效率势必会得到提升。这就是着色的意义所在。
    SLUB把着色给去掉了,是因为对内存使用更加抠门了,尽可能的把边边角角减少到最小,也就干脆不要着色了。还有就是,既然kmem_cache可以被size差不多的多种对象所复用,复用得越多,着色也就越没意义了。
  • 相关阅读:
    cf1100 F. Ivan and Burgers
    cf 1033 D. Divisors
    LeetCode 17. 电话号码的字母组合
    LeetCode 491. 递增的子序列
    LeetCode 459.重复的子字符串
    LeetCode 504. 七进制数
    LeetCode 3.无重复字符的最长子串
    LeetCode 16.06. 最小差
    LeetCode 77. 组合
    LeetCode 611. 有效三角形个数
  • 原文地址:https://www.cnblogs.com/hehehaha/p/6332799.html
Copyright © 2011-2022 走看看