linux内存源码分析 - SLAB分配器概述

基于linux 2.6.34

 之前说了管理区页框分配器,这里我们简称为页框分配器,在页框分配器中主要是管理物理内存,将物理内存的页框分配给申请者,而且我们知道也可页框大小为4K(也可设置为4M),这时候就会有个问题,如果我只需要1KB大小的内存,页框分配器也不得不分配一个4KB的页框给申请者,这样就会有3KB被白白浪费掉了。为了应对这种情况,在页框分配器上一层又做了一层SLAB层,SLAB分配器的作用就是从页框分配器中拿出一些页框,专门把这些页框拆分成一小块一小块的小内存,当申请者申请的是小内存时,系统就会从SLAB中获取一小块分配给申请者。它们的整个关系如下图:
 这里写图片描述
 可以看出,SLAB分配器和页框分配器并没有什么直接的联系,对于页框分配器来说,SLAB分配器也只是一个从它那里申请页框的申请者而已。
 

  在SLAB分配器中将SLAB分为两大类:专用SLAB和普通SLAB。专用SLAB用于特定的场合(比如TCP有自己专用的SLAB,当TCP模块需要小内存时,会从自己的SLAB中分配),而普通SLAB就是用于常规分配的时候。我们可以使用命令查看SLAB的状态

cat /proc/slabinfo
 命令结果如下:
这里写图片描述
这里写图片描述

  如刚才所有,我们看到有些SLAB的名字比较特别,如TCP,UDP,dquot这些,它们都是专用SLAB,专属于它们自己的模块。而后面这张图,如kmalloc-8,kmalloc-16…还有dma-kmalloc-96,dma-kmalloc-192…这些都是普通SLAB,当需要为一些小数据分配内存时(比如一个结构体),就会从这些普通SLAB中获取内存。值得注意的是,对于kmalloc-8这些普通SLAB,都有一个对应的dma-kmalloc-8这种类型的普通SLAB,这种类型是专门使用了ZONE-DMA区域的内存,方便用于DMA模式申请内存。

  在SLAB中,可分配的内存块称之为对象,在后面那张图中,如kmalloc-8这个普通SLAB,里面所有的对象都是8B大小,同理,kmalloc-16中的对象都是以16B为大小。当你申请1B~8B的内存时,系统会从kmalloc-8中分配一个对象给你,当你申请8B~16B的内存时,系统会从kmalloc-16里给你分配。虽然即使申请5B,分配了一个8B的对象,还有3B空闲,但这样设计已经大大减小了内存碎片化了,保证了碎片内存不会超过50%(kmalloc-8除外)。需要注意,在kmalloc-8中申请到的对象,释放时也会回到kmalloc-8中。

  除了减小了内存碎片化,SLAB还有一个作用,提高了系统的效率,当对象拥有者释放一个对象后,SLAB的处理是仅仅标记对象为空闲,并不做多少处理,而又有申请者申请相应大小的对象时,SLAB会优先分配最近释放的对象,这样这个对象甚至有可能还在硬件高速缓存中,有点类似管理区页框分配器中每CPU高速缓存的做法。
  


slab分配器的整体结构

这里写图片描述
这里写图片描述

kmem_cache结构

  虽然叫SLAB分配器,但是在SLAB分配器中,最顶层的数据结构却不是SLAB,而是kmem_cache,我们暂且叫它SLAB缓存吧,每个SLAB缓存都有它自己的名字,就是上图中的kmalloc-8,kmalloc-16等。总的来说,kmem_cache结构用于描述一种SLAB,并且管理着这种SLAB中所有的对象。所有的kmem_cache结构会保存在以slab_caches作为头的链表中。在内核模块中可以通过kmem_cache_create自行创建一个kmem_cache用于管理属于自己模块的SLAB。

  我们先看看kmem_cache结构:

/*
    每一个高速缓存对应着一个struct kmem_cache,可以有若干个高速缓存存放不同的对象
    每一个kmem_cache存放的对象大小都是相同的;
 * struct kmem_cache
 *
 * manages a cache.
 */

struct kmem_cache {
/* 1) per-cpu data, touched during every alloc/free */
    /*
    本地CPU空闲链表
    将释放的对象加入到这个链表中*/
    struct array_cache *array[NR_CPUS];
/* 2) Cache tunables. Protected by cache_chain_mutex */
     /* 要转移进本地高速缓存或从本地高速缓存中转移出去的对象的数量 */
    unsigned int batchcount;
    /* 本地高速缓存中空闲对象的最大数目 */
    unsigned int limit;
     /* 是否存在CPU共享高速缓存,CPU共享高速缓存指针保存在kmem_cache_node结构中 
    也就是CPU共享的空闲对象链表(区别于本地空闲对象列表);
    =0:没有CPU共享的链表
    =1:存在
    */
    unsigned int shared;

    /* 对象长度 + 填充字节 */
    unsigned int buffer_size;
    /* size的倒数,加快计算 */
    u32 reciprocal_buffer_size;
/* 3) touched by every alloc & free from the backend */
     /* 高速缓存永久属性的标识,
     如果SLAB描述符放在外部(不放在SLAB中),则CFLAGS_OFF_SLAB置1 */
    unsigned int flags;     /* constant flags */
    /* 每个SLAB中对象的个数(在同一个高速缓存中slab中对象个数相同) */
    unsigned int num;       /* # of objs per slab */

/* 4) cache_grow/shrink */
    /* order of pgs per slab (2^n) */
    /* 一个单独SLAB中包含的连续页框数目的对数 log2(N)*/
    unsigned int gfporder;

    /* force GFP flags, e.g. GFP_DMA */
     /* 分配页框时传递给伙伴系统的一组标识 */
    gfp_t gfpflags;

    /* SLAB使用的最大颜色个数,slab使用的着色值不得超过它,超过它就重新置为0 */
    size_t colour;          /* cache colouring range */
    /* SLAB中基本对齐偏移,
    当新SLAB着色时,偏移量的值需要乘上这个基本对齐偏移量,
    理解就是1个偏移量等于多少个B大小的值 */
    unsigned int colour_off;    /* colour offset */
    /*???*/
    struct kmem_cache *slabp_cache;
    unsigned int slab_size;
    unsigned int dflags;        /* dynamic flags */

    /* constructor func */
    /* 构造函数,一般用于初始化这个SLAB高速缓存中的对象 */
    void (*ctor)(void *obj);

/* 5) cache creation/removal */
    /* 存放高速缓存名字 */
    const char *name;
    /* 高速缓存描述符双向链表指针 ,指向下一个、上一个kmem_cache结构*/
    struct list_head next;

/* 6) statistics */
    /* 统计 */
#ifdef CONFIG_DEBUG_SLAB
    unsigned long num_active;
    unsigned long num_allocations;
    unsigned long high_mark;
    unsigned long grown;
    unsigned long reaped;
    unsigned long errors;
    unsigned long max_freeable;
    unsigned long node_allocs;
    unsigned long node_frees;
    unsigned long node_overflow;
    atomic_t allochit;
    atomic_t allocmiss;
    atomic_t freehit;
    atomic_t freemiss;

    /*
     * If debugging is enabled, then the allocator can add additional
     * fields and/or padding to every object. buffer_size contains the total
     * object size including these internal fields, the following two
     * variables contain the offset to the user object and its size.、
     在使用debug的情况下,分配器会给每一个对象增加一些格外的字段
     所以,下面两个变量表示:每个对象之间的偏移和每个对象的大小。
     */
     /* 对象间的偏移 */
    int obj_offset;
    /*对象的大小相同*/
    int obj_size;
#endif /* CONFIG_DEBUG_SLAB */

    /*
     * We put nodelists[] at the end of kmem_cache, because we want to size
     * this array to nr_node_ids slots instead of MAX_NUMNODES
     * (see kmem_cache_init())
     * We still use [MAX_NUMNODES] and not [1] or [0] because cache_cache
     * is statically defined, so we reserve the max number of nodes.
     */
     //该nodelist是一个指针数组,重点是数组,每个数组元素是一个指针
      /* 结点链表,此高速缓存可能在不同NUMA的结点都有SLAB链表,所以要设置成数组的形式 */
    struct kmem_list3 *nodelists[MAX_NUMNODES];
    /*
     * Do not add fields after nodelists[]
     */
};

从结构中可以看出,在这个kmem_cache中所有对象的大小是相同的(object_size),并且此kmem_cache中所有SLAB的大小也是相同的(gfporder、num)。

  在这个结构中,最重要的可能就属struct kmem_list3 *nodelists[MAX_NUMNODES]这个指针数组了,指向的struct kmem_list3中保存着slab链表,在NUMA架构中每个node对应数组中的一个元素,因为每个SLAB高速缓存都有可能在不同结点维护有自己的SLAB用于这个结点的分配。我们看看struct kmem_list3:

/*一个高速缓存中所有的slab都在此结构中;
 * The slab lists for all objects.
 */
struct kmem_list3 {
    /*部分满、满、空闲的slab双向循环链表
    其中保存的是SLAB描述符。*/
    struct list_head slabs_partial; /* partial list first, better asm code */
    struct list_head slabs_full;
    struct list_head slabs_free;
    /* 高速缓存中空闲对象个数
    (包括slabs_partial链表中和slabs_free链表中所有的空闲对象) */
    unsigned long free_objects;
    /* 高速缓存中空闲对象的上限 */
    unsigned int free_limit;
    /* 下一个被分配的SLAB使用的颜色 ,每次进行着色操作后,colour_next++即可
    着色的偏移量 = colour_next * kmem_cache.colour_off 
    colour_next++
    */
    unsigned int colour_next;   /* Per-node cache coloring */
    /* 锁 */
    spinlock_t list_lock;
     /* 指向这个结点上所有CPU共享的一个本地高速缓存,=1:可以共享 ;=0:不可以*/
    struct array_cache *shared; /* shared per node */
    struct array_cache **alien; /* on other nodes */
    /* 两次缓存收缩时的间隔,降低次数,提高性能??? */
    unsigned long next_reap;    /* updated without locking */
    /* 0:收缩   1:获取一个对象 */
    int free_touched;       /* updated without locking */
};

在这个结构中,最重要的就是slabs_partial、slabs_full、slabs_free这三个链表头。

slabs_partial:维护部分对象被使用了的SLAB链表,保存的是SLAB描述符。
slabs_full:维护所有对象都被使用了的SLAB链表,保存的是SLAB描述符。
slabs_free:维护所有对象都没被使用的SLAB链表,保存的是SLAB描述符。

  可能到这里大家会比较郁闷,怎么又有SLAB链表,SLAB到底是什么东西?如果看了我linux内存源码分析 - 页框分配器的朋友,或许可以联系起来了。SLAB就是一组连续的页框,它的描述符结合在页描述符中,也就是页描述符描述SLAB的时候,就是SLAB描述符。这三个链表保存的是这组页框的首页框的SLAB描述符。链表的组织形式与伙伴系统的组织页框的形式一样。

  刚开始创建kmem_cache完成后,这三个链表都为空,只有在申请对象时发现没有可用的slab时才会创建一个新的SLAB,并加入到这三个链表中的一个中。也就是说kmem_cache中的SLAB数量是动态变化的,当SLAB数量太多时,kmem_cache会将一些SLAB释放回页框分配器中。


关于SLAB着色

  看名字很难理解,其实又很好理解,我们知道内存需要处理时要先放入CPU硬件高速缓存中,而CPU硬件高速缓存与内存的映射方式有多种。在同一个kmem_cache中所有SLAB都是相同大小,都是相同连续长度的页框组成,这样的话在不同SLAB中相同对象号对于页框的首地址的偏移量也相同,这样有很可能导致不同SLAB中相同对象号的对象放入CPU硬件高速缓存时会处于同一行,当我们交替操作这两个对象时,CPU的cache就会交替换入换出,效率就非常差。SLAB着色就是在同一个kmem_cache中对不同的SLAB添加一个偏移量,就让相同对象号的对象不会对齐,也就不会放入硬件高速缓存的同一行中,提高了效率,如下图:
  这里写图片描述
着色空间就是前端的空闲区域,这个区有大小都是在分配新的SLAB时计算好的,计算方法很简单,node结点对应的kmem_cache_node中的colour_next乘上kmem_cache中的colour_off就得到了偏移量,然后colour_next++,当colour_next等于kmem_cache中的colour时,colour_next回归到0。

    偏移量 = kmem_cache.colour_off * kmem_cache.nodelists[NODE_ID].colour_next;

    kmem_cache.nodelist3[NODE_ID].colour_next++;
    if (kmem_cache.nodelists[NODE_ID].colour_next == kmem_cache.colour)
        kmem_cache.nodelists[NODE_ID].colour_next = 0;

本地CPU空闲对象链表

  现在说说本地CPU空闲对象链表。这个在kmem_cache结构中用cpu_cache表示,整个数据结构是struct array_cache,它的目的是将释放的对象加入到这个链表中,我们可以先看看数据结构:
  

/*
 * struct array_cache
 *
 * Purpose:
 * - LIFO ordering, to hand out cache-warm objects from _alloc
 * - reduce the number of linked list operations
 * - reduce spinlock operations
 *
 * The limit is stored in the per-cpu structure to reduce the data cache
 * footprint.
 *
 */
struct array_cache {
    /* 可用对象数目 */
    unsigned int avail;
    /* 可拥有的最大对象数目,和kmem_cache中一样 */
    unsigned int limit;
    /* 同kmem_cache,
    要转移进本地高速缓存或从本地高速缓存中转移出去的对象的数量 */
    unsigned int batchcount;
    /* 是否在收缩后被访问过 */
    unsigned int touched;
    spinlock_t lock;
     /* 伪数组,初始没有任何数据项,
     之后会增加并保存释放的对象指针 
     里面保存的是指向空闲对象的首地址的指针*/
    void *entry[];  /*
             * Must have this definition in here for the proper
             * alignment of array_cache. Also simplifies accessing
             * the entries.
             */
};

因为每个CPU都有它们自己的硬件高速缓存,当此CPU上释放对象时,可能这个对象很可能还在这个CPU的硬件高速缓存中,所以内核为每个CPU维护一个这样的链表,当需要新的对象时,会优先尝试从当前CPU的本地CPU空闲对象链表获取相应大小的对象。这个本地CPU空闲对象链表在系统初始化完成后是一个空的链表,只有释放对象时才会将对象加入这个链表。当然,链表对象个数也是有所限制,其最大值就是limit,链表数超过这个值时,会将batchcount个数的对象返回到所有CPU共享的空闲对象链表(也是这样一个结构)中。

  注意在array_cache中有一个entry数组,里面保存的是指向空闲对象的首地址的指针,注意这个链表是在kmem_cache结构中的,也就是kmalloc-8有它自己的本地CPU高速缓存链表,dquot也有它自己的本地CPU高速缓存链表,每种类型kmem_cache都有它自己的本地CPU空闲对象链表。
  


所有CPU共享的空闲对象链表

  原理和本地CPU空闲对象链表一样,唯一的区别就是所有CPU都可以从这个链表中获取对象,一个常规的对象申请流程是这样的:系统首先会从本地CPU空闲对象链表中尝试获取一个对象用于分配;如果失败,则尝试来到所有CPU共享的空闲对象链表链表中尝试获取;如果还是失败,就会从SLAB中分配一个;这时如果还失败,kmem_cache会尝试从页框分配器中获取一组连续的页框建立一个新的SLAB,然后从新的SLAB中获取一个对象。对象释放过程也类似,首先会先将对象释放到本地CPU空闲对象链表中,如果本地CPU空闲对象链表中对象过多,kmem_cache会将本地CPU空闲对象链表中的batchcount个对象移动到所有CPU共享的空闲对象链表链表中,如果所有CPU共享的空闲对象链表链表的对象也太多了,kmem_cache也会把所有CPU共享的空闲对象链表链表中batchcount个数的对象移回它们自己所属的SLAB中,这时如果SLAB中空闲对象太多,kmem_cache会整理出一些空闲的SLAB,将这些SLAB所占用的页框释放回页框分配器中。

  这个所有CPU共享的空闲对象链表也不是肯定会有的,kmem_cache中有个shared字段如果为1,则这个kmem_cache有这个高速缓存,如果为0则没有。


总体框架

这里写图片描述


转载自:https://www.cnblogs.com/tolimit/p/4566189.html

猜你喜欢

转载自blog.csdn.net/jasonlee_lijiaqi/article/details/79924103