您当前的位置:首页 > 电脑百科 > 软件技术 > 操作系统 > linux百科

Slub分配器的来龙去脉

时间:2022-07-25 17:24:07  来源:  作者:嵌入式Linux

slab分配器设计的需求

linux内核的内存子系统中,伙伴系统无疑处于内存管理的核心地带,但是如果将内存管理从逻辑上分层,它的位置则处于最底层。Buddy是所有物理内存的管家,不论使用何种接口申请内存都要经由伙伴系统进行分配。但是,伙伴系统管理的物理内存是以页为单位,以4K页为例,它也包含了4096个字节。但是无论是内核自己还是用户程序,在日常的使用中都很少会需要使用四千多字节大小的内存。试想如果我们仅需要为10个字符的字符串分配内存,但是伙伴系统却给了我们一页,那这一页剩余没有使用的内存就浪费了,而且这个浪费近乎奢侈。除了浪费的问题, 还有一个更需要关心的问题是,在这样的分配情况下,如果分配非常频繁,系统可能很快就会面临严重的碎片化问题。因为频繁使用的数据结构也会频繁的分配和释放,加速生产内存碎片。另外,直接调用伙伴系统的操作对系统的数据和指令高速缓存也有很大的影响。所以,基于以上的原因,也源于现实需求,内核需要一种轻量的、快速的、灵活的新型内存分配器,最主要的是,它可以提供小块内存的分配。为了实现这样的小内存分配器,Sun公司的J.Bonwick首先在Solaris 2.4中设计并实现了slab分配器,并对其开源。在Linux中也实现了具有相同的基本设计思想的同名分配器slab。

slab、slob和slub关系

slab、slob和slub都是小内存分配器,slab是slob和slub实现的基础,而slob和slub是针对slab在不同场景下的优化版本。在slab引入Linux的很多年内,其都是Linux内核管理对象缓冲区的主流算法。并且由于slab的实现非常复杂,很长一段时间内都少有对它的改动。随着多处理器的发展和NUMA架构的广泛应用,slab的不足也逐渐显现。slab的缓存队列管理复杂,其用于管理的数据结构存储开销大,对NUMA支持复杂,slab着色机制效果不明显。这些不足让slab很难在两种场景下提供最优的性能:小型嵌入式系统和配备有大量物理内存的大规模并行系统。对于小型嵌入式系统来说,slab分配器的代码量和复杂性都太高;对于大规模并行系统,slab用于自身管理的数据结构就需要占用很多G字节内存。针对slab的不足,内核开发人员Christoph Lameter在在内核版本2.6开发期间,引入了新的Slub分配器。Slub简化了slab一些复杂的设计,但保持slab的基本设计思想。同时,一种新的针对小型嵌入式系统的分配器slob也被引入,为了适应嵌入式系统的特点,slob进行了特别的优化,以大幅减少代码量(slob只有大约600行代码)。

slab层在内存管理子系统的层次

slab层可以理解为一个通用层,其包含了slab、slob和slub,至于底层具体使用哪种分配器可以通过配置内核选项进行选择。对于内核的其他模块,则不需要关注底层使用了哪个分配器。因为为了保证内核的其他模块都可以无缝迁移到Slub/slob,所有分配器的接口都是相同的,它们都实现了一组特定的接口用于内存分配。下图为Slab层在内存管理中的层次图:

Slub分配器的来龙去脉

 

逻辑上看,slab层位于伙伴系统之上。因为Buddy是最底层的分配器,Slub需要先向Buddy申请内存,而不能越过Buddy获取page。从Buddy申请到内存后,Slub才可以对其进行自己的操作。

 

、slab分配器设计的需求

在Linux内核的内存子系统中,伙伴系统无疑处于内存管理的核心地带,但是如果将内存管理从逻辑上分层,它的位置则处于最底层。Buddy是所有物理内存的管家,不论使用何种接口申请内存都要经由伙伴系统进行分配。但是,伙伴系统管理的物理内存是以页为单位,以4K页为例,它也包含了4096个字节。但是无论是内核自己还是用户程序,在日常的使用中都很少会需要使用四千多字节大小的内存。试想如果我们仅需要为10个字符的字符串分配内存,但是伙伴系统却给了我们一页,那这一页剩余没有使用的内存就浪费了,而且这个浪费近乎奢侈。除了浪费的问题, 还有一个更需要关心的问题是,在这样的分配情况下,如果分配非常频繁,系统可能很快就会面临严重的碎片化问题。因为频繁使用的数据结构也会频繁的分配和释放,加速生产内存碎片。另外,直接调用伙伴系统的操作对系统的数据和指令高速缓存也有很大的影响。所以,基于以上的原因,也源于现实需求,内核需要一种轻量的、快速的、灵活的新型内存分配器,最主要的是,它可以提供小块内存的分配。为了实现这样的小内存分配器,Sun公司的J.Bonwick首先在Solaris 2.4中设计并实现了slab分配器,并对其开源。在Linux中也实现了具有相同的基本设计思想的同名分配器slab。

slab、slob和slub关系

slab、slob和slub都是小内存分配器,slab是slob和slub实现的基础,而slob和slub是针对slab在不同场景下的优化版本。在slab引入Linux的很多年内,其都是Linux内核管理对象缓冲区的主流算法。并且由于slab的实现非常复杂,很长一段时间内都少有对它的改动。随着多处理器的发展和NUMA架构的广泛应用,slab的不足也逐渐显现。slab的缓存队列管理复杂,其用于管理的数据结构存储开销大,对NUMA支持复杂,slab着色机制效果不明显。这些不足让slab很难在两种场景下提供最优的性能:小型嵌入式系统和配备有大量物理内存的大规模并行系统。对于小型嵌入式系统来说,slab分配器的代码量和复杂性都太高;对于大规模并行系统,slab用于自身管理的数据结构就需要占用很多G字节内存。针对slab的不足,内核开发人员Christoph Lameter在在内核版本2.6开发期间,引入了新的Slub分配器。Slub简化了slab一些复杂的设计,但保持slab的基本设计思想。同时,一种新的针对小型嵌入式系统的分配器slob也被引入,为了适应嵌入式系统的特点,slob进行了特别的优化,以大幅减少代码量(slob只有大约600行代码)。

slab层在内存管理子系统的层次

slab层可以理解为一个通用层,其包含了slab、slob和slub,至于底层具体使用哪种分配器可以通过配置内核选项进行选择。对于内核的其他模块,则不需要关注底层使用了哪个分配器。因为为了保证内核的其他模块都可以无缝迁移到Slub/slob,所有分配器的接口都是相同的,它们都实现了一组特定的接口用于内存分配。下图为Slab层在内存管理中的层次图:

Slub分配器的来龙去脉

 

逻辑上看,slab层位于伙伴系统之上。因为Buddy是最底层的分配器,Slub需要先向Buddy申请内存,而不能越过Buddy获取page。从Buddy申请到内存后,Slub才可以对其进行自己的操作。

slub分配器框架

下图是在读完宋牧春大侠的《图解Slub》后,我也总结了一张Slub分配器框架图,可以大致的看到Slub的框架。Slub的框架如下图(图片很大,可以放大):

Slub分配器的来龙去脉

 

这篇文章(原文链接以置文末)中用了一个通俗易懂的例子来介绍Slub的工作原理,我觉的这个例子很恰当,所以这里继续借举一下。

每个数组元素对应一种大小的内存,可以把一个kmem_cache结构体看做是一个特定大小内存的零售商,整个Slub系统中有很多个这样的零售商,每个“零售商”只“零售”特定大小的内存,例如:有的“零售商”只"零售"8Byte大小的内存,有的只”零售“16Byte大小的内存。——引自luken.《linux内核内存管理slub算法(一)原理》

Slub的工作原理和日常生产生活的产销环节很类似,所以为了清晰直观的看到其工作原理,我把这个过程画了一幅图来表示,如下图:

Slub分配器的来龙去脉

 

每个零售商(kmem_cache)有两个“部门”,一个是“仓库”:kmem_cache_node,一个“营业厅”:kmem_cache_cpu。“营业厅”里只保留一个slab,只有在营业厅(kmem_cache_cpu)中没有空闲内存的情况下才会从仓库中换出其他的slab。所谓slab就是零售商(kmem_cache)批发的连续的整页内存,零售商把这些整页的内存分成许多小内存,然后分别“零售”出去,一个slab可能包含多个连续的内存页。slab的大小和零售商有关。——引自luken.《linux内核内存管理slub算法(一)原理》

总的来说,Slub就相当于零售商,它从伙伴系统“批发”内存,然后再零售出去。

slub的重要数据结构

  • kmem_cache
struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;
    /* Used for retriving partial slabs etc */
    unsigned long flags;
    unsigned long min_partial;
    /* size = object_size + 对象后面下个空闲对象的指针的size */
    int size;           /* The size of an object including meta data */
    int object_size;    /* The size of an object without meta data */
    /* object首地址 + offset = 下一个空闲对象的指针地址 */
    int offset;         /* Free pointer offset. */
    int cpu_partial;    /* Number of per cpu partial objects to keep around */
    /* 
     * oo表示存放最优slab的order和object的数量
     * 低16位表示对象数,高16位表示slab的order
     */
    struct kmem_cache_order_objects oo;
    /* Allocation and freeing of slabs */
    struct kmem_cache_order_objects max;
    /* 
     * 最小slab只需要足够存放一个对象。当设备长时间运行以后,内存碎片化严重,
     * 分配连续物理页很难成功,如果分配最优slab失败,就分配最小slab。
     */
    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;          /* Alignment */
    // 当slab长度不是对象长度的整数倍的时候,尾部有剩余部分,保存在reserved中
    int reserved;       /* Reserved bytes at the end of slabs */
    const char *name;   /* Name (only for display!) */
    struct list_head list;  /* List of slab caches */
    int red_left_pad;   /* Left redzone padding size */
#ifdef CONFIG_SYSFS
    struct kobject kobj;    /* For sysfs */
#endif
#ifdef CONFIG_MEMCG
    struct memcg_cache_params memcg_params;
    int max_attr_size; /* for propagation, maximum size of a stored attr */
#ifdef CONFIG_SYSFS
    struct kset *memcg_kset;
#endif
#endif
#ifdef CONFIG_NUMA
    /*
     * Defragmentation by allocating from a remote node.
     */
    int remote_node_defrag_ratio;
#endif
#ifdef CONFIG_SLAB_FREELIST_RANDOM
    unsigned int *random_seq;
#endif
#ifdef CONFIG_KASAN
    struct kasan_cache kasan_info;
#endif
    struct kmem_cache_node *node[MAX_NUMNODES]; /* 每个NUMA节点都有一个kmem_cache_node */
};

根据是否打开Slub Debug,next object指针可以有两种方式放置,如果打开了Slub Debug,则采用指针外置式;反之,采用指针内置式。两种指针放置方式如下图:

  • 指针外置式
Slub分配器的来龙去脉

 

  • 指针内置式
Slub分配器的来龙去脉

 

指针内置式的方法实际上是复用了object的前8个字节,因为在object被分配出去之前,这一段内存具体存放什么内容并不重要,所以可以利用这一段内存来保存下一个free object的地址。

  • kmem_cache_cpu
struct kmem_cache_cpu {
    /* 指向下一个空闲的object,用于快速找到可用对象 */
    void **freelist;    /* Pointer to next avAIlable object */
    /*
     * 要保证tid和kmem_cache是由同一个CPU访问。
     * 开启了内核抢占后,访问tid和kmem_cache的CPU可能不是同一个CPU,
     * 所以要检查是否匹配,直到它们是由同一个CPU进行访问 
     */
    unsigned long tid;  /* Globally unique transaction id */
    /* 指向当前使用的slab */
    struct page *page;  /* The slab from which we are allocating */
    /* 指向当前cpu上缓存的部分空闲slab链表 */
    struct page *partial;   /* Partially allocated frozen slabs */
#ifdef CONFIG_SLUB_STATS
    /* 
     * 记录对slab操作的状态变化,这个stat非常重要,
     * 通过这个stat就大概了解object从申请到释放经过了哪些步骤 
     */
    unsigned stat[NR_SLUB_STAT_ITEMS]; 
#endif
};
  • kmem_cache_node
struct kmem_cache_node {
        spinlock_t list_lock;
        /* 此处省略掉SLAB的配置 */  
#ifdef CONFIG_SLUB
        /* 挂入kmem_cache_node中的slab数量 */
        unsigned long nr_partial;
        /* 指向当前内存节点上的部分空闲slab链表 */
        struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
        atomic_long_t nr_slabs;
        atomic_long_t total_objects;
        struct list_head full;
#endif
#endif
};

page中描述Slub信息的字段:

struct page {
     /* 如果flag设置成PG_slab,表示页属于slub分配器 */
      unsigned long flags;
      union {
        struct address_space *mApping;  
        /* 指向当前slab中第一个object */
        void *s_mem;      /* slab first object */
        atomic_t compound_mapcount;  /* first tail page */
      };
      union {
        pgoff_t index;    /* Our offset within mapping. */
        /* 指向当前slab中第一个空闲的object */
        void *freelist;    /* sl[aou]b first free object */
      };
      union {
        unsigned counters;
        struct {
          union {
            atomic_t _mapcount;
            unsigned int active;    /* SLAB */
            struct {      /* SLUB */
              /* 该slab中已经分配使用的object数量 */
              unsigned inuse:16;
              /* 该slab中的所有object数量 */
              unsigned objects:15;
              /* 
               * 如果slab在kmem_cache_cpu中,表示处于冻结状态;
               * 如果slab在kmem_cache_node的部分空闲slab链表中,表示处于解冻状态
        */
              unsigned frozen:1;
            };
            int units;      /* SLOB */
          };
          atomic_t _refcount;
        };
      };
      union {
        /* 作为链表节点加入到kmem_cache_node的部分空闲slab链表中
        struct list_head lru;  /* Pageout list   */
        struct dev_pagemap *pgmap; 
        struct {    /* slub per cpu partial pages */
          struct page *next;  /* Next partial slab */
          int pages;  /* Nr of partial slabs left */
          int pobjects;  /* Approximate # of objects */
        };
        struct rcu_head rcu_head;
        struct {
          unsigned long compound_head; /* If bit zero is set */
          unsigned int compound_dtor;
          unsigned int compound_order;
        };
      };
      union {
        unsigned long private;
        struct kmem_cache *slab_cache;  /* SL[AU]B: Pointer to slab */
      };
    ......
    }

Slub的分配过程

Slub的分配流程大致如下:首先从kmem_cache_cpu中分配,如果没有则从kmem_cache_cpu的partial链表分配,如果还没有则从kmem_cache_node中分配,如果kmem_cache_node中也没有,则需要向伙伴系统申请内存。

Slub分配器的来龙去脉

 

Slub的分配接口是kmem_cache_malloc()。其分配object的流程大概如下:首先在kmem_cache_cpu所使用的slab中查找free object,如果当前slab中有free object,则返回这个object。如果当前slab没有free object,就要看Slub是否开启了kmem_cache_cpu的Partial队列,如果开启了partial队列,就在Partial队列中查看有没有free object的slab,如果有的话就选定这个slab,并返回其free object。如果kmem_cache_cpu的partial链表中也没有拥有free object的slab,则在kmem_cache_node中查找。如果kmem_cache_node中的slab有free object,则选定这个slab并返回free object。如果kmem_cache_node中也没有free object,则需要向伙伴系统申请内存,制作新的slab。

创建slab缓存(kmem_cache)的函数分析

斗胆分析一下slab缓存的创建过程,新手小白分析内核代码,分析的可能不够深度和完整,如有不对还请各路高手指教,提前谢过。

函数调用流程:

kmem_cache_create()
    ——> kmem_cache_create_usercopy()
        ——> create_cache()
            ——> __kmem_cache_create()
                ——> kmem_cache_open()

下面是每个函数的主干分析,代码有精简。

kmem_cache_create():

kmem_cache_create()里继续调用了
kmem_cache_create_usercopy()。

kmem_cache_create() {
 return kmem_cache_create_usercopy(name, size, align, flags, 0, 0, ctor);
}

kmem_cache_create_usercopy():

kmem_cache_create_usercopy() {
 struct kmem_cache *s = NULL;
 const char *cache_name;
 
 /*
  * Some allocators will constraint the set of valid flags to a subset
  * of all flags. We expect them to define CACHE_CREATE_MASK in this
  * case, and we'll just provide them with a sanitized version of the
  * passed flags.
  */
 flags &= CACHE_CREATE_MASK;
 
 /* 定义这个缓存的名字,用于在/proc/slabinfo中显示 */
 cache_name = kstrdup_const(name, GFP_KERNEL);
 
 /* kmem_cache结构,并返回其地址 */
 s = create_cache(cache_name, size,
    calculate_alignment(flags, align, size),
    flags, useroffset, usersize, ctor, NULL, NULL);
 
 return s;
}

create_cache():

create_cache() {
 struct kmem_cache *s;
 int err;

 /* 为kmem_cache结构申请一段内存并清零 */
 s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);

 /* 初始化kmem_cache结构的部分成员 */
 s->name = name;
 s->size = s->object_size = object_size;
 s->align = align;
 s->ctor = ctor;
 s->useroffset = useroffset;
 s->usersize = usersize;

 /* 核心函数,slub/slab/slob都实现了这个函数 */
 err = __kmem_cache_create(s, flags);
 
 /* 将新创建的kmem_cache加入slab caches链表 */
 list_add(&s->list, &slab_caches);
 
 return s;
    }

__kmem_cache_create():

__kmem_cache_create() {
  int err;

  /* 在kmem_cache_open中处理剩余的结构成员,如min_partial、cpu_partial等 */
  err = kmem_cache_open(s, flags);
 }

kmem_cache_open():

kmem_cache_open() {
 /* 设置kmem_cache中的min_partial,它表示kmem_cache_node中partial链表可挂入的slab数量 */
 set_min_partial(s, ilog2(s->size) / 2);
 
 /* 设置kmem_cache中的cpu_partial,它表示per cpu partial上所有slab中free object总数 */
 set_cpu_partial(s);
 
 /* 为每个节点分配kmem_cache_node */
 if (!init_kmem_cache_nodes(s))
  goto error;

 /* 为kmem_cache_cpu变量创建每CPU副本 */
 if (alloc_kmem_cache_cpus(s))
  return 0;
}

分配对象(object)的函数分析

函数调用流程:

kmem_cache_alloc()
    ——> slab_alloc()
        ——> slab_alloc_node()
           ——> __slab_alloc()
              ——> ___slab_alloc()

kmem_cache_alloc():

kmem_cache_alloc() {
 /* 直接调用slab_alloc */
 void *ret = slab_alloc(s, gfpflags, _RET_IP_);

 return ret;
}

slab_alloc():

slab_alloc() {
 return slab_alloc_node(s, gfpflags, NUMA_NO_NODE, addr);
}

slab_alloc_node():

slab_alloc_node() {
 void *object;  
 struct kmem_cache_cpu *c;
 struct page *page;

redo:
 /*
  * 要保证tid和kmem_cache是由同一个CPU访问。但是如果配置了CONFIG_PREEMPT = y,
  * 即开启了内核抢占后,访问tid和kmem_cache的CPU可能不是同一个CPU,所以要检查
  * 是否匹配,直到它们是由同一个CPU进行访问。
  * 
  * 内核态抢占的时机是:
  * 1.中断处理函数返回内核空间之前会检查请求重新调度的标志(TIF_NEED_RESCHED),
  * 如果置位则调用preempt_schedule_irq()执行抢占。
  * 2. 当内核从non-preemptible(禁止抢占)状态变成preemptible(允许抢占)的时候。
  */
 do {
  tid = this_cpu_read(s->cpu_slab->tid); /* 访问当前CPU的per CPU变量的副本的tid */
  c = raw_cpu_ptr(s->cpu_slab);
 } while (IS_ENABLED(CONFIG_PREEMPT) &&  /* 检查是否开启了内核抢占 */
   unlikely(tid != READ_ONCE(c->tid)));

 barrier(); /* 内存屏障,消除指令乱序执行的影响 */

 object = c->freelist;  /* 下一个free object的地址 */
 page = c->page;   /* 当前使用的slab */
 if (unlikely(!object || !node_match(page, node))) {
  /* 调用核心函数__slab_alloc() */
  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);
 }

 maybe_wipe_obj_freeptr(s, object);

 /* 如果gfpflags标志需要对object对象的内存清零 */
 if (unlikely(slab_want_init_on_alloc(gfpflags, s)) && object)
  memset(object, 0, s->object_size);

 slab_post_alloc_hook(s, gfpflags, 1, &object);

 return object;
    }

__slab_alloc():

__slab_alloc() {
 void *p;
 unsigned long flags;
 
 /* 
  * 关中断。关闭当前处理器上的所有中断处理
  *
  * local_irq_save()将当前的中断状态(开或关)
  * 保存在flags中然后再禁用处理器上的中断。
  * 
  * 与local_irq_save不同,local_irq_disable()
  * 不保存状态而关闭本地处理器的中断服务。 
  */
 local_irq_save(flags);  
#ifdef CONFIG_PREEMPT
 /*
  * 在关中断之前,可能已经被抢占并被调度在不同的CPU上,
  * 所以需要重新加载CPU区域的指针。
  */
 c = this_cpu_ptr(s->cpu_slab);
#endif
 /* 调用核心函数___slab_alloc() */
 p = ___slab_alloc(s, gfpflags, node, addr, c);
 
 /*
  * 恢复本地处理器的中断。
  *
  * local_irq_restore()将local_irq_save()保存的状态值(flags)恢复,
  * 注意是恢复之前的中断状态,不一定会开启中断。如果之前的状态是
  * 开中断,就打开中断;如果之前的状态是关中断,就关闭中断。
  * 而local_irq_enable()会无条件开启中断,所以可能会破坏之前的中
  * 断环境。所以local_irq_restore()比local_irq_enable()更安全。
  */
 local_irq_restore(flags); 
 
        return p;
}

slub的frozen(冻结)和unfrozen(解冻)

如果cpu1的kcmem_cache_cpu的slab是frozen, 那么cpu1可以从该slab中取出或放回obj,但是cpu2不能从该slab中取obj, 只能把obj还给该slab。另外,cpu partial上的slab都是frozen状态。node partial上的slab都是unfrozen。耗尽kmem_cache_cpu的slab的obj后解冻slab。

嵌入式物联网需要学的东西真的非常多,千万不要学错了路线和内容,导致工资要不上去!

无偿分享大家一个资料包,差不多150多G。里面学习内容、面经、项目都比较新也比较全!某鱼上买估计至少要好几十。

点击这里找小助理0元领取:嵌入式物联网学习资料(头条)

Slub分配器的来龙去脉

 


Slub分配器的来龙去脉

 

slub分配器框架

下图是在读完宋牧春大侠的《图解Slub》后,我也总结了一张Slub分配器框架图,可以大致的看到Slub的框架。Slub的框架如下图(图片很大,可以放大):

Slub分配器的来龙去脉

 

这篇文章(原文链接以置文末)中用了一个通俗易懂的例子来介绍Slub的工作原理,我觉的这个例子很恰当,所以这里继续借举一下。

每个数组元素对应一种大小的内存,可以把一个kmem_cache结构体看做是一个特定大小内存的零售商,整个Slub系统中有很多个这样的零售商,每个“零售商”只“零售”特定大小的内存,例如:有的“零售商”只"零售"8Byte大小的内存,有的只”零售“16Byte大小的内存。——引自luken.《linux内核内存管理slub算法(一)原理》

Slub的工作原理和日常生产生活的产销环节很类似,所以为了清晰直观的看到其工作原理,我把这个过程画了一幅图来表示,如下图:

Slub分配器的来龙去脉

 

每个零售商(kmem_cache)有两个“部门”,一个是“仓库”:kmem_cache_node,一个“营业厅”:kmem_cache_cpu。“营业厅”里只保留一个slab,只有在营业厅(kmem_cache_cpu)中没有空闲内存的情况下才会从仓库中换出其他的slab。所谓slab就是零售商(kmem_cache)批发的连续的整页内存,零售商把这些整页的内存分成许多小内存,然后分别“零售”出去,一个slab可能包含多个连续的内存页。slab的大小和零售商有关。——引自luken.《linux内核内存管理slub算法(一)原理》

总的来说,Slub就相当于零售商,它从伙伴系统“批发”内存,然后再零售出去。

slub的重要数据结构

  • kmem_cache
struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;
    /* Used for retriving partial slabs etc */
    unsigned long flags;
    unsigned long min_partial;
    /* size = object_size + 对象后面下个空闲对象的指针的size */
    int size;           /* The size of an object including meta data */
    int object_size;    /* The size of an object without meta data */
    /* object首地址 + offset = 下一个空闲对象的指针地址 */
    int offset;         /* Free pointer offset. */
    int cpu_partial;    /* Number of per cpu partial objects to keep around */
    /* 
     * oo表示存放最优slab的order和object的数量
     * 低16位表示对象数,高16位表示slab的order
     */
    struct kmem_cache_order_objects oo;
    /* Allocation and freeing of slabs */
    struct kmem_cache_order_objects max;
    /* 
     * 最小slab只需要足够存放一个对象。当设备长时间运行以后,内存碎片化严重,
     * 分配连续物理页很难成功,如果分配最优slab失败,就分配最小slab。
     */
    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;          /* Alignment */
    // 当slab长度不是对象长度的整数倍的时候,尾部有剩余部分,保存在reserved中
    int reserved;       /* Reserved bytes at the end of slabs */
    const char *name;   /* Name (only for display!) */
    struct list_head list;  /* List of slab caches */
    int red_left_pad;   /* Left redzone padding size */
#ifdef CONFIG_SYSFS
    struct kobject kobj;    /* For sysfs */
#endif
#ifdef CONFIG_MEMCG
    struct memcg_cache_params memcg_params;
    int max_attr_size; /* for propagation, maximum size of a stored attr */
#ifdef CONFIG_SYSFS
    struct kset *memcg_kset;
#endif
#endif
#ifdef CONFIG_NUMA
    /*
     * Defragmentation by allocating from a remote node.
     */
    int remote_node_defrag_ratio;
#endif
#ifdef CONFIG_SLAB_FREELIST_RANDOM
    unsigned int *random_seq;
#endif
#ifdef CONFIG_KASAN
    struct kasan_cache kasan_info;
#endif
    struct kmem_cache_node *node[MAX_NUMNODES]; /* 每个NUMA节点都有一个kmem_cache_node */
};

根据是否打开Slub Debug,next object指针可以有两种方式放置,如果打开了Slub Debug,则采用指针外置式;反之,采用指针内置式。两种指针放置方式如下图:

  • 指针外置式
Slub分配器的来龙去脉

 

  • 指针内置式
Slub分配器的来龙去脉

 

指针内置式的方法实际上是复用了object的前8个字节,因为在object被分配出去之前,这一段内存具体存放什么内容并不重要,所以可以利用这一段内存来保存下一个free object的地址。

  • kmem_cache_cpu
struct kmem_cache_cpu {
    /* 指向下一个空闲的object,用于快速找到可用对象 */
    void **freelist;    /* Pointer to next available object */
    /*
     * 要保证tid和kmem_cache是由同一个CPU访问。
     * 开启了内核抢占后,访问tid和kmem_cache的CPU可能不是同一个CPU,
     * 所以要检查是否匹配,直到它们是由同一个CPU进行访问 
     */
    unsigned long tid;  /* Globally unique transaction id */
    /* 指向当前使用的slab */
    struct page *page;  /* The slab from which we are allocating */
    /* 指向当前cpu上缓存的部分空闲slab链表 */
    struct page *partial;   /* Partially allocated frozen slabs */
#ifdef CONFIG_SLUB_STATS
    /* 
     * 记录对slab操作的状态变化,这个stat非常重要,
     * 通过这个stat就大概了解object从申请到释放经过了哪些步骤 
     */
    unsigned stat[NR_SLUB_STAT_ITEMS]; 
#endif
};
  • kmem_cache_node
struct kmem_cache_node {
        spinlock_t list_lock;
        /* 此处省略掉SLAB的配置 */  
#ifdef CONFIG_SLUB
        /* 挂入kmem_cache_node中的slab数量 */
        unsigned long nr_partial;
        /* 指向当前内存节点上的部分空闲slab链表 */
        struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
        atomic_long_t nr_slabs;
        atomic_long_t total_objects;
        struct list_head full;
#endif
#endif
};

page中描述Slub信息的字段:

struct page {
     /* 如果flag设置成PG_slab,表示页属于slub分配器 */
      unsigned long flags;
      union {
        struct address_space *mapping;  
        /* 指向当前slab中第一个object */
        void *s_mem;      /* slab first object */
        atomic_t compound_mapcount;  /* first tail page */
      };
      union {
        pgoff_t index;    /* Our offset within mapping. */
        /* 指向当前slab中第一个空闲的object */
        void *freelist;    /* sl[aou]b first free object */
      };
      union {
        unsigned counters;
        struct {
          union {
            atomic_t _mapcount;
            unsigned int active;    /* SLAB */
            struct {      /* SLUB */
              /* 该slab中已经分配使用的object数量 */
              unsigned inuse:16;
              /* 该slab中的所有object数量 */
              unsigned objects:15;
              /* 
               * 如果slab在kmem_cache_cpu中,表示处于冻结状态;
               * 如果slab在kmem_cache_node的部分空闲slab链表中,表示处于解冻状态
        */
              unsigned frozen:1;
            };
            int units;      /* SLOB */
          };
          atomic_t _refcount;
        };
      };
      union {
        /* 作为链表节点加入到kmem_cache_node的部分空闲slab链表中
        struct list_head lru;  /* Pageout list   */
        struct dev_pagemap *pgmap; 
        struct {    /* slub per cpu partial pages */
          struct page *next;  /* Next partial slab */
          int pages;  /* Nr of partial slabs left */
          int pobjects;  /* Approximate # of objects */
        };
        struct rcu_head rcu_head;
        struct {
          unsigned long compound_head; /* If bit zero is set */
          unsigned int compound_dtor;
          unsigned int compound_order;
        };
      };
      union {
        unsigned long private;
        struct kmem_cache *slab_cache;  /* SL[AU]B: Pointer to slab */
      };
    ......
    }

Slub的分配过程

Slub的分配流程大致如下:首先从kmem_cache_cpu中分配,如果没有则从kmem_cache_cpu的partial链表分配,如果还没有则从kmem_cache_node中分配,如果kmem_cache_node中也没有,则需要向伙伴系统申请内存。

Slub分配器的来龙去脉

 

Slub的分配接口是kmem_cache_malloc()。其分配object的流程大概如下:首先在kmem_cache_cpu所使用的slab中查找free object,如果当前slab中有free object,则返回这个object。如果当前slab没有free object,就要看Slub是否开启了kmem_cache_cpu的Partial队列,如果开启了partial队列,就在Partial队列中查看有没有free object的slab,如果有的话就选定这个slab,并返回其free object。如果kmem_cache_cpu的partial链表中也没有拥有free object的slab,则在kmem_cache_node中查找。如果kmem_cache_node中的slab有free object,则选定这个slab并返回free object。如果kmem_cache_node中也没有free object,则需要向伙伴系统申请内存,制作新的slab。

创建slab缓存(kmem_cache)的函数分析

斗胆分析一下slab缓存的创建过程,新手小白分析内核代码,分析的可能不够深度和完整,如有不对还请各路高手指教,提前谢过。

函数调用流程:

kmem_cache_create()
    ——> kmem_cache_create_usercopy()
        ——> create_cache()
            ——> __kmem_cache_create()
                ——> kmem_cache_open()

下面是每个函数的主干分析,代码有精简。

kmem_cache_create():

kmem_cache_create()里继续调用了
kmem_cache_create_usercopy()。

kmem_cache_create() {
 return kmem_cache_create_usercopy(name, size, align, flags, 0, 0, ctor);
}

kmem_cache_create_usercopy():

kmem_cache_create_usercopy() {
 struct kmem_cache *s = NULL;
 const char *cache_name;
 
 /*
  * Some allocators will constraint the set of valid flags to a subset
  * of all flags. We expect them to define CACHE_CREATE_MASK in this
  * case, and we'll just provide them with a sanitized version of the
  * passed flags.
  */
 flags &= CACHE_CREATE_MASK;
 
 /* 定义这个缓存的名字,用于在/proc/slabinfo中显示 */
 cache_name = kstrdup_const(name, GFP_KERNEL);
 
 /* kmem_cache结构,并返回其地址 */
 s = create_cache(cache_name, size,
    calculate_alignment(flags, align, size),
    flags, useroffset, usersize, ctor, NULL, NULL);
 
 return s;
}

create_cache():

create_cache() {
 struct kmem_cache *s;
 int err;

 /* 为kmem_cache结构申请一段内存并清零 */
 s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);

 /* 初始化kmem_cache结构的部分成员 */
 s->name = name;
 s->size = s->object_size = object_size;
 s->align = align;
 s->ctor = ctor;
 s->useroffset = useroffset;
 s->usersize = usersize;

 /* 核心函数,slub/slab/slob都实现了这个函数 */
 err = __kmem_cache_create(s, flags);
 
 /* 将新创建的kmem_cache加入slab caches链表 */
 list_add(&s->list, &slab_caches);
 
 return s;
    }

__kmem_cache_create():

__kmem_cache_create() {
  int err;

  /* 在kmem_cache_open中处理剩余的结构成员,如min_partial、cpu_partial等 */
  err = kmem_cache_open(s, flags);
 }

kmem_cache_open():

kmem_cache_open() {
 /* 设置kmem_cache中的min_partial,它表示kmem_cache_node中partial链表可挂入的slab数量 */
 set_min_partial(s, ilog2(s->size) / 2);
 
 /* 设置kmem_cache中的cpu_partial,它表示per cpu partial上所有slab中free object总数 */
 set_cpu_partial(s);
 
 /* 为每个节点分配kmem_cache_node */
 if (!init_kmem_cache_nodes(s))
  goto error;

 /* 为kmem_cache_cpu变量创建每CPU副本 */
 if (alloc_kmem_cache_cpus(s))
  return 0;
}

分配对象(object)的函数分析

函数调用流程:

kmem_cache_alloc()
    ——> slab_alloc()
        ——> slab_alloc_node()
           ——> __slab_alloc()
              ——> ___slab_alloc()

kmem_cache_alloc():

kmem_cache_alloc() {
 /* 直接调用slab_alloc */
 void *ret = slab_alloc(s, gfpflags, _RET_IP_);

 return ret;
}

slab_alloc():

slab_alloc() {
 return slab_alloc_node(s, gfpflags, NUMA_NO_NODE, addr);
}

slab_alloc_node():

slab_alloc_node() {
 void *object;  
 struct kmem_cache_cpu *c;
 struct page *page;

redo:
 /*
  * 要保证tid和kmem_cache是由同一个CPU访问。但是如果配置了CONFIG_PREEMPT = y,
  * 即开启了内核抢占后,访问tid和kmem_cache的CPU可能不是同一个CPU,所以要检查
  * 是否匹配,直到它们是由同一个CPU进行访问。
  * 
  * 内核态抢占的时机是:
  * 1.中断处理函数返回内核空间之前会检查请求重新调度的标志(TIF_NEED_RESCHED),
  * 如果置位则调用preempt_schedule_irq()执行抢占。
  * 2. 当内核从non-preemptible(禁止抢占)状态变成preemptible(允许抢占)的时候。
  */
 do {
  tid = this_cpu_read(s->cpu_slab->tid); /* 访问当前CPU的per CPU变量的副本的tid */
  c = raw_cpu_ptr(s->cpu_slab);
 } while (IS_ENABLED(CONFIG_PREEMPT) &&  /* 检查是否开启了内核抢占 */
   unlikely(tid != READ_ONCE(c->tid)));

 barrier(); /* 内存屏障,消除指令乱序执行的影响 */

 object = c->freelist;  /* 下一个free object的地址 */
 page = c->page;   /* 当前使用的slab */
 if (unlikely(!object || !node_match(page, node))) {
  /* 调用核心函数__slab_alloc() */
  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);
 }

 maybe_wipe_obj_freeptr(s, object);

 /* 如果gfpflags标志需要对object对象的内存清零 */
 if (unlikely(slab_want_init_on_alloc(gfpflags, s)) && object)
  memset(object, 0, s->object_size);

 slab_post_alloc_hook(s, gfpflags, 1, &object);

 return object;
    }

__slab_alloc():

__slab_alloc() {
 void *p;
 unsigned long flags;
 
 /* 
  * 关中断。关闭当前处理器上的所有中断处理
  *
  * local_irq_save()将当前的中断状态(开或关)
  * 保存在flags中然后再禁用处理器上的中断。
  * 
  * 与local_irq_save不同,local_irq_disable()
  * 不保存状态而关闭本地处理器的中断服务。 
  */
 local_irq_save(flags);  
#ifdef CONFIG_PREEMPT
 /*
  * 在关中断之前,可能已经被抢占并被调度在不同的CPU上,
  * 所以需要重新加载CPU区域的指针。
  */
 c = this_cpu_ptr(s->cpu_slab);
#endif
 /* 调用核心函数___slab_alloc() */
 p = ___slab_alloc(s, gfpflags, node, addr, c);
 
 /*
  * 恢复本地处理器的中断。
  *
  * local_irq_restore()将local_irq_save()保存的状态值(flags)恢复,
  * 注意是恢复之前的中断状态,不一定会开启中断。如果之前的状态是
  * 开中断,就打开中断;如果之前的状态是关中断,就关闭中断。
  * 而local_irq_enable()会无条件开启中断,所以可能会破坏之前的中
  * 断环境。所以local_irq_restore()比local_irq_enable()更安全。
  */
 local_irq_restore(flags); 
 
        return p;
}

slub的frozen(冻结)和unfrozen(解冻)

如果cpu1的kcmem_cache_cpu的slab是frozen, 那么cpu1可以从该slab中取出或放回obj,但是cpu2不能从该slab中取obj, 只能把obj还给该slab。另外,cpu partial上的slab都是frozen状态。node partial上的slab都是unfrozen。耗尽kmem_cache_cpu的slab的obj后解冻slab。

文章链接:
https://mp.weixin.qq.com/s/CJW8crYUqtRztavhpOR6uA

转载自一口Linux

文章来源:人人都是极客 ,作者贺东升

版权申明:本文来源于网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。



Tags:Slub分配器   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
Slub分配器的来龙去脉
slab分配器设计的需求在Linux内核的内存子系统中,伙伴系统无疑处于内存管理的核心地带,但是如果将内存管理从逻辑上分层,它的位置则处于最底层。Buddy是所有物理内存的管家,不论...【详细内容】
2022-07-25  Search: Slub分配器  点击:(566)  评论:(0)  加入收藏
▌简易百科推荐
微软 Win11 Linux 子系统(WSL)发布 2.2.2 版本
IT之家 4 月 8 日消息,微软近日更新 Windows Subsystem for Linux(WSL),最新 2.2.2 版本中带来了诸多改进,重点更新了 nft 规则,可以让 IPv6 流量通过 Linux 容器。图源: dev.to,AI...【详细内容】
2024-04-08    IT之家  Tags:Linux   点击:(10)  评论:(0)  加入收藏
从原理到实践:深入探索Linux安全机制
Linux 是一种开源的类Unix操作系统内核,由Linus Torvalds在1991年首次发布,其后又衍生出许多不同的发行版(如Ubuntu、Debian、CentOS等)。前言本文将从用户和权限管理、文件系统...【详细内容】
2024-03-27  凡夫编程  微信公众号  Tags:Linux安全   点击:(26)  评论:(0)  加入收藏
在Linux系统中,如何处理内存管理和优化的问题?
本文对 Linux 内存管理和优化的一些高级技巧的详细介绍,通过高级的内存管理技巧,可以帮助系统管理员和开发人员更好地优化 Linux 系统的内存使用情况,提高系统性能和稳定性。在...【详细内容】
2024-03-26  编程技术汇  微信公众号  Tags:Linux   点击:(18)  评论:(0)  加入收藏
Linux 6.9-rc1 内核发布:AMD P-State 首选核心、BH 工作队列
IT之家 3 月 25 日消息,Linus Torvalds 宣布,Linux 6.9 内核的首个 RC(候选发布)版 Linux 6.9-rc1 发布。▲ Linux 6.9-rc1Linus 表示,Linux 内核 6.9 看起来是一个“相当正常”...【详细内容】
2024-03-25    IT之家  Tags:Linux   点击:(16)  评论:(0)  加入收藏
轻松实现Centos系统的软件包安装管理:yum指令实战详解
yum 是一种用于在 CentOS、Red Hat Enterprise Linux (RHEL) 等基于 RPM 的 Linux 发行版上安装、更新和管理软件包的命令行工具。它可以自动解决软件包依赖关系,自动下载并...【详细内容】
2024-02-27  凡夫贬夫  微信公众号  Tags:Centos   点击:(61)  评论:(0)  加入收藏
Win + Ubuntu 缝合怪:第三方开发者推出“Wubuntu”Linux 发行版
IT之家 2 月 26 日消息,一位第三方开发者推出了一款名为“Wubuntu”的缝合怪 Linux 发行版,系统本身基于 Ubuntu,但界面为微软 Windows 11 风格,甚至存在微软 Windows 徽标。据...【详细内容】
2024-02-27    IT之家  Tags:Ubuntu   点击:(55)  评论:(0)  加入收藏
Linux中磁盘和文件系统工作原理解析
在Linux系统中,一切皆文件的概念意味着所有的资源,包括普通文件、目录以及设备文件等,都以文件的形式存在。这种统一的文件系统管理方式使得Linux系统具有高度的灵活性和可扩展...【详细内容】
2024-02-20  王建立    Tags:Linux   点击:(61)  评论:(0)  加入收藏
Linux子系统概览
inux操作系统是一个模块化的系统,由多个子系统组成。这些子系统协同工作,使Linux能够执行各种任务。了解Linux的子系统有助于更好地理解整个操作系统的运作机制。以下是Linux...【详细内容】
2024-02-01    简易百科  Tags:Linux   点击:(89)  评论:(0)  加入收藏
Linux内核:系统之魂与交互之源
内核,作为任何基于Linux的操作系统的心脏,扮演着至关重要的角色。它不仅是计算机系统软件与硬件之间的桥梁,更是确保系统稳定、高效运行的关键。内核提供了一系列核心功能,为上...【详细内容】
2024-02-01  松鼠宝贝    Tags:Linux内核   点击:(74)  评论:(0)  加入收藏
如何确保Linux进程稳定与持久
在Linux系统中,进程的稳定性与持久性对于维持系统的持续运行至关重要。然而,由于各种原因,进程可能会面临崩溃或系统重启的情况。为了确保关键进程能够持续运行,我们必须采取一...【详细内容】
2024-01-19  松鼠宝贝    Tags:Linux进程   点击:(94)  评论:(0)  加入收藏
相关文章
    无相关信息
站内最新
站内热门
站内头条