slab源码分析--主要数据结构分析

基本原理

Linux 在保护模式下,由伙伴系统负责内存分配,分配内存的最小单位是页。在伙伴系统之上,Linux 又增加了 slab 机制,其工作是针对一些经常分配并释放的对象,如 task_struct 结构体等,这种对象的大小一般比较小,如果直接采用伙伴系统来进行分配和释放,不仅会造成大量的内部碎片,而且处理速度也太慢。而 slab 机制是基于对象进行管理的,相同类型的对象归为一类(如 tast_struct 就是一类),每次要申请这样一个对象,slab 缓存器就从一个 slab 列表中分配一个这样大小的单元出去,而当要释放时,将其重新归还给 slab 缓存器,而不是直接归还给伙伴系统。slab 分配对象时,会使用最近释放的对象内存块,因此其驻留在 CPU 高速缓存的概率较高。并且 slab 机制针对 SMP 和 NUMA 架构进行了处理,还考虑到了硬件 cache 方面的优化。

内存管理这一部分的处理框架就是下图:
这里写图片描述

管理机构图示

slab 机制管理机构如图:
这里写图片描述

主要数据结构

kmem_cache缓存器结构体

slab 机制为每种对象建立单独的 kmem_cache(下文统称缓存器),每个 缓存器实际上就是指定了一种规则,符合该缓存器描述规则的对象都将从该缓存器分配。缓存器自身被组织成了一个双向链表,它的头节点是 cache_chain。

实际上,有两种缓存器,general cache(通用缓存器) 和 specific cache(专用缓存器),通用缓存器包括:

  1. 缓存 kmem_cache 的缓存器(呵呵,有点玄乎,kmem_cache 即缓存器,它自己也是一个小内存,也需要使用 slab 机制来分配,所以这就是一个鸡与蛋的问题,我们需要“手工”制造一个缓存器,名曰 cache_cache 来缓存 kmem_cache 这个通用缓存器,完了再取而代之就行)。
  2. kmalloc 使用的对象按照 malloc_sizes[] 表的大小分属不同的缓存器,32、64、128…,每种大小对应两个两个缓存器,一个对应DMA,一个用于普通分配,这也是通用缓存器。

通用缓存器是按照大小进行划分,所以大小就是通用缓存器的主要规则。

专用缓存器为系统特定结构创建的对象,比如 struct file,此类缓存器对象来源于同一个结构。

下面来看缓存器的结构体:

//缓存器
struct kmem_cache {
/* 1) per-cpu data, touched during every alloc/free */
    //per-cpu数据,本地缓存,记录了本地高速缓存的信息,也用于跟踪最近释放的对象,每次分配和释放都先访问它
    struct array_cache *array[NR_CPUS];
/* 2) Cache tunables. Protected by cache_chain_mutex */
    unsigned int batchcount;  //本地缓存转入或转出的大批对象数目
    unsigned int limit;            //本地缓存空闲对象的最大数目
    unsigned int shared;         //是否支持本节点共享一部分cache的标志,如果支持,那就存在本地共享缓存

    unsigned int buffer_size;   //管理的对象大小
    u32 reciprocal_buffer_size;  //上面这个大小的倒数,貌似利用这个可用牛顿迭代法求什么:)
/* 3) touched by every alloc & free from the backend */

    unsigned int flags;     //cache 的永久标志
    unsigned int num;       //一个 slab 所包含的对象数目!!! 也就是说,kmem_cache 控制了它所管辖的所有对象大小数目及其他属性

/* 4) cache_grow/shrink */
    /* order of pgs per slab (2^n) */
    unsigned int gfporder;  //一个slab所包含的 page 的对数,也就是一个slab分配 2^gfporder 个 page

    /* force GFP flags, e.g. GFP_DMA */
    gfp_t gfpflags;     //与伙伴系统交互时所提供的分配标识

    size_t colour;          /* cache colouring range */ //着色的范围吧
    unsigned int colour_off;    /* colour offset */  //着色的偏移量
    struct kmem_cache *slabp_cache;//如果将slab描述符存储在外部,该指针指向slab描述符的 cache,否则为 NULL
    unsigned int slab_size;    // slab 的大小
    unsigned int dflags;        /* dynamic flags */  //FIXME: 动态标志

    /* constructor func */
    void (*ctor) (void *, struct kmem_cache *, unsigned long);

/* 5) cache creation/removal */
    const char *name;     //名字:)
    struct list_head next;    //构造链表所用

/* 6) statistics */
#if STATS
    //都是调试信息,略去
#endif
    /*
     * 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.
     */
     //nodelists 用于组织所有节点的 slab,每个节点寻找自己拥有的 cache 将自己作为 nodelists 的下标就可以访问了
     //不过从这里访问的只是每个节点的 slab 管理的 cache 以及每个节点的共享 cache ,per-cpu cache 是上面的array数组管理的
     //当然,针对同一个缓存器kmem_cache,它管理的是同一种对象,所以通过本 kmem_cache 结构体的 nodelists 成员访问的也就只是同种对象的 cache
    struct kmem_list3 *nodelists[MAX_NUMNODES];  
    /*
     * Do not add fields after nodelists[]?
     */
};

始终记住一点:缓存器的职责就是为它负责缓存的对象指定规则! 符合某个规则的对象就会从相应的缓存器去申请内存。

从上面的结构体可以看出,同一个缓存器中所有对象大小是相同的(buffer_size),并且同一缓存器中所有 slab 大小也是相同的(gfporder、num)。

缓存器最重要有三个成员:

  1. array[] 数组。它是每 CPU(per_cpu) 数据,也就是我们的本地缓存。这是为了减小 SMP 架构下自旋锁而设置的成员,无论是对象的分配还是回收都优先考虑本地缓存。
  2. shared 标志。该标志如果使能的话,缓存器成员 nodelist 对应的三链就会启用 struct array_cache* 类型的成员 shared,它用来串接共享的缓存对象。这就是所谓的本地共享缓存,用于 CPU 之间的对象共享。
  3. nodelist[] 数组。该数组的 index 是 NUMP 架构下的内存结点 node,数组成员都是三链。针对每个内存结点,缓存器都为它维持一个三链。三链是指 struct kmem_list3 结构体,该结构体内部有满(full)、部分满(partial)、空(free)三种以 slab 内部成员分配情况为依据建立的链表,所有的 slab 缓存都维系在三链上。

为什么要引入本地共享缓存?

考虑到下面的场景:
CPU1 收到大量的网络报文,分配 struct sk_buff 对象,报文处理完成后,由 CPU2 发出并释放,这样对象就被 CPU2 正好回收。这会造成 CPU1 的本地缓存耗尽,需要从三链中分配对象。而 CPU2 的本地缓存过多需要释放对象到三链中。此时本地共享缓存相当于给它们架了一座桥梁,有了它,上述情形执行由本地共享缓存负责交互即可,无需再访问三链。

array_cache结构体

本地缓存其实就是一个 array_cache 结构体。不过由于处于多 CPU 环境,因此就组建了一个该类型的 array 数组,将各个 CPU 的本地缓存组织在一起,作为缓存器的成员,统一起来方便管理。

/*
 * 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.
 *
 */  //array_cache中都是per-cpu数据,不会共享,这可以减少NUMA架构中多CPU的自旋锁竞争
struct array_cache {  
    unsigned int avail;     //本地缓存中可用的空闲对象数
    unsigned int limit;     //本地缓存空闲对象数目上限
    unsigned int batchcount;   //本地缓存一次性转入和转出的对象数量
    unsigned int touched;     //标识本地对象是否最近被使用
    spinlock_t lock;       //自旋锁
    void *entry[0]; /*    //这是一个柔性数组,便于对后面用于跟踪空闲对象的指针数组的访问
             * Must have this definition in here for the proper
             * alignment of array_cache. Also simplifies accessing
             * the entries.
             * [0] is for gcc 2.95. It should really be [].
             */
};

注意该结构体的最后一个元素 void* entry[0],这就是它能串接缓存对象的原因。

kmem_list3三链结构体

三链结构体如下:

/*
 * The slab lists for all objects.
 */
struct kmem_list3 {
    struct list_head slabs_partial;//部分满的slab链表,也就是部分对象呗分配出去的slab
    struct list_head slabs_full;    //满slab链表
    struct list_head slabs_free;  //空slab链表
    unsigned long free_objects;  //空闲对象的个数
    unsigned int free_limit;        //空闲对象的上限数目
    unsigned int colour_next;   /* Per-node cache coloring */   //每个节点下一个slab使用的颜色
    spinlock_t list_lock;      
    struct array_cache *shared; /* shared per node */    //每个节点共享出去的缓存
    struct array_cache **alien; /* on other nodes */    //FIXME: 其他节点的缓存,应该是共享的
    unsigned long next_reap;    /* updated without locking */  
    int free_touched;       /* updated without locking */
};

申请对象三部曲,先从本地缓存申请,本地缓存没有就找本地共享缓存,如果还没有,就来找三链了。最差的情况是,三链也没有,那就只能去伙伴系统要点 page,新建 slab 了。

当空闲对象比较富余时,free 链表的部分 slab 可能被定期回收。

slab结构体

struct slab 结构体如下,它的另外一个名字是 slab 描述符(与kmem_bufclt数组组成 slab 管理者(manager)。

/*
 * struct slab
 *
 * Manages the objs in a slab. Placed either at the beginning of mem allocated
 * for a slab, or allocated from an general cache.     //该管理者可以在slab头部申请内存,也可以从general cache处申请内存。
 * Slabs are chained into three list: fully used, partial, fully free slabs.
 */
struct slab {
    struct list_head list;       //用于将slab纳入三链之中
    unsigned long colouroff;    //该slab的着色偏移,是一大块,不是单位
    void *s_mem;            //指向slab中的第一个对象
    unsigned int inuse;     //slab中已分配出去的对象数目
    kmem_bufctl_t free;     //下一个空闲对象的下标
    unsigned short nodeid;  //NUMA架构节点标识号
};

这个结构体负责描述一个 slab 的情况。slab 机制还采用 kmem_bufctl_t (unsigned int) 类型数组来存储对象的下标(在这个结构体内没出现该数组,是在外部和它组合的),它们的存储位置是相连的。将它们两个统称为“slab管理者”。

slab 管理者的自身存储位置有两种,一种是 on-slab(内置式),一种是 off-slab (外置式)。内置式需要占据 slab 内部空间,所以计算偏移量什么的要加上。而外置式是重新申请一块 slab 专门用来存放 slab 管理者(总共两块 slab)。该数组使用指针强制转化方式访问,所以没有设为结构体成员。

通常对象小于 512 的小对象采用内置式 slab,大于等于 512 的大对象采用外置式 slab。

内置式:
pic

外置式:
pic

参考:

后记

先默哀一秒钟,从 9 号晚上开始看 slab,这几天晕晕乎乎的,尤其是所谓的缓存器的缓存 cache_cache,我至少想了一天才想明白套路。内核确实不好剖析,主要还是没有人出相应的书吧,期待有一天有相关的书,那就可以节约学习内核的人大量时间了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值