kernel: 5.10
Arch: aarch64
上文 介绍过, 伙伴系统在分配内存时是以物理页为单位的,但在很多场景下,内存需要分配的大小是以字节为单位的,达不到一个物理页的大小。如果继续使用伙伴系统进行内存分配, 那么就会出现严重的内存内碎片问题。 内存内碎片指的是已经被分配出去却不能被利用的内存空间, 一般分配的内存空间大于请求所需的内存空间就会产生内存内碎片。
slub分配器就是为了解决小内存分配问题的。
原理
slub分配器依赖于伙伴系统,slub分配器所做的事情就是把伙伴系统分配的大块内存进一步的细分成很多份小块内存进行管理。
如下图所示:
至上而下:
(1) slab缓存: 一个或多个大小相同的slab页会组成一个slab缓存。主要控制slab页的布局。
(2) slab页: 一个slab由一个或多个连续的物理页组成
(3) object对象:细粒度内存分配对象。确定一个size,依照该大小将上面的 SLAB 切分成相同大小的小块内存。
这篇文章描述伙伴系统buddy和slub系统的关系就是批发商和零售商的关系。我觉得很合理。
伙伴系统是某白酒生产商,不过该生产商只按罐来卖。 然后有小的零售商去批发白酒,有的零售商批发10罐,有的零售商批发20罐, 这些零售商就是slab缓存, 批发的一罐一罐的饮白酒就是slab页。 批发好的白酒会继续进行加工,为了形成差异化的竞争,有的零售商包装成500ml每瓶, 有的零售商包装成1000ml每瓶, 这些瓶装酒就是object对象。 消费者想要喝多大容量的酒,就去对应的零售商那里买就可以了。
用户态
内核实现
1. 数据结构
kmem_cache是slab缓存的数据结构
kmem_cache: [include/linux/slub_def.h]
/*
* Slab cache management.
*/
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab;
/* Used for retrieving partial slabs, etc. */
slab_flags_t flags;
unsigned long min_partial;
unsigned int size; /* The size of an object including metadata */
unsigned int object_size;/* The size of an object without metadata */
unsigned int offset; /* Free pointer offset */
#ifdef CONFIG_SLUB_CPU_PARTIAL
/* Number of per cpu partial objects to keep around */
unsigned int cpu_partial;
#endif
struct kmem_cache_order_objects oo;
/* 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_NUMA
unsigned int remote_node_defrag_ratio;
#endif
#ifdef CONFIG_SLAB_FREELIST_RANDOM
unsigned int *random_seq;
#endif
unsigned int useroffset; /* Usercopy region offset */
unsigned int usersize; /* Usercopy region size */
struct kmem_cache_node *node[MAX_NUMNODES];
};
成员 | 描述 |
---|---|
cpu_slab | percpu 类型的 slab, percpu 类型的 slab, 是每个CPU的本地对象缓冲池,主要是为了解决多核之间的锁竞争问题。 |
flags | slab 相关的标志位 |
min_partial | 每个node结点中部分空slab缓冲区数量不能低于这个值 |
size | 一个缓存块(object)所占用的内存空间,包含对齐字节 |
object_size | object 实际大小 |
offset | slub 中利用空闲的 object 内存,来保存下一个空闲 object 的指针,以此组成一个链表结构,该 offset 就是存放 next 指针的基地址偏移,通常情况下是 0。 |
cpu_partial | 限制 cpu_slab 上保存的 partial 链表数量 |
oo | struct kmem_cache_order_objects 的结构体, 低16位表示object 数量,高16位表示slab的order,即slab 占用2 ^ order个页 |
max | 限定 oo 的上限 |
min | 限定 oo 的下限 |
allocflags | 从 buddy 子系统分配内存时使用的掩码 |
refcount | 引用计数, 内存回收机制会用到 |
ctor | 创建slab时的构造函数 |
inuse | 元数据的偏移量 |
align | 对齐字节数 |
red_left_pad | 用于检测左oob |
name | slab缓存的名称 |
list | 链表节点,通过该节点将当前 kmem_cache 链接到 slab_caches 链表中。 |
remote_node_defrag_ratio | 用于NUMA架构,该值越小,越倾向于在本结点分配对象 |
useroffset | usercopy区域的偏移量 |
usersize | usercopy区域的大小 |
mode[MAX_NUMNODES] | 是每个内存节点的共享对象缓冲池。MAX_NUMNODES 就是 NUMA node 节点的数量 |
再着重看下cpu_slab和node, 分别对应kmem_cache_cpu
和kmem_cache_node
这两个数据结构
kmem_cache_cpu: [include/linux/slub_def.h]
struct kmem_cache_cpu {
void **freelist; /* Pointer to next available object */
unsigned long tid; /* Globally unique transaction id */
struct page *page; /* The slab from which we are allocating */
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct page *partial; /* Partially allocated frozen slabs */
#endif
#ifdef CONFIG_SLUB_STATS
unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};
成员 | 描述 |
---|---|
freelist | 指向下一个可用的object。 |
tid | 这是一个用作校验的字段,主要用来判断tid和kmem_cache是否由同一个CPU访问。 |
page | 指向当前使用的slab |
partial | 指向当前cpu上缓存的部分空闲slab链表 |
kmem_cache_node: [mm/slab.h]
struct kmem_cache_node {
spinlock_t list_lock;
unsigned long nr_partial;
struct list_head partial;
};
成员 | 描述 |
---|---|
list_lock | 自旋锁,保护数据 |
nr_partial | 当前 node 上保留的 partial slab 的数量 |
partial | 连接 partial slab 的链表头 |
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 */
};
......
}
2. API
2.1 kmem_cache_create
内核通过kmem_cache_create()
接口来创建一个slab缓存。
struct kmem_cache *
kmem_cache_create(const char *name, unsigned int size, unsigned int align,
slab_flags_t flags, void (*ctor)(void *))
一共有5个参数。
- name: 要创建的slab对象的名称
- size: slab对象的大小
- align: slab对象的对齐大小
- flags: slab内存分配器的掩码和标志位, 比如常用的SLAB_HWCACHE_ALIGN标志位,创建的kmem_cache管理的object按照硬件cache 对齐
- ctor: 对象的构造函数
kmem_cache_create()
函数的流程如下图所示:
kmem_cache_alias()
函数用于查找是否有现成的slab描述符可以使用, 如果有直接退出。create_cache()
函数创建slab描述符kmem_cache_zalloc()
函数分配一个kmem_cache数据结构kmem_cache_open()
函数是核心函数, 对结构体的成员进行初始化。
calcute_sizes() 用于初始化object数目, 大小,分配order等值;
set_min_partial()设置kmem_cache中的min_partial,它表示kmem_cache_node中partial链表可挂入的slab数量;
set_cpu_partial()设置kmem_cache中的cpu_partial,它表示per cpu partial上所有slab中free object总数;
init_kmem_cache_nodes()为每个节点分配kmem_cache_node;
alloc_kmem_cache_cpus()为kmem_cache_cpu变量创建每CPU副本;
2.2 kmem_cache_alloc
get_freelist()
从percpu缓存的页面中获取freelist, 获取成功则返回new_slab_object()
会先调用get_partial()
从Node的partial链表中获取slab页,获取成功则返回; 如果没有获取到,则allocate_slab()
从伙伴系统分配slab页面,并初始化slab页面中的空闲对象get_free_pointer_safe()
获取下一个object对象this_cpu_cmpxhcg_double()
比较object和next object并进行交换处理prefetch_freepointer()
将next_object地址放到cacheline,提高命中率
kmem_cache_alloc() 首先会通过local_irq_save()函数关闭本地中断,防止在处理percpu的slabs 不会因为内核调度而产生变化。
和Buddy System中分配页面类似,slub分配器存在快速路径和慢速路径两种,所谓的快速路径就是per-CPU缓存,可以无锁访问,因而效率更高。
大致流程是:会先判断本地缓冲池有没有空闲的对象,有的话直接获取slab对象,—> 如果没有就会则从Node管理的slab页面中迁移slab页到per-CPU缓存中,再重新分配---->当Node管理的slab页面也不足的情况下,则从Buddy System中分配新的页面,添加到per-CPU缓存中。
3. kmalloc
内核中常用的kmalloc()
函数的核心实现就是slub机制.
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
if (__builtin_constant_p(size)) { --------------- (1)
if (size > KMALLOC_MAX_CACHE_SIZE) --------------- (2)
return kmalloc_large(size, flags);
index = kmalloc_index(size); ------------------- (3)
if (!index)
return ZERO_SIZE_PTR;
return kmem_cache_alloc_trace(
kmalloc_caches[kmalloc_type(flags)][index],
flags, size);
}
return __kmalloc(size, flags);
}
(1) __builtin_constant_p编译器内联函数,判断传入参数是否为常量。如果是变量,直接调用__kmalloc()
函数。
(2) 如果分配的size大于KMALLOC_MAX_CACHE_SIZE , 直接调用kmalloc_large()
函数
#ifdef CONFIG_SLUB
#define KMALLOC_SHIFT_HIGH (PAGE_SHIFT + 1)
#define KMALLOC_MAX_CACHE_SIZE (1UL << KMALLOC_SHIFT_HIGH)
#endif
可以看出KMALLOC_MAX_CACHE_SIZE 大小为2 * PAGE_SIZE = 8k.
(3) 系统启动初期会创建多个管理不同大小对象的kmem_cache。 通过`kmalloc_index()函数查找符合满足分配大小的最小kmem_cache。
static __always_inline unsigned int kmalloc_index(size_t size)
{
if (!size)
return 0;
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 (size <= 64 * 1024 * 1024) return 26;
BUG();
/* Will never be reached. Needed because the compiler may complain */
return -1;
}
比如通过kmalloc(20, GFP_KERNEL)申请内存,系统会从名称“kmalloc-32”管理的slab缓存池中分配一个对象, 即使浪费了12Byte内存。
但是从这个函数来看,最大可以到64MB, 这里应该写的有问题, slab最大宏定义为32M, 64M这个分支多少显得有些多余。
5. 参考资料
图解slub
Slab Memory Allocator
linux内存子系统 - slub 分配器0 - slub原理