slub分配器
- 内存管理
- 2023-08-13
- 7热度
- 0评论
伙伴系统内存分配是以物理页面4KB为单位,但是实际使用的时候不会一下使用到4KB。实际使用中很多情况会以字节为单位。因此为了更精确的划分使用内存,linux内核在伙伴系统之上使用slab分配器来进行管理。截止目前linux内核中从最初slab发展到现在,衍生了slub,slob三种方式。Linux内核通过配置,选择其中一种。本章节主要围绕slub分配器进行说明。
乒乓球的管理
某公司的组织架构如上,公司划分为多个中心(事业部),各事业部再划分为多个二级部门,员工所在的部门就在各二级部门。
公司有一批数量有限的乒乓球作为全公司的公有资产,提供给员工用于日常借用。乒乓球以盒装为单位,每盒有4个,并对盒和球都进行了编号,球归还是也要与盒子对应。为了有限管理这批乒乓球,假设有如下规定:
- 兵乓球被借出去需要明确知道谁借出去了,便于兵乓球资产追溯管理。
- 闲置的球能尽快收回,以便其他人能够使用,使兵乓球利用最大化。
- 员工在借用乒乓球时需要经过部门->中心->公司各级领导的审批。
当前出现一个问题,就是员工想打球的时候,每次都要跨多级走流程,时间周期比较长。好不容易有个空闲个时间想打个球,等漫长的流程走完活又来了,没时间打了,但是也不能提前先把球借了,闲置屯着,这样别人想打也打不了,每个人都这样,那实际想打球的也无球可打。
为了解决这个问题,小明同学于是设计了这么一套方案,让员工想打球的时候能够快速获得兵乓球,也让公司的球能够利用最大化。
借球
部门主管向公司申请一盒的球(每盒有4个),然后将球分给D同学。并将该盒乒乓球做标记,在部门内部宣导,此后谁要是向借用球,可直接从该盒从获取,自觉做好登记皆可(不再需要走漫长流程)。
有一天部门集体运动,一下子需要多个球,部门主管发现原来的一盒已经不够了,所以又向公司再借了3盒(公司以盒为单位借出,方便管理)。
1、2、3号盒球已经全部借出,所以部门没有再管理了(盒子没啥好管理的,由对应借出的同学共同保管盒子,盒子变成没人要的“孩子”),部门当前管理的是4号盒子(盒子里面还有球,如果还有谁要借,自己拿并自觉做好登记就行)。
还球
借球人借球时需记住自己从那个盒子里面拿的球,还球时需要找到对应的盒子还球。1号、2号、3号盒分别有人归还了球,但是还没有还满,此前的空盒子是不需要管理的,但是现在盒子里面现在有球了,那必须得管理起来了(一旦盒子非空,就需要管理起来了),直接退还给公司也不行啊,一个是盒子没还满,另外一个就是下次又有大需求量还得走流程申请慢。
所以索性部门先将这些盒子管理起来,当前部门累计球数有9个球,闲置在二级部门太多球,也不行,每个二级部门都闲置很多球,球总有耗尽的时候,就有可能其他二级部门的员工没法获得球了,于是公司做了规定,每个部门闲置的球数不能大于4个,因此只有1号和2号盒子可以继续由部门管理,而3号盒子可以不归还公司,但可以由中心先保管。
当盒子不再为空时,先由部门进行管理,当部门的球数闲置超过一定数量后,需要交给由中心管理,交给中心管理的单位也是以盒子为单位(还球的人是根据盒子编号找还球的),这样的好处就是,当有该中心的其他二级部门需要借用球是,发现部门没球了,可以先中心是否有球,如果中心有,那就从中心拿就行,不用从公司申请,这样流程虽然没这么快,但相比跟公司申请的流程也有些优化。
长期进行下去,中心可能会很多个盒子,同时有些盒子是满的(球都还完了),中心也不能由太多闲置球,否则其他中心就可能没法从公司申请到球了,所以公司规定,对中心管理的盒子数量进行了闲置,如盒子数量不能超过10个,当超过10个时,满盒子的球需要归还给公司。
再论借球
当二级部门员工进行借球时,可以从4号盒子免报备申请直接登记拿球就好,这样的借球周期是最短的。而1号和2号盒子虽也归部门管理,但是员工不能从这些盒子里面拿,为了管理效率,二级部门只开个了一个盒子的权限(免报备直接登记即可获取),所以借球只能先从4号盒子拿,当4号盒子被拿完了之后,就需要跟主管报备,看部门还没有备用,发现还有,那就将1号盒子再拿出去,此后大家就又可以从1号盒子免报备借球。当部门没有备用球了,就先问中心有没有,如果中心有,那就将先用中心的,从中心拿到的盒子就归部门管理了。如果中心也没有了,那就只能走流程从公司申请了。
slub 基本原理
一块缓存 = nslab,slab = m * obj,slab = k page。系统从伙伴系统中分配一个或多个连续的物理页组成一个slab,然后将slab切分为n个相同大小的内存(obj),提供给linux内核系统使用,这些相同obj大小组成一个集合。
可以通过cat /proc/slabinfo查看系统中slab的信息。
- actives_objs:已经分配出去的对象数量
- num_objs:一共有多少个对象,包含使用的和未使用的。
- objsize:每个对象的大小是多少,单位是字节
- objperslab:每个slab中的对象数量是多少。
- pagesperslab:每个slab对应的page数量
- limit/batchcount/sharedfactor:这些是可调整的参数,使用slub分配器没有使用
- actives_slabs: 非游离状态的slab数量
- nums_slabs:一共有多少slab
- sharedavail:待记录
slub函数接口
关键数据结构
设计思想
为什么kmem_cache中分为每个cpu分配对应有缓存池,每个节点有对应的缓存池。每个cpu分配的缓存池又再划分为当前正在使用的缓存slab,以及备用缓存slab(Per-CPU partial)?
访问Per-CPU slab是不需要加锁的,所以获取速度很快。访问node slab是需要加锁的,因为这是多个cpu共享的slab,访问速度慢。
而中间的Partial slab是方便Current slab的,系统分配slab必须要从current slab中分区对象,当current slab对象使用完时,就会从依次L2从L3中获取新的slab变成Per-CPU current。因为L3是需要加锁,为了进一步解决这速率问题,中间加了Per-CPU partial slab,当Per-CPU current slab中的所有对象被分配完后将会被移除变成游离状态,而当系统释放当前处理游离状态的full slab中对象时其就会变成部分full 对象的slab,其会被再次从游离状态添加到链表中等待系统从中分配,因此这个slab就会被添加到Partial slab链表中,当下次current slab中没有可用对象时,再将其Partial 链表中的slab置为current slab,这样就不用从Node Partial slab中获取,减少锁的使用,提高系统使用率。
kmem_cache
struct kmem_cache是管理slub分配器的的基础数据结构。
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; 一个cpu对应一个本地内存缓存池
slab_flags_t flags;
unsigned long min_partial; 限制struct kmem_cache_node中partial链表slab的数量,如果slab数量超过这个值,那么多余的slab需要被释放会伙伴系统。
unsigned int size; 分配object的大小,包含一些管理数据。
unsigned int object_size; object对象的内存大小,用户层每次分配大小。
struct reciprocal_value reciprocal_size;
unsigned int offset; 用于寻找object的地址
#ifdef CONFIG_SLUB_CPU_PARTIAL
/* Number of per cpu partial objects to keep around */
unsigned int cpu_partial; 每CPU中slab的空闲对象最大值,当超过这个值,需要将slab转移到kmem_cache_node的partial链表
#endif
struct kmem_cache_order_objects oo; 低16代表一个slab中的object数量,高16代表一个slab需要多个page数量。
/* Allocation and freeing of slabs */
struct kmem_cache_order_objects max;
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 *);
unsigned int inuse; /* Offset to metadata */
unsigned int align; /* Alignment */
unsigned int red_left_pad; /* Left redzone padding size */
const char *name; /* Name (only for display!) */
struct list_head list; /* List of slab caches */
#ifdef CONFIG_SYSFS
struct kobject kobj; /* For sysfs */
#endif
#ifdef CONFIG_SLAB_FREELIST_HARDENED
unsigned long random;
#endif
#ifdef CONFIG_NUMA
/*
* Defragmentation by allocating from a remote node.
*/
unsigned 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
unsigned int useroffset; /* Usercopy region offset */
unsigned int usersize; /* Usercopy region size */
struct kmem_cache_node *node[MAX_NUMNODES];NUMA系统中,每个node都有一个slab缓存池。
};
kmem_cache_cpu
每个cpu都有个自己的slab缓存池,使用struct kmem_cache_cpu来描述每个cpu自己所属的缓存池.
struct kmem_cache_cpu {
void **freelist; 指向下一个可用的object地址
unsigned long tid; /* Globally unique transaction id */
struct page *page; 指向当前正在使用的slab地址,只有一个slab。复用struct page来描述一个slab
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct page *partial; 指向slab中只有一部分空闲object的地址,可能存在多个部分空闲对象的slab,slab直接通过struct page中的next链表进行串联起来。
与上一个的区别:这是一个slab集合,而上一个只有一个slab,表示正在使用的slab ,当正在使用的slab中object对象全部用完后,就会变成一个full slab将会被游离出去,而当slab中某个object被释放后,就变成了存在部分空闲对象的slab,这个slab将会被重新被添加到partial中去。
#endif
local_lock_t lock; /* Protects the fields above */
#ifdef CONFIG_SLUB_STATS
unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};
kmem_cache_node
struct kmem_cache_node {
spinlock_t list_lock;
#ifdef CONFIG_SLUB
unsigned long nr_partial; 节点中slab的数量
struct list_head partial; 用于将各个slab串起来的链表
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs;
atomic_long_t total_objects;
struct list_head full;
#endif
#endif
};
struct page
复用struct page结构体来描述slub。
struct page {
unsigned long flags; 设置标志位,PG_slab,表示页属于SLUB内存管理器
union {
struct { /* slab, slob and slub */
union {
struct list_head slab_list; 用于将slab添加到partial部分空闲链表
struct { /* Partial pages */
struct page *next;
int pages; /* Nr of pages left */
int pobjects; /* Approximate count */
};
};
struct kmem_cache *slab_cache; /* not slob */索引page所所属的kmem_cache
void *freelist; /* first free object */ 指向slab中第一个空闲对象
union {
void *s_mem; /* slab: first object */
unsigned long counters; /* SLUB */
struct { /* SLUB */
unsigned inuse:16; 当前slab已分配对象的数量
unsigned objects:15; 当slab所包含对象的总数
unsigned frozen:1; slab是否缓存到Per-CPU缓存池(冻结),
};
};
};
} _struct_page_alignment;
slub重要概念
内核中通过一下配置来使能SLUB内存管理。
CONFIG_SLUB_DEBUG=y
CONFIG_SLUB=y
CONFIG_SLAB_MERGE_DEFAULT=y
CONFIG_SLUB_CPU_PARTIAL=y
CONFIG_MEMCG_KMEM=y
# CONFIG_SLAB_FREELIST_RANDOM is not set
# CONFIG_SLAB_FREELIST_HARDENED is not set
对象内存组织
对象的内部组织如上,有两种布局方式,主要区别是指向下一个空闲对象的指针存储方式不同。当flags设置了SLAB_TYPESAFE_BY_RCU/SLAB_POISON/ctor构造函数不为空则使用第一种方式,即下一个空闲对象的指针放到当前空闲对象的末端,占据8个字节(64bit)空间;反之使用第二种方式,复用当前对象的空间,存放下一个对象的地址再起始地址。
同时如果使能了CONFIG_SLUB_DEBUG,对象内部的布局会新增用于跟踪分配/释放的用户,便于调试。下面是4个对象的示意图。
struct kmem_cache_cpu::freelist和struct page::freelist这两个都是用于指向第一个空闲对象的地址,其中struct page * page->freelist指向内存节点空闲链表slab中的第一个空闲对象,当这个slab被设置为活动slab后,表示当前该slab正在被使用,page->frozen=1,表示已经处于冻结,那么page->freelist=NULL,slab中第一个空闲对象地址被存放到cpu_slab->freelist中。在分配对象时,值需要将当前freelist地址返回,让后将freelist地址更新到下一个空闲对象的起始地址即可。
slub的挂载和活动的slub
系统中的Slub的可以认为被挂载在4个地方:
- 正在使用的slub(只有一个slub):cpu_slab->page指向的slub。
- Per-CPU partial上的slub:cpu_slab->partial指向的slub,用链表组织起来,可以存在多个。
- Node节点partial的slub:node->partial指向的slub,用链表组织起来,每个节点对应一个链表,每个链表有可以多个slub。
- 游离状态的slub:前三种slub中的至少还有有部分对象未被使用,处于空闲,当slub中所有对象都被用完时,将会移除,相当于游离状态,如果打开了SLUB_DEBUG,这些slub会被串到链表上。
分配内存对象,都是从kmem_cache中cpu_slab->freelist上获取,该slub为正在使用的slub,也称为活动的slub(自己命名的),即使当cpu_slab->freelist为空或者cpu_slab->page为空,从Per-CPU partial或者Node partial中获取slub时,其就会被设置为活动的slub。
创建kmem_cache
分配slub cache
第一次分配
Slub刚创建的时候并没有实际分配内存,所以kmem_cache中无论时cpu_slab还是node节点中,都没有slab缓存,第一次申请的时候会从伙伴系统分配页面生成一个slab,然后取其中一个object返回给系统。此后,在没有分配完成当前slab中的object时,分配内存直接返回freelist就是对应的空闲object内存。
从Per CPU partial中分配获取
从当前活动的slab中无法分配到object,那就从Per CPU partial上进行获取一个slab进行分配,设置为活动的slab,此前的活动的slab就会被游离出去(full slab,如果开了SLUB DEBUG,会添加到这个debug链表中,如果没有开,就相当于没有要的孩子,当释放对象的时候会通过对应的slab描述符号相关成员找出来。)。
从Node Partial中获取
如果从Per CPU partial中依旧没法后去到slab,就会从node partial中获取slab,然后将其设置为活动的slab,此前活动的slab设置为游离状态。
释放slub cache
- Slab中对象部分被使用(非游离):① ② ③场景
(1)释放了obj后,slab中还存在部分obj未释放:直接释放,建立好空闲obj之间的联系即可。
(2)释放了obj后,slab所有的obj都为空闲:如果在node partial上,nr_partial>min_partial,表明节点上存在的slab数量超过上限,空闲的slab会回收到伙伴系统中;如果是在Per-CPU partial上,管理的slab中空闲object数量大于cpu_partial(kmem_cache成员),将该slab移动到node partial链表上管理。 - Slab中对象全部被使用(游离):④⑤场景
释放了obj之后,slab变成部分空闲的slab,由于此前是处于游离状态,没有添加到对应链表管理(未开SLUB DEBUG),当变成部分空闲是,就需要将其进行管理。首先尝试将slab添加到Per-CPU partial中,如果Per-CPU超过阈值没法管理了,就添加到Node parital中。
kmalloc
函数接口
kmalloc实现
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
1.判断参数是否为常数
if (__builtin_constant_p(size)) {
#ifndef CONFIG_SLOB
unsigned int index;
#endif
if (size > KMALLOC_MAX_CACHE_SIZE)
return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
index = kmalloc_index(size);
if (!index)
return ZERO_SIZE_PTR;
return kmem_cache_alloc_trace(
kmalloc_caches[kmalloc_type(flags)][index],
flags, size);
#endif
}
return __kmalloc(size, flags);
2.直接走这里
}
void *__kmalloc(size_t size, gfp_t flags)
{
struct kmem_cache *s;
void *ret;
1.如果分配空间大于KMALLOC_MAX_CACHE_SIZE,直接从伙伴系统进行分配
if (unlikely(size > KMALLOC_MAX_CACHE_SIZE))
return kmalloc_large(size, flags);
2.创建slab的数据结构,实际上建立的是全局kmem_cache
s = kmalloc_slab(size, flags);
if (unlikely(ZERO_OR_NULL_PTR(s)))
return s;
3.slab分配器
ret = slab_alloc(s, flags, _RET_IP_, size);
trace_kmalloc(_RET_IP_, ret, size, s->size, flags);
ret = kasan_kmalloc(s, ret, size, flags);
return ret;
}
struct kmem_cache *kmalloc_slab(size_t size, gfp_t flags)
{
unsigned int index;
struct kmem_cache *s = NULL;
if (size <= 192) {
if (!size)
return ZERO_SIZE_PTR;
1.根据分配大小找到对应的index系数,用于后续查找对应的kmem_cache
index = size_index[size_index_elem(size)];
} else {
if (WARN_ON_ONCE(size > KMALLOC_MAX_CACHE_SIZE))
return NULL;
index = fls(size - 1);
}
trace_android_vh_kmalloc_slab(index, flags, &s);
if (s)
return s;
2.在全局kmalloc_caches数组中找到对应的kmem_cache示例返回。
return kmalloc_caches[kmalloc_type(flags)][index];
}
static u8 size_index[24] __ro_after_init = {
3, /* 8 */
4, /* 16 */
5, /* 24 */
5, /* 32 */
6, /* 40 */
6, /* 48 */
6, /* 56 */
6, /* 64 */
1, /* 72 */
1, /* 80 */
1, /* 88 */
1, /* 96 */
7, /* 104 */
7, /* 112 */
7, /* 120 */
7, /* 128 */
2, /* 136 */
2, /* 144 */
2, /* 152 */
2, /* 160 */
2, /* 168 */
2, /* 176 */
2, /* 184 */
2 /* 192 */
};
启动阶段创建kmem_cache
系统启动初期调用create_kmalloc_caches创建多个管理不同大小对应的kmem_cache,最大的size一般是8K,也就是对应的是kmalloc-8192,当系统通过kmalloc申请内存时,会直接从其中获取。
void __init create_kmalloc_caches(slab_flags_t flags)
{
int i;
enum kmalloc_cache_type type;
if (android_kmalloc_64_create)
for (type = KMALLOC_NORMAL; type <= KMALLOC_RECLAIM; type++)
new_kmalloc_cache(6, type, flags);
/*
* Including KMALLOC_CGROUP if CONFIG_MEMCG_KMEM defined
*/
for (type = KMALLOC_NORMAL; type <= KMALLOC_RECLAIM; type++) {
for (i = KMALLOC_SHIFT_LOW; i <= KMALLOC_SHIFT_HIGH; i++) {
if (!kmalloc_caches[type][i])
new_kmalloc_cache(i, type, flags);
/*
* Caches that are not of the two-to-the-power-of size.
* These have to be created immediately after the
* earlier power of two caches
*/
if (KMALLOC_MIN_SIZE <= 32 && i == 6 &&
!kmalloc_caches[type][1])
new_kmalloc_cache(1, type, flags);
if (KMALLOC_MIN_SIZE <= 64 && i == 7 &&
!kmalloc_caches[type][2])
new_kmalloc_cache(2, type, flags);
}
}
/* Kmalloc array is now usable */
slab_state = UP;
#ifdef CONFIG_ZONE_DMA
for (i = 0; i <= KMALLOC_SHIFT_HIGH; i++) {
struct kmem_cache *s = kmalloc_caches[KMALLOC_NORMAL][i];
if (s) {
kmalloc_caches[KMALLOC_DMA][i] = create_kmalloc_cache(
kmalloc_info[i].name[KMALLOC_DMA],
kmalloc_info[i].size,
SLAB_CACHE_DMA | flags, 0,
kmalloc_info[i].size);
}
}
#endif
}
const struct kmalloc_info_struct kmalloc_info[] __initconst = {
INIT_KMALLOC_INFO(0, 0),
INIT_KMALLOC_INFO(96, 96),
INIT_KMALLOC_INFO(192, 192),
INIT_KMALLOC_INFO(8, 8),
INIT_KMALLOC_INFO(16, 16),
INIT_KMALLOC_INFO(32, 32),
INIT_KMALLOC_INFO(64, 64),
INIT_KMALLOC_INFO(128, 128),
INIT_KMALLOC_INFO(256, 256),
INIT_KMALLOC_INFO(512, 512),
INIT_KMALLOC_INFO(1024, 1k),
INIT_KMALLOC_INFO(2048, 2k),
INIT_KMALLOC_INFO(4096, 4k),
INIT_KMALLOC_INFO(8192, 8k),
INIT_KMALLOC_INFO(16384, 16k),
INIT_KMALLOC_INFO(32768, 32k),
INIT_KMALLOC_INFO(65536, 64k),
INIT_KMALLOC_INFO(131072, 128k),
INIT_KMALLOC_INFO(262144, 256k),
INIT_KMALLOC_INFO(524288, 512k),
INIT_KMALLOC_INFO(1048576, 1M),
INIT_KMALLOC_INFO(2097152, 2M),
INIT_KMALLOC_INFO(4194304, 4M),
INIT_KMALLOC_INFO(8388608, 8M),
INIT_KMALLOC_INFO(16777216, 16M),
INIT_KMALLOC_INFO(33554432, 32M)
};
static __always_inline unsigned int __kmalloc_index(size_t size,
bool size_is_constant)
{
if (!size)
return 0;
if (android_kmalloc_64_create && size <= 64)
return 6;
if (size <= KMALLOC_MIN_SIZE)
return KMALLOC_SHIFT_LOW;
if (KMALLOC_MIN_SIZE <= 32 && size > 64 && size <= 96)
return 1;
if (KMALLOC_MIN_SIZE <= 64 && size > 128 && size <= 192)
return 2;
if (size <= 8) return 3;
if (size <= 16) return 4;
if (size <= 32) return 5;
if (size <= 64) return 6;
if (size <= 128) return 7;
if (size <= 256) return 8;
if (size <= 512) return 9;
if (size <= 1024) return 10;
if (size <= 2 * 1024) return 11;
if (size <= 4 * 1024) return 12;
if (size <= 8 * 1024) return 13;
if (size <= 16 * 1024) return 14;
if (size <= 32 * 1024) return 15;
if (size <= 64 * 1024) return 16;
if (size <= 128 * 1024) return 17;
if (size <= 256 * 1024) return 18;
if (size <= 512 * 1024) return 19;
if (size <= 1024 * 1024) return 20;
if (size <= 2 * 1024 * 1024) return 21;
if (size <= 4 * 1024 * 1024) return 22;
if (size <= 8 * 1024 * 1024) return 23;
if (size <= 16 * 1024 * 1024) return 24;
if (size <= 32 * 1024 * 1024) return 25;
if ((IS_ENABLED(CONFIG_CC_IS_GCC) || CONFIG_CLANG_VERSION >= 110000)
&& !IS_ENABLED(CONFIG_PROFILE_ALL_BRANCHES) && size_is_constant)
BUILD_BUG_ON_MSG(1, \\\\\\\"unexpected size in kmalloc_index()\\\\\\\");
else
BUG();
/* Will never be reached. Needed because the compiler may complain */
return -1;
}