引子
前文介绍了使用为了解决外部碎片,使用Buddy System进行连续内存页面的分配,但对于使用内存的程序而言,Buddy System分配的内存粒度过大,假如我们需要动态申请一个内核结构体(占 20 字节),若仍然分配一页内存,这将严重浪费内存。那么该如何分配呢?slab 分配器专为小内存分配而生,由Sun公司的一个雇员Jeff Bonwick在Solaris 2.4中设计并实现。slab分配器分配内存以字节为单位,基于Buddy System的伙伴分配器的大内存进一步细分成小内存分配。换句话说,slab 分配器仍然从 Buddy 分配器中申请内存,之后自己对申请来的内存细分管理。
除了提供小内存外,slab 分配器的第二个任务是维护常用对象的缓存。对于内核中使用的许多结构,初始化对象所需的时间可等于或超过为其分配空间的成本。当创建一个新的slab 时,许多对象将被打包到其中并使用构造函数(如果有)进行初始化。释放对象后,它会保持其初始化状态,这样可以快速分配对象。
SLAB详细思路还可以参考论文:The Slab Allocator: An Object-Caching Kernel Memory Allotor
slub
SLUB是SLAB的改进版本,从版本 2.6.24 开始,SLUB 分配器取代 SLAB,成为 Linux 内核的默认分配器。SLUB 通过减少 SLAB 分配器所需的大量开销,来解决 slab 分配的性能问题,一个改变是,在 SLAB 分配下每个 slab 存储的元数据,移到 Linux 内核用于每个页面的结构 page。此外,对于 SLAB 分配器,每个 CPU 都有队列以维护每个 cache 内的对象,SLUB 会删除这些队列。 对于具有大量处理器的系统,分配给这些队列的内存量是很重要的。因此,随着系统处理器数量的增加,SLUB 性能也更好。
slob
SLOB是用于嵌入式等内存容量不大的场景下的对象分配算法,SLOB 使用简单的链表结构来管理空闲内存块。只维护一个空闲块的链表,所有分配和释放操作都在这个链表上进行。其简单,开销低,但是也有一些缺点如:性能较低,由于采用线性搜索,分配和释放操作的性能较低,尤其在内存块较多时。缺乏复杂功能,如缓存和对象复用,不能满足大型系统的需求。简而言之,它就是用于简单的嵌入式系统这类场景下的。
Slab介绍
包含高速缓存(cache)的主内存区域被划分为多个 slab,每个 slab 由一个或多个连续的页框组成,这些页框既包含已分配的对象(object),也包含空闲的对象
或者说可以用这个图
以下默认皆为linux2.6.11源码(可见于 https://elixir.bootlin.com/linux/v2.6.11/source)
高速缓存
每个高速缓存都是由kmem_cache_t类型(等价于kmem_cache_s类型)的数据结构来描述的
include/linux/slab.h:
/** The slab lists of all objects.* Hopefully reduce the internal fragmentation* NUMA: The spinlock could be moved from the kmem_cache_t* into this structure, too. Figure out what causes* fewer cross-node spinlock operations.*/
struct kmem_list3 {struct list_head slabs_partial; /* partial list first, better asm code */struct list_head slabs_full;struct list_head slabs_free;unsigned long free_objects;int free_touched;unsigned long next_reap;struct array_cache *shared;
};/** kmem_cache_t** manages a cache.*/struct kmem_cache_s {
/* 1) per-cpu data, touched during every alloc/free */struct array_cache *array[NR_CPUS];unsigned int batchcount;unsigned int limit;
/* 2) touched by every alloc & free from the backend */struct kmem_list3 lists;/* NUMA: kmem_3list_t *nodelists[MAX_NUMNODES] */unsigned int objsize;unsigned int flags; /* constant flags */unsigned int num; /* # of objs per slab */unsigned int free_limit; /* upper limit of objects in the lists */spinlock_t spinlock;/* 3) cache_grow/shrink *//* order of pgs per slab (2^n) */unsigned int gfporder;/* force GFP flags, e.g. GFP_DMA */unsigned int gfpflags;size_t colour; /* cache colouring range */unsigned int colour_off; /* colour offset */unsigned int colour_next; /* cache colouring */kmem_cache_t *slabp_cache;unsigned int slab_size;unsigned int dflags; /* dynamic flags *//* constructor func */void (*ctor)(void *, kmem_cache_t *, unsigned long);/* de-constructor func */void (*dtor)(void *, kmem_cache_t *, unsigned long);/* 4) cache creation/removal */const char *name;struct list_head next;/* 5) statistics */...
};typedef struct kmem_cache_s kmem_cache_t;
slab描述符
由于slab的缓存特性,slab 分配器从 buddy 分配器中获取的物理内存称为 内存缓存(与 CPU 的硬件缓存进行区别,下文都称为缓存),使用结构体struct kmem_cache (定义在include/linux/slab.h文件中)描述。
SLAB分配器由可变数量的缓存组成,这些缓存由称为“缓存链”的双向循环链表链接在一起(如下图中的 kmem_cache 链表)。 在slab分配器的上下文中,缓存是特定类型的多个对象的管理器,例如使用cat /proc/slabinfo命令输出的mm_struct 或 fs_cache缓存,其名字保存在kmem_cache->name中(Linux 支持单个最大的 slab 缓存大小为32MB )。kmem_cache 中所有对象的大小是相同的(object_size),并且此 kmem_cache 中所有SLAB的大小也是相同的(gfporder、num)。
每个缓存节点在内存中维护称为slab的连续页块,这些页面被切成小块,用于缓存数据结构和对象。 kmem_cache的 kmem_cache_node 成员记录了该kmem_cache 下的所有 slabs 列表。形成的结构如下图所示。
kmem_cache_noed 记录了3种slab:(可见上方代码中struct kmem_list3部分)
slabs_full :已经完全分配的 slab
slabs_partial: 部分分配的slab
slabs_free:空slab,或者没有对象被分配
以上3个链表保存的是slab 描述符,Linux kernel 使用 struct page 来描述一个slab。单个slab可以在slab链表之间移动,例如如果一个半满slab被分配了对象后变满了,就要从 slabs_partial 中被删除,同时插入到 slabs_full 中去。
如果一个 slab 中的对象全部分配出去了,slab cache 就会将其视为一个 full slab,表示这个 slab 此刻已经满了,无法在分配对象了。slab cache 就会到伙伴系统中重新申请一个 slab 出来,供后续的内存分配使用。
当内核将对象释放回其所属的 slab 之后,如果 slab 中的对象全部归位,slab cache 就会将其视为一个 empty slab,表示 slab 此刻变为了一个完全空闲的 slab。如果超过了 slab cache 中规定的 empty slab 的阈值,slab cache 就会将这些空闲的 empty slab 重新释放回伙伴系统中。
如果一个 slab 中的对象部分被分配出去使用,部分却未被分配仍然在 slab 中缓存,那么内核就会将该 slab 视为一个 partial slab。
这些不同状态的 slab,会在 slab cache 中被不同的链表所管理,同时 slab cache 会控制管理链表中 slab 的个数以及链表中所缓存的空闲对象个数,防止它们无限制的增长。
slab cache 中除了需要管理众多的 slab 之外,还包括了很多 slab 的基础信息。比如:
slab 对象内存布局相关信息。
slab 中的对象需要按照什么方式进行内存对齐。
一个 slab 具体到底需要多少个物理内存页 page,一个 slab 中具体能够容纳多少个 object (内存块)。
对象描述符
每个对象都有一个类型为 kmem_bufctl_t 的描述符,该描述符定义于 include/asm-xx/types.h (xx 代表相应体系结构),是一个无符号整型。
slab 的对象描述符也可以用两种方式存放:
外部对象描述符:存放在 slab 外部,位于高速缓存描述符 slabp_cache 字段指向的一个普通高速缓存中,内存区大小取决于在 slab 中所存放的对象个数(高速缓存描述符的 num 字段)。
内部对象描述符:存放在 slab 内部,正好位于描述符所描述的对象之前。
各描述符之间的关系
高速缓存描述符与 slab 描述符的关系如下:
slab 描述符与对象描述符的关系如下:
普通高速缓存和专用高速缓存
高速缓存被分为普通和专用两种,普通高速缓存只由 slab 分配器用于自己的目的,而专用高速缓存由内核的其余部分使用。
普通高速缓存
第一个高速缓存叫 kmem_cache,包含在 cache_cache 中。
slab 着色
简单概括,slab 着色就是对 slab 使用不同的颜色(不同的偏移量),尽量使得不同的对象的映射到不同的硬件高速缓存行上。着色相关的内容最终被摒弃了(slub 分配器),所以没必要细看了。
分配和释放内存
分配 slab 对象
位于mm/slab.c:
static inline void ** ac_entry(struct array_cache *ac)
{return (void**)(ac+1);
}static inline struct array_cache *ac_data(kmem_cache_t *cachep)
{return cachep->array[smp_processor_id()];
}/*** kmem_cache_alloc - Allocate an object* @cachep: The cache to allocate from.* @flags: See kmalloc().** Allocate an object from this cache. The flags are only relevant* if the cache has no available objects.*/
void * kmem_cache_alloc (kmem_cache_t *cachep, int flags)
{return __cache_alloc(cachep, flags);
}static inline void * __cache_alloc (kmem_cache_t *cachep, int flags)
{unsigned long save_flags;void* objp;struct array_cache *ac;cache_alloc_debugcheck_before(cachep, flags);local_irq_save(save_flags);ac = ac_data(cachep);if (likely(ac->avail)) {STATS_INC_ALLOCHIT(cachep);ac->touched = 1;objp = ac_entry(ac)[--ac->avail];} else {STATS_INC_ALLOCMISS(cachep);objp = cache_alloc_refill(cachep, flags);}local_irq_restore(save_flags);objp = cache_alloc_debugcheck_after(cachep, flags, objp, __builtin_return_address(0));return objp;
}
首先试图从本地高速缓存获得一个空闲对象,如果没有,则调用 cache_alloc_refill() 函数重新填充本地高速缓存并获得一个空闲对象。cache_alloc_refill() 函数比较复杂,这里不展开。
释放 slab 对象
位于mm/slab.c:
/*** kmem_cache_free - Deallocate an object* @cachep: The cache the allocation was from.* @objp: The previously allocated object.** Free an object which was previously allocated from this* cache.*/
void kmem_cache_free (kmem_cache_t *cachep, void *objp)
{unsigned long flags;local_irq_save(flags);__cache_free(cachep, objp);local_irq_restore(flags);
}/** __cache_free* Release an obj back to its cache. If the obj has a constructed* state, it must be in this state _before_ it is released.** Called with disabled ints.*/
static inline void __cache_free (kmem_cache_t *cachep, void* objp)
{struct array_cache *ac = ac_data(cachep);check_irq_off();objp = cache_free_debugcheck(cachep, objp, __builtin_return_address(0));if (likely(ac->avail < ac->limit)) {STATS_INC_FREEHIT(cachep);ac_entry(ac)[ac->avail++] = objp;return;} else {STATS_INC_FREEMISS(cachep);cache_flusharray(cachep, ac);ac_entry(ac)[ac->avail++] = objp;}
}
首先检查本地高速缓存是否有空间给指向一个空闲对象的额外指针,如果有,该指针被加到本地高速缓存然后返回。否则,调用 cache_flusharray() 函数来清空本地高速缓存,再将指针加到本地高速缓存。 同样,cache_alloc_refill() 函数比较复杂,这里不展开。
分配通用对象
位于include/linux/slab.h:
static inline void *kmalloc(size_t size, int flags)
{if (__builtin_constant_p(size)) {int i = 0;
#define CACHE(x) \if (size <= x) \goto found; \else \i++;
#include "kmalloc_sizes.h"
#undef CACHE{extern void __you_cannot_kmalloc_that_much(void);__you_cannot_kmalloc_that_much();}
found:return kmem_cache_alloc((flags & GFP_DMA) ?malloc_sizes[i].cs_dmacachep :malloc_sizes[i].cs_cachep, flags);}return __kmalloc(size, flags);
}
mm/slab.c:
/*** kmalloc - allocate memory* @size: how many bytes of memory are required.* @flags: the type of memory to allocate.** kmalloc is the normal method of allocating memory* in the kernel.** The @flags argument may be one of:** %GFP_USER - Allocate memory on behalf of user. May sleep.** %GFP_KERNEL - Allocate normal kernel ram. May sleep.** %GFP_ATOMIC - Allocation will not sleep. Use inside interrupt handlers.** Additionally, the %GFP_DMA flag may be set to indicate the memory* must be suitable for DMA. This can mean different things on different* platforms. For example, on i386, it means that the memory must come* from the first 16MB.*/
void * __kmalloc (size_t size, int flags)
{struct cache_sizes *csizep = malloc_sizes;for (; csizep->cs_size; csizep++) {if (size > csizep->cs_size)continue;
#if DEBUG/* This happens if someone tries to call* kmem_cache_create(), or kmalloc(), before* the generic caches are initialized.*/BUG_ON(csizep->cs_cachep == NULL);
#endifreturn __cache_alloc(flags & GFP_DMA ?csizep->cs_dmacachep : csizep->cs_cachep, flags);}return NULL;
}EXPORT_SYMBOL(__kmalloc);
释放通用对象
mm/slab.c:
/*** kfree - free previously allocated memory* @objp: pointer returned by kmalloc.** Don't free memory not originally allocated by kmalloc()* or you will run into trouble.*/
void kfree (const void *objp)
{kmem_cache_t *c;unsigned long flags;if (!objp)return;local_irq_save(flags);kfree_debugcheck(objp);c = GET_PAGE_CACHE(virt_to_page(objp));__cache_free(c, (void*)objp);local_irq_restore(flags);
}
总结
深入理解linux内核这本书实在是过老了,里面linux2.6.11内核的许多代码很明显已经存在缺陷并被后续高得多版本的内核进行了优化,就slab而言,还有许多机制如内存池、多cpu等本书没有讲明白或者属于后面slub的优化内容,以及一些内存的分配机制,本书也未说的很清楚。因而也可以看到本文实际上有很多内容并不是非常详细和逻辑相关联,其实根源是为了针对本书的阅读笔记,因为此书确实很多也写作并不逻辑相关。
本人强烈推荐可以继续阅读(https://www.cnblogs.com/binlovetech/p/17288990.html) 的内容来更清楚的了解slab。
参考:
https://www.cnblogs.com/binlovetech/p/17288990.html
https://www.dingmos.com/index.php/archives/23/#
https://freeflyingsheep.github.io/posts/kernel/memory/slab/