目录
1. 前言
本专题我们开始学习内存管理部分,本文主要参考了《奔跑吧, Linux内核》、ULA、ULK的相关内容。
本文主要关注内核地址空间的管理部分,主要包括buddy管理,slab,以及非连续物理内存分配vmalloc。
本文为slab分配器的学习笔记。
1.slab用来解决小内存块分配问题,不同于buddy以页为单位分配
2.slab实际也是通过buddy来分配,只不过可以在buddy上层实现自己的分配算法,来对小内存块进行管理
3.slab分配器主要有如下的接口:
struct kmem_cache * kmem_cache_create(const char *name, size_t size, size_t align,
unsigned long flags, void (*ctor)(void *))
创建slab描述符, 其中name为slab描述符名,size缓存对象大小,align为对齐,flags分配掩码,
ctor对象构造函数
void kmem_cache_destroy(struct kmem_cache *s)
释放slab描述符
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
分配缓存对象
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
释放缓存对象
kernel版本:5.10
平台:arm64
2. slab总体说明
slab系统由slab描述符(struct kmem_cache),slab节点(struct kmemcache_node),本地对象缓冲池(struct array_cache array[]),共享对象缓冲池(struct array_cache *shared), 3个slab链表,n个slab,以及slab中众多的缓存对象obj组成。slab系统架构如上图所示。
- slab描述符(struct kmem_cache)
内核采用kmem_cache对内核对象进行管理,称为slab描述符,是slab系统的核心数据结构,Slab分配器为每种内核对象建立单独的slab描述符(kmem_cache),描述slab中对象多少;由于 kmem_cache 自身也是一种内核对象,所以需要一个专门的缓冲区。所有缓冲区的 kmem_cache 控制结构被组织成以 slab_caches 为队列头的一个双向循环队列 - slab节点(struct kmemcache_node)
对于numa特性,每个numa node创建一个slab节点,slab节点的数目为NUMA_NODES,对每个NUMA node都有三个slab链表:
(1)Full 链表,该链表中的所有slab中的对象都已经被完全分配,没有空闲对象。
(2)Partial 链表,该链表中的slab中还有空闲对象,从slab中分配对象时,优先从此链表中分配。当 slab 的最后一个已分配对象被释放时,该 slab 将从 Partial 链表移入 free链表;当 slab 的最后一个空闲对象被分配时,该 slab 将从Partial 链表移入Full 链表里。
(3)Free链表, 该链表中的slab中全是空闲对象,当partial链表中slab用尽时,从这里分配,并将相应的slab从free链表移入partial链表。
当缓冲区中空闲对象总数不足时,则按需从伙伴系统中分配更多的page,用于slab;当空闲对象比较富余,free链表中的的部分 slab 可能被定期回收。 - slab链表节点
每个slab链表节点代表一个slab, 每个slab可能有多个page组成,其第一个page->slab_list会挂载到相应的slab链表 - 本地对象缓冲池/共享对象缓冲器(struct array_cache)
slab描述符为每个CPU提供了一个本地对象缓冲池,也会有一个所有CPU共享的对象缓冲池,slab节点的shared指向了所有cpu共享的对象缓冲池。entry是其数据成员,空闲对象会保存到slab描述符的本地对象缓冲池或共享缓冲池中;
slab分配时将按照如下的优先顺序进行分配:
本地对象缓冲池 -> 共享对象缓冲池 -> Partial 链表 -> Free链表 -> buddy
slab释放时将按照如下的优先顺序进行释放:
本地对象缓冲池 -> 共享对象缓冲池 -> Partial 链表 /Free链表 -> buddy
注意:本地对象缓冲池或共享缓冲池中都不保存真正的对象,而是保存slab对象的指针,真正的对象仍然保存在高速缓存的slab中。 - slab
每个 slab包含一组连续的物理内存页,这些内存页被划分成固定数目的对象。根据对象大小的不同,缺省情况下一个 slab 最多可以由 1024 个物理内存页框构成。充分利用硬件特性,需要对每个对象进行一定的对齐处理(比如cache line对齐),所以slab 中分配给对象的内存可能大于用户要求的对象实际大小,可能会有一定的内存浪费。 根据slab类型不同,slab管理信息可以放置到slab内部或slab外部,每个slab对象要占用1Byte来存放到slab的freelist,一般是对象的编号。slab已经没有专门的数据结构表征它,一个slab是通过此slab的首个page来代表,page中有专门表征slab的成员,如slab_list用于链接到slab节点的三个链表之一。 - freelist
freelist可以放在slab内部也可以放在slab外部,freelist的作用其实非常非常简单,就是记录slab中没有使用的object。freelist实际上是一个数组,数组中存放未使用的object的index值。
3. kmem_cache_create
kmem_cache_create(name, size, align, flags, void (*ctor)(void *))
\--kmem_cache_create_usercopy(name, size, align, flags, 0, 0,ctor)
|--s = __kmem_cache_alias(name, size, align, flags, ctor)
|--cache_name = kstrdup_const(name, GFP_KERNEL)
\--create_cache(cache_name, size,calculate_alignment(flags, align, size),
flags, useroffset, usersize, ctor, NULL);
|--s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL)
|--初始化kmem_cache
|--__kmem_cache_create(s, flags)
\--list_add(&s->list, &slab_caches)
kmem_cache_create用于创建slab描述符,但是并未真正的为slab分配空间,只是创建了管理slab相关的结构体。
1.__kmem_cache_alias
查找是否有现成的slab描述符可用
2.kmem_cache_zalloc
如果没有现成的slab描述符,分配一个数据结构kmem_cache的slab描述符
3.初始化slab描述符kmem_cache
slab描述符名字、对象实际大小、对象大小(包括对齐)、对齐长度等
4.__kmem_cache_create
初始化slab描述符,此处也会创建每CPU对象缓冲池
5.list_add(&s->list, &slab_caches)
将新创建的slab描述符加到全局的链表slab_caches
|- -__kmem_cache_create
__kmem_cache_create
| //检查创建的缓冲区大小是否是word对齐
|--size = ALIGN(size, BYTES_PER_WORD)
|--计算align对齐大小
|--SLAB_RED_ZONE检查是否溢出,实现调试功能
| //colour_off表示着色区长度,与L1高速缓存行大小同
|--cachep->colour_off = cache_line_size()
| //检查slab系统的状态,如果slab完全初始化完成则为FULL,使用GFP_KERNEL
|--slab_is_available()
|--set_objfreelist_slab_cache(cachep, size, flags)
| | //slab需要分配多少页面
| |-- left=calculate_slab_order(cachep, size,flags | CFLGS_OBJFREELIST_SLAB)
| |--cachep->colour = left / cachep->colour_off//着色区的个数
| //如果对象大小大于128B,则设置标志flags |= CFLGS_OFF_SLAB
|--set_off_slab_cache(cachep, size, flags)
| |--left = calculate_slab_order(cachep, size, flags | CFLGS_OFF_SLAB)
| |--cachep->colour = left / cachep->colour_off
|--set_on_slab_cache(cachep, size, flags)
| |--left = calculate_slab_order(cachep, size, flags)
| |--cachep->colour = left / cachep->colour_off
|--cachep->freelist_size = cachep->num * sizeof(freelist_idx_t)
|--setup_cpu_cache(cachep, gfp)
-
Linux5.0内核支持3种slab内存布局:
(1)OBJFREELIST_SLAB模式:linux4.6引入,使用slab分配器中最后一个slab对象的空间作为管理区;
(2)OFF_SLAB模式:管理区不在slab中,额外分配的内存用于管理区
(3)正常模式。传统布局模式 -
三种布局方式都会调用calculate_slab_order,calculate_slab_order主要完成:
(1)需要多少连续的物理页面,通过cache_estimate预估
(2)一个slab包含多少个slab对象,保存到cachep->num;
(3)返回剩余空间保存到left;
通过一个slab剩余空间left来计算包含多少个着色区,保存到cachep->colour; -
cachep->freelist_size保存了slab管理区大小
-
setup_cpu_cache初始化per-cpu本地高速缓存array_cache数组
|- - -setup_cpu_cache
setup_cpu_cache
\--enable_cpucache(cachep, gfp)//如果slab_state为FULL,即slab初始化机制完成则调用此函数
|--根据空闲对象大小计算空闲对象个数的最大阀值limit
|--计算share变量的值,与共享缓存池中空闲slab对象的数目有关
|--计算batchcount的值,一般为limit的一半,用于本地缓冲池和共享缓冲池填充对象的数量
\--do_tune_cpucache(cachep, limit, batchcount, shared, gfp)
| //分配本地对象缓冲池
|--cpu_cache = alloc_kmem_cache_cpus(cachep, limit, batchcount)
|--for_each_online_cpu(cpu)
| free_block(cachep, ac->entry, ac->avail, node, &list)
\--setup_kmem_cache_nodes(cachep, gfp)
\--for_each_online_node(node)
setup_kmem_cache_node(cachep, node, gfp, true)
|--if (cachep->shared)
| alloc_arraycache(node,cachep->shared * cachep->batchcount,...)
|--init_cache_node(cachep, node, gfp)
\--free_block(cachep, n->shared->entry,n->shared->avail, node, &list)
enable_cpucache
主要用于初始化本地缓存池和共享缓存池,同时会配置slab描述符,其中:
(1)limit为slab中最大空闲对象数量,大于此将释放batchcount个对象到buddy;
(2)batchcount为本地对象缓冲池和共享对象缓冲池一次互相迁移的对象数目,一般为limit的一半
(3)share变量值与共享缓存池中空闲slab对象的数目有关
do_tune_cpucache
(1)首先通过alloc_kmem_cache_cpus分配本地对象缓冲池,并通过free_block清空本地对象缓冲池;
(2)setup_kmem_cache_nodes主要创建slab描述符的各个slab节点,每个numa node一个,并初始化。此处会通过alloc_arraycache创建共享对象缓冲区;通过init_cache_node初始化node的slab节点,并通过free_block来清空本地对象缓冲池。注意此处slab节点的初始化主要包含:
free_objects表示3个链表中空闲对象的总和;free_limit表示所有slab上容许空闲对象的最大数目
4. kmem_cache_alloc
kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
\--slab_alloc(cachep, flags, _RET_IP_)
\--objp = __do_cache_alloc(cachep, flags)
\--objp = ____cache_alloc(cache, flags)
|--check_irq_off()
|--ac = cpu_cache_get(cachep)
|--if (likely(ac->avail))
| ac->touched = 1
| objp = ac->entry[--ac->avail]
| goto out;
\--objp = cache_alloc_refill(cachep, flags)
kmem_cache_alloc用于分配slab对象,此处会真正的调用buddy来分配内存
check_irq_off:slab分配要求在关闭中断的条件下进行,为何?(TODO)
cpu_cache_get获取slab描述符cachep的本地对象缓冲池ac, 如果本地对象缓冲池有空闲,则从本地对象缓冲池分配,其中ac->entry数组就保存着空闲的slab指针,如果有空闲,则slab对象分配到此就结束了,否则将通过cache_alloc_refill进一步分配
注:由于slab基于buddy,是在一个大块连续物理内存上分配,因此其分配的slab对象也是物理地址连续的。
|- -cache_alloc_refill
cache_alloc_refill
|--check_irq_off()
| //获取当前cpu关联的numa nid
|--node = numa_mem_id()
| //获取当前cpu的本地slab对象缓存池
|--ac = cpu_cache_get(cachep)
| //每次从共享对象缓冲池迁移的空闲对象个数
|--batchcount = ac->batchcount;
|--n = get_node(cachep, node)//获取当前numa node的slab节点
| //尝试从共享对象缓冲池迁移空闲对象,如果成功则完成分配
|--transfer_objects(ac, shared, batchcount)
| //如果共享缓冲池没有,则需要进一步查看partial甚至free链表
|--while (batchcount > 0)
| //尝试从partial链表甚至free链表拿空闲slab
| page = get_first_slab(n, false)
| //从上步得到的slab迁移batchcount个对象到本地缓冲池
| batchcount = alloc_block(cachep, ac, page, batchcount)
| //将拿到的slab迁移到partial或full链表
| fixup_slab_list(cachep, n, page, &list)
| //如果partial/free链表中没有空闲slab则要从buddy分配
|--if (unlikely(!ac->avail))
page = cache_grow_begin(cachep, gfp_exact_node(flags), node)
|- - -cache_grow_begin
cache_grow_begin(cachep, gfp_exact_node(flags), node)
|--page = kmem_getpages(cachep, local_flags, nodeid)
|--page_node = page_to_nid(page)
|--n = get_node(cachep, page_node) //获取slab节点
| //n->colour_next表示slab节点中下一个slab应该包括的着色区数目
| n->colour_next++;
| offset = n->colour_next
| offset *= cachep->colour_off//计算slab的着色区大小
|--freelist=alloc_slabmgmt(cachep,page,offset,local_flags&~GFP_CONSTRAINT_MASK,page_node)
| |--void *addr = page_address(page)
| |--page->s_mem = addr + colour_off
| |--page->active = 0
| | //freelist保存了freelist管理区的地址
| \--freelist=addr+(PAGE_SIZE << cachep->gfporder)-cachep->freelist_size
|--slab_map_pages(cachep, page, freelist)
| |--page->slab_cache = cache;
| \--page->freelist = freelist
\--cache_init_objs(cachep, page)
kmem_getpages分配gfporder个页面用于一个slab,主要通过buddy分配
n->colour_next表示slab节点中下一个slab应该包括的着色区数目,从0开始增加,每个slab加1,直到这个slab描述符的着色区最大值cachep->colour,然后又从0开始计算。着色区大小cachep->colour_off与L1的cache line大小相同,这样可以提高高速缓存的访问效率?
alloc_slabmgmt用于分配slab管理数据数据,其中addr 为slab首个page的虚拟地址,page->s_mem保存了首页page偏移着色区大小后的地址;page->active保存了活跃slab对象的计数;此处以slab传统内存布局方式为例,freelist保存了freelist管理区的地址
slab_map_pages: Map pages beginning at addr to the given cache and slab
从如上可以看出,page中与slab相关的变量主要包括:
slab_list主要作为slab的首页page连入slab节点的三个链表之一;
s_mem保存了slab首页page偏移着色区大小后的地址,也就是首个slab对象的地址;
active保存了活跃slab对象的计数;
slab_cache保存所在slab描述符的地址
freelist保存了page所在slab的freelist管理区的地址
5. kmem_cache_free
kmem_cache_free(struct kmem_cache *cachep, void *objp)
\--__cache_free(cachep, objp, _RET_IP_)
\--___cache_free(cachep, objp, caller)
|--struct array_cache *ac = cpu_cache_get(cachep)
|--check_irq_off()
| //本地缓冲池可用对象大于限制limit
|--if (ac->avail > ac->limit)
| STATS_INC_FREEMISS(cachep)
| //释放本地对象缓冲池对象
| cache_flusharray(cachep, ac)
\--__free_one(ac, objp)
\--ac->entry[ac->avail++] = objp
kmem_cache_free主要用于释放slab缓存。如果本地缓冲池可用对象数目大于本地对象缓冲池最大允许的空闲对象数目limit,则需要通过cache_flusharray释放本地对象缓冲池对象。最后通过__free_one释放对象objp到本地对象缓冲池
|- -cache_flusharray
cache_flusharray(cachep, ac)
|--迁移到共享对象缓冲池
| //如果没有共享对象缓冲池或共享对象缓冲池满,则释放到partial/free链表
|--free_block(cachep, ac->entry, batchcount, node, &list)
| //更新本地对象缓冲池空闲对象数量
|--ac->avail -= batchcount
|--memmove(ac->entry, &(ac->entry[batchcount]), sizeof(void *)*ac->avail)
\--slabs_destroy(cachep, &list)
cache_flusharray首先尝试迁移本地对象缓冲池超过空闲限额的对象到共享对象缓冲池,如果共享对象缓冲池不存在或超过了共享对象缓冲池的空闲对象限额,将进一步通过free_block释放到slab节点的partial/free链表。
如果slab节点空闲对象超过本节点允许的最大空闲限额,则将这些slab记录到一个临时链表 list,后续通过slabs_destroy释放到buddy
更新本地对象缓冲池可用对象数目ac->avail,并通过memmove将本地对象缓冲池空闲对象前移batchcount,表示本地对象缓冲池已迁移batchcount个对象
slabs_destroy将前述超过本节点最大空闲限额的slab归还给buddy
|- - -迁移到共享对象缓冲池
迁移到共享对象缓冲池
| //获取numa nid
|--int node = numa_mem_id()
| //获取本地对象缓冲区的一次空闲对象迁移数目
|--batchcount = ac->batchcount
| //获取slab节点
|--n = get_node(cachep, node)
| //如果有共享对象缓冲池
\--if (n->shared)
//获取共享对象缓冲池还可以接收的对象数目
int max=shared_array->limit-shared_array->avail;
//共享对象缓冲池还没有达到最高空闲对象数目limit,
//则从本地对象缓冲池迁移batchcount个到共享对象缓冲池
if (max)
memcpy(&(shared_array->entry[shared_array->avail]),
ac->entry, sizeof(void *) * batchcount)
//增加共享对象缓冲池可用对象数目
shared_array->avail += batchcount
如果n->shared不为0,则说明有共享对象缓冲池,则从本地对象缓冲池迁移shared_array->limit-shared_array->avail个空闲对象到共享对象缓冲池
|- - -free_block
free_block(cachep, ac->entry, nr_objects, node, &list)
|--n->free_objects += nr_objects
|--for (i = 0; i < nr_objects; i++)
| page = virt_to_head_page(objp)
| list_del(&page->slab_list) //从slab节点的链表移除
| if (page->active == 0) //slab没有任何活跃对象,则移入slab节点free链表
| list_add(&page->slab_list, &n->slabs_free)
| n->free_slabs++
| else //slab还有活跃对象,则移入slab节点partial链表
| list_add_tail(&page->slab_list, &n->slabs_partial)
|--while (n->free_objects > n->free_limit && !list_empty(&n->slabs_free))
n->free_objects -= cachep->num
page = list_last_entry(&n->slabs_free, struct page, slab_list)
list_move(&page->slab_list, list)
对于没有共享对象缓冲池将通过free_block将本地对象缓冲池中的空闲对象移入slab节点partial/free链表
free_block对本地对象缓冲池要迁移的batchcount个slab对象进行遍历,对每个slab对象通过virt_to_head_page获取slab对象所在slab的首页page,将其从所在slab节点的链表移除,如果slab没有任何活跃对象,则移入slab节点free链表,如果slab还有活跃对象,则移入slab节点partial链表。
如果n->free_objects > n->free_limit,则说明slab节点的空闲对象个数free_objects 大于slab节点所允许的最大空闲对象个数free_limit ,此时将从slab节点的free链表迁移n->free_objects - n->free_limit个slab对象(每次迁移cachep->num个对象,一个slab)到临时链表list,这个临时链表将在free_block之后通过slabs_destroy来销毁
|- - -slabs_destroy
slabs_destroy(cachep, &list)
\--list_for_each_entry_safe(page, n, list, slab_list)
|--list_del(&page->slab_list)
\--slab_destroy(cachep, page)
|--kmem_freepages(cachep, page)
|--int order = cachep->gfporder//获取slab所占用的页面order
|--__ClearPageSlabPfmemalloc(page)
|--__ClearPageSlab(page)
|--page_mapcount_reset(page)
|--page->mapping = NULL
\--__free_pages(page, order)//释放slab占用的page到buddy
slabs_destroy主要是通过循环遍历链表list上的所有slab最终通过__free_pages释放slab对应的page
6. kmalloc
kmalloc(size_t size, gfp_t flags)
\--__kmalloc(size, flags)
\--__do_kmalloc(size, flags, _RET_IP_)
|--struct kmem_cache *cachep;
|--cachep = kmalloc_slab(size, flags)
| |--index = size_index[size_index_elem(size)]
| \--return kmalloc_caches[kmalloc_type(flags)][index]
|--slab_alloc(cachep, flags, caller)
\--kasan_kmalloc(cachep, ret, size, flags)
-
kmalloc_slab
主要是根据slab对象大小返回通用相应的slab描述符,通用slab对象的slab描述符保存在kmalloc_caches数组,它是在kmem_cache_init->create_kmalloc_caches时创建,可参考
《kernel启动流程-start_kernel的执行_4.mm_init》 的kmem_cache_init -
slab_alloc
完成slab对象的分配,详细过程可参考前述 4. kmem_cache_alloc 部分
7. slab debug
cat /proc/slabinfo
如下为slabinfo部分信息
/ # cat /proc/slabinfo
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
ext4_groupinfo_1k 32 34 472 17 2 : tunables 0 0 0 : slabdata 2 2 0
jbd2_1k 0 0 3072 10 8 : tunables 0 0 0 : slabdata 0 0 0
p9_req_t 0 0 472 17 2 : tunables 0 0 0 : slabdata 0 0 0
sd_ext_cdb 2 22 368 22 2 : tunables 0 0 0 : slabdata 1 1 0
asd_sas_event 0 0 512 16 2 : tunables 0 0 0 : slabdata 0 0 0
sas_task 0 0 704 23 4 : tunables 0 0 0 : slabdata 0 0 0
sgpool-128 2 7 4544 7 8 : tunables 0 0 0 : slabdata 1 1 0
sgpool-64 2 13 2496 13 8 : tunables 0 0 0 : slabdata 1 1 0
sgpool-32 2 22 1472 22 8 : tunables 0 0 0 : slabdata 1 1 0
sgpool-16 2 17 960 17 4 : tunables 0 0 0 : slabdata 1 1 0
sgpool-8 2 23 704 23 4 : tunables 0 0 0 : slabdata 1 1 0
mqueue_inode_cache 1 12 1344 12 4 : tunables 0 0 0 : slabdata 1 1 0
v9fs_inode_cache 0 0 984 16 4 : tunables 0 0 0 : slabdata 0 0 0
nfs4_xattr_cache_cache 0 0 2456 13 8 : tunables 0 0 0 : slabdata 0 0 0
nfs_direct_cache 0 0 560 14 2 : tunables 0 0 0 : slabdata 0 0 0
nfs_commit_data 4 14 1152 14 4 : tunables 0 0 0 : slabdata 1 1 0
......
kmalloc-rcl-8k 0 0 24576 1 8 : tunables 0 0 0 : slabdata 0 0 0
kmalloc-rcl-4k 0 0 12288 2 8 : tunables 0 0 0 : slabdata 0 0 0
kmalloc-rcl-2k 0 0 6144 5 8 : tunables 0 0 0 : slabdata 0 0 0
kmalloc-rcl-1k 0 0 3072 10 8 : tunables 0 0 0 : slabdata 0 0 0
kmalloc-rcl-512 0 0 1536 21 8 : tunables 0 0 0 : slabdata 0 0 0
kmalloc-rcl-256 0 0 1024 16 4 : tunables 0 0 0 : slabdata 0 0 0
kmalloc-rcl-128 0 0 640 12 2 : tunables 0 0 0 : slabdata 0 0 0
kmalloc-8k 14 14 24576 1 8 : tunables 0 0 0 : slabdata 14 14 0
kmalloc-4k 28 28 12288 2 8 : tunables 0 0 0 : slabdata 14 14 0
kmalloc-2k 92 95 6144 5 8 : tunables 0 0 0 : slabdata 19 19 0
kmalloc-1k 379 380 3072 10 8 : tunables 0 0 0 : slabdata 38 38 0
kmalloc-512 292 294 1536 21 8 : tunables 0 0 0 : slabdata 14 14 0
kmalloc-256 1088 1104 1024 16 4 : tunables 0 0 0 : slabdata 69 69 0
kmalloc-128 4205 4224 640 12 2 : tunables 0 0 0 : slabdata 352 352 0
kmem_cache_node 171 180 640 12 2 : tunables 0 0 0 : slabdata 15 15 0
kmem_cache 171 189 768 21 4 : tunables 0 0 0 : slabdata 9 9 0
参考文档
- http://blog.chinaunix.net/uid-14528823-id- 4634134.html
- 奔跑吧,Linux内核