23.slab分配器(分配对象kmem_cache_alloc和缓存的增长cache_grow)
kmem_cache_alloc用于从特定的缓存获取对象。类似于所有的malloc函数,其结果可能是指向分配内存区的指针,也可能分配失败,返回NULL指针。该函数需要两个参数:用于获取对象的缓存,以及精确描述分配特征的标志变量。
<slab.h>
void *kmem_cache_alloc (kmem_cache_t *cachep, gfp_t flags)
图3-50清楚地说明了,可以跟循下面列举的一条途径完成工作。第一个使用更为频繁也更方便,如果per-CPU缓存中有空闲对象,则从中获取。但如果其中的所有对象都已经分配,则必须重新填充缓存。在最坏的情况下,可能需要新建一个slab。
选择被缓存的对象
如果在per-CPU缓存中有对象,那么____cache_alloc检查相对容易,如下列代码片段所示:
mm/slab.c static inline void *____cache_alloc(kmem_cache_t *cachep, gfp_t flags) { ac = ac_data(cachep); if (likely(ac->avail)) { ac->touched = 1; objp = ac->entry[--ac->avail]; } else { objp = cache_alloc_refill(cachep, flags); } return objp;
cachep是一个指针,指向缓存使用的kmem_cache_t实例。ac_data宏通过返回cachep->array[smp_processor_ id()],从而获得当前活动CPU相关的array_cache实例。
因为内存中的对象紧跟array_cache实例之后,内核可以借助于该结构末尾的伪数组访问对象,而无需指针运算。通过将ac->avail减1,可以将对象从缓存移除。
重新填充per-CPU缓存
在per-CPU缓存中没有对象时,工作负荷会加重。该情形下所需的重新填充操作由cache_alloc_refill实现,在per-CPU缓存无法直接满足分配请求时,则调用该函数。
内核现在必须找到array_cache->batchcount个未使用对象重新填充per-CPU缓存。首先扫描所有部分空闲slab的链表(slabs_partial),然后通过slab_get_obj依次获取所有的对象,直至相应的slab中没有空闲对象为止。内核接下来对slabs_partial链表中的所有其他slab执行同样的过程。如果仍未找到所需数目的对象,内核会遍历slabs_free链表中所有未使用的slab。在从slab获取对象时,内核还必须将slab放置到正确的slab链表中(slabs_full或slabs_partial,取决于slab已经完全用尽还是仍然包含一些空闲对象)。上述逻辑由下列代码实现:
mm/slab.c static void *cache_alloc_refill(kmem_cache_t *cachep, gfp_t flags) { ... while (batchcount > 0) { /* 选择获取对象的slab链表(首先是slabs_partial,然后是slabs_free) */ ... slabp = list_entry(entry, struct slab, list); while (slabp->inuse < cachep->num && batchcount--) { //cachep->num是每个slab中的数量 /* 获取对象指针 */ ac->entry[ac->avail++] = slab_get_obj(cachep, slabp, node); } check_slabp(cachep, slabp); /*将slabp移动到正确的slab链表: */ list_del(&slabp->list); if (slabp->free == BUFCTL_END) list_add(&slabp->list, &l3->slabs_full); else list_add(&slabp->list, &l3->slabs_partial); } ... }
按次序移除slab中对象的关键在于slab_get_obj:
mm/slab.c static void *slab_get_obj(struct kmem_cache *cachep, struct slab *slabp, int nodeid) { void *objp = index_to_obj(cachep, slabp, slabp->free); kmem_bufctl_t next; slabp->inuse++; next = slab_bufctl(slabp)[slabp->free]; slabp->free = next; return objp; }
回想20.slab分配器原理(缓存和slab结构详解),内核在跟踪空闲项时使用了一个有趣的系统:当前考虑的空闲对象的索引保存在slabp->free,而下一个空闲对象的索引,则保存在管理数组中。
获取对应于给定索引的对象,不过是index_to_obj执行的一些简单指针操作而已。slab_bufctl是一个宏,返回一个指向slabp之后的kmem_bufctl数组的指针。
我们回到cache_alloc_grow。如果扫描了所有的slab仍然没有找到空闲对象,那么必须使用cache_grow扩大缓存。这是一个代价较高的操作,将在下一节讲述。
缓存的增长
图3-51给出了cache_grow的代码流程图。

首先计算颜色和偏移量:
mm/slab.c static int cache_grow(struct kmem_cache *cachep,gfp_t flags, int nodeid, void *objp) { ... l3 = cachep->nodelists[nodeid]; ... offset = l3->colour_next; l3->colour_next++; if (l3->colour_next >= cachep->colour) l3->colour_next = 0; offset *= cachep->colour_off; ... }
如果达到了颜色的最大数目,则内核重新开始从0计数,这自动导致了零偏移。所需的内存空间是使用kmem_getpages辅助函数从伙伴系统逐页分配的。该函数唯一的目的就是用适当的参数调用13.分配页之选择页(zone_watermark_ok和get_page_from_freelist 讨论的alloc_pages_node函数。各个页都设置了PG_slab标志位,表示该页属于slab分配器。在一个slab用于满足短期或可回收分配时,则将标志__GFP_RECLAIMABLE传递到伙伴系统。回想11.伙伴系统(避免碎片及内存的迁移类型 的内容,我们知道重要的是从适当的迁移列表分配页。
如果slab头存储在slab之外,则调用相关的alloc_slabmgmt函数分配所需空间。否则,相应的空间已经在slab中分配。在两种情况下,都必须用适当的值初始化slab数据结构的colouroff、s_mem和inuse成员。
接下来,内核调用slab_map_pages创建slab的各页与slab或缓存之间的关联。该函数遍历新分配的所有page实例,分别调用page_set_cache和page_set_slab。这两个函数如下操作(或滥用)page实例的lru成员:
mm/slab.c static inline void page_set_cache(struct page *page, struct kmem_cache *cache) { page->lru.next = (struct list_head *)cache; } static inline void page_set_slab(struct page *page, struct slab *slab) { page->lru.prev = (struct list_head *)slab; }
slab的kmem_bufctl数组也会初始化,在数组位置i存储i+1:因为slab至今完全未使用,下一个空闲的对象总是下一个对象。根据惯例,最后一个数组元素的值为BUFCTL_END。现在slab已经完全初始化,可以添加到缓存的slabs_free链表。新产生的对象的数目也加到缓存中空闲对象的数目上(cachep->free_objects)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!