> 文章列表 > [linux kernel]slub内存管理分析(6) 销毁slab

[linux kernel]slub内存管理分析(6) 销毁slab

[linux kernel]slub内存管理分析(6) 销毁slab

文章目录

    • 背景
      • 省流
      • 前情回顾
      • 描述方法约定
    • kmem_cache销毁操作总览
      • 简介
      • 调用栈
    • 详细分析
      • kmem_cache_destroy->shutdown_cache
      • __kmem_cache_shutdown
        • flush_all
          • flush_slab
          • unfreeze_partials
        • free_partial
      • discard_slab
      • slab_kmem_cache_release
        • __kmem_cache_release
        • kmem_cache_free
    • kmem_cache销毁操作总结

背景

省流

如果对代码细节不感兴趣,可以直接跳转底部kmem_cache销毁操作总结

前情回顾

关于slab几个结构体的关系和初始化和内存分配和释放的逻辑请见:

[linux kernel]slub内存管理分析(0) 导读

[linux kernel]slub内存管理分析(1) 结构体

[linux kernel]slub内存管理分析(2) 初始化

[linux kernel]slub内存管理分析(2.5) slab重用

[linux kernel]slub内存管理分析(3) kmalloc

[linux kernel]slub内存管理分析(4) 细节操作以及安全加固

[linux kernel]slub内存管理分析(5) kfree

描述方法约定

PS:为了方便描述,这里我们将一个用来切割分配内存的page 称为一个slab page,而struct kmem_cache我们这里称为slab管理结构,它管理的真个slab 体系成为slab cachestruct kmem_cache_node这里就叫node。单个堆块称为object或者堆块内存对象

kmem_cache销毁操作总览

简介

slab销毁操作的入口是kmem_cache_destroy,主要是销毁一整个slab 管理结构和其中的node、cpu_slab 和各个slab。一般是很多临时slab 用完后销毁使用。里面分步释放了kmem_cache 下面的多个字结构体和各个slab page,最后再释放kmem_cache结构。

调用栈

  • kmem_cache_destroy
    • shutdown_cache 核心销毁函数
      • __kmem_cache_shutdown 处理各个slab page
        • flush_all 把所有cpu_slab里的slab page下架
          • flush_cpu_slab -> __flush_cpu_slab
            • flush_slab 下架并刷新cpu_slab
              • deactivate_slab 强制下架
          • unfreeze_partials: cpu_slab->partial 转移到node->partial
            • discard_slab 销毁空slab page
              • free_slab -> __free_slab
                • __free_pages
        • free_partial 处理node->partial 中的slab page
          • remove_partial 移出partial
          • discard_slab 销毁空slab page
            • free_slab -> __free_slab
              • __free_pages
      • slab_kmem_cache_release 释放slub管理相关结构
        • __kmem_cache_release 释放次级结构体
          • cache_random_seq_destroy freelist 随机化相关释放
          • free_percpu 释放所有cpu_slub
          • free_kmem_cache_nodes 释放所有node
            • kmem_cache_free 释放 kmem_cache_node 结构
              • slab_free
        • kmem_cache_free 释放kmem_cache 结构
          • slab_free

详细分析

kmem_cache_destroy->shutdown_cache

kmem_cache_destroy是slab销毁函数的入口:

linux\\mm\\slab_common.c : kmem_cache_destroy

void kmem_cache_destroy(struct kmem_cache *s)//销毁入口
{int err;if (unlikely(!s))return;mutex_lock(&slab_mutex);//加锁操作s->refcount--;//引用-1if (s->refcount)//如果引用不为0说明还有引用它的,则不操作goto out_unlock;err = shutdown_cache(s);//核心操作if (err) {pr_err("kmem_cache_destroy %s: Slab cache still has objects\\n",s->name);dump_stack();}
out_unlock:mutex_unlock(&slab_mutex);
}

简单进行了加锁和引用判断便调用了shutdown_cache,接下来分析shutdown_cache

linux\\mm\\slab_common.c : shutdown_cache

static int shutdown_cache(struct kmem_cache *s)
{/* free asan quarantined objects */kasan_cache_shutdown(s);if (__kmem_cache_shutdown(s) != 0)//[1] 处理slab管理结构下面的各个slab page。return -EBUSY;list_del(&s->list);//[2] 释放完毕后从链表中删除if (s->flags & SLAB_TYPESAFE_BY_RCU) {//RCU相关,默认不开启
#ifdef SLAB_SUPPORTS_SYSFSsysfs_slab_unlink(s);
#endiflist_add_tail(&s->list, &slab_caches_to_rcu_destroy);schedule_work(&slab_caches_to_rcu_destroy_work);} else {kfence_shutdown_cache(s);//kfence相关
#ifdef SLAB_SUPPORTS_SYSFS//不开启sysfs_slab_unlink(s);sysfs_slab_release(s);
#elseslab_kmem_cache_release(s);//[3] 释放node、cpu_slab等结构体
#endif}return 0;
}

[1] 先调用__kmem_cache_shutdown函数对该slab管理的各个slab page进行处理,包括cpu_slab下面的、node下面的。主要对page进行释放操作。

[2] 然后要把slab 管理结构kmem_cache从链表中删除。

[3] 最后把kmem_cache结构中的cpu_slab 和node 等结构体的内存释放,并释放自己。

__kmem_cache_shutdown

__kmem_cache_shutdown 函数负责释放slab 管理结构下面的所有slab page (cpu_slab中的和node中的)

linux\\mm\\slub.c : __kmem_cache_shutdown

int __kmem_cache_shutdown(struct kmem_cache *s)
{int node;struct kmem_cache_node *n;flush_all(s);//[1] 将所有cpu_slab下架/* Attempt to free all objects */for_each_kmem_cache_node(s, node, n) {//[2] 释放所有node的partialfree_partial(s, n);if (n->nr_partial || slabs_node(s, node))return 1;}return 0;
}

[1] 先调用flush_all 刷新所有cpu,其实刷新就是将cpu_slab现有的slab page下架放入node对应状态的list中,然后cpu_slab->pagefreelist 指针都置空。

[2] 然后再调用for_each_kmem_cache_node释放node里的slab。

flush_all

linux\\mm\\slub.c : flush_all

static void flush_all(struct kmem_cache *s)
{on_each_cpu_cond(has_cpu_slab, flush_cpu_slab, s, 1);
}

其实直接对每个cpu,如果存在cpu_slab,则调用flush_cpu_slab

linux\\mm\\slub.c : flush_cpu_slab、__flush_cpu_slab

static void flush_cpu_slab(void *d)
{struct kmem_cache *s = d;__flush_cpu_slab(s, smp_processor_id());
}static inline void __flush_cpu_slab(struct kmem_cache *s, int cpu)
{struct kmem_cache_cpu *c = per_cpu_ptr(s->cpu_slab, cpu);if (c->page)flush_slab(s, c); //最后掉用的是flush_slab函数unfreeze_partials(s, c);//将cpu_slab->partial 列表里的slab 加入node->partial中
}

对每一个cpu_slab,如果page不为空就会调用flush_slab 刷新该cpu_slab,将该slab page强制下架并将cpu_slab->freelistcpu_slab->page都置空。然后再将每个cpu_slab->partial中的slab page都移动到node->partial中。

flush_slab

linux\\mm\\slub.c : flush_slab

static inline void flush_slab(struct kmem_cache *s, struct kmem_cache_cpu *c)
{stat(s, CPUSLAB_FLUSH);deactivate_slab(s, c->page, c->freelist, c);//强制下架c->tid = next_tid(c->tid);
}

flush_slab 在之前分析过,主要是调用deactivate_slabcpu_slab 当前的slab page进行强制下架,将其根据状态放入对应的node的列表中或释放,然后再将cpu_slab->freelistcpu_slab->page都置空。

unfreeze_partials

unfreeze_partials 函数解冻所有cpu_slab->partial中的slab page,然后将他们加入到node->partial list中。

linux\\mm\\slub.c : unfreeze_partials

static void unfreeze_partials(struct kmem_cache *s,struct kmem_cache_cpu *c)
{
#ifdef CONFIG_SLUB_CPU_PARTIALstruct kmem_cache_node *n = NULL, *n2 = NULL;struct page *page, *discard_page = NULL;while ((page = slub_percpu_partial(c))) {//[1] 获取cpu_slab->partial中第一个slab page,也就是遍历struct page new;struct page old;slub_set_percpu_partial(c, page);//[1] cpu_slab->partial向后移动一个n2 = get_node(s, page_to_nid(page));//获取nodeif (n != n2) {if (n)spin_unlock(&n->list_lock);n = n2;spin_lock(&n->list_lock);}do {//[2] 循环直到成功,尝试更新slab page 的冻结状态old.freelist = page->freelist;old.counters = page->counters;VM_BUG_ON(!old.frozen);new.counters = old.counters;new.freelist = old.freelist;new.frozen = 0;} while (!__cmpxchg_double_slab(s, page,//[2] 原子结构,就是更新page状态,主要是frozen=0解冻old.freelist, old.counters,     new.freelist, new.counters,"unfreezing slab"));//[3] 如果该slab为空,则加入到discard_page列表,后面会直接释放该slabif (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {page->next = discard_page;discard_page = page;} else {//[4] 否则加入到node->partial中add_partial(n, page, DEACTIVATE_TO_TAIL);stat(s, FREE_ADD_PARTIAL);}}if (n)spin_unlock(&n->list_lock);while (discard_page) {//[3.1] discard_page列表中的都是空slab,调用discard_slab释放掉。page = discard_page;discard_page = discard_page->next;stat(s, DEACTIVATE_EMPTY);discard_slab(s, page);//[3.1] 释放slab pagestat(s, FREE_SLAB);}
#endif	/* CONFIG_SLUB_CPU_PARTIAL */
}

[1] 遍历操作cpu_slab->partial中所有的page,要对每个page都进行解冻操作,并且空的slab要在这里直接释放掉。

[2] 循环尝试原子操作,该原子操作就是同事操作两个值,但这里主要是尝试更新frozen。对page进行解冻

[3] 如果inuse=1说明该slab为空,则加入到discard_page 列表里,准备释放。

​ [3.1] 这里会将discard_page 列表里所有的空page都调用discard_slab释放掉,discard_slab后面分析。

[4] 非空的加入到node->partial列表中。

free_partial

for_each_kmem_cache_node主要是处理node中的slab page,刚已经把cpu_slabpagepartial 中的slab都释放或者加入到了node->partial中,在这里进行集中处理:

linux\\mm\\slub.c : free_partial

static void free_partial(struct kmem_cache *s, struct kmem_cache_node *n)
{LIST_HEAD(discard);//[1] discard用来存放准备释放的slab pagestruct page *page, *h;BUG_ON(irqs_disabled());spin_lock_irq(&n->list_lock);list_for_each_entry_safe(page, h, &n->partial, slab_list) {if (!page->inuse) {//[1] 没有正在使用的内存块了则可以释放remove_partial(n, page);//从partial 中移除list_add(&page->slab_list, &discard);//加入discard队列准备删除} else {list_slab_objects(s, page,//如果还有正在使用的,报个错。"Objects remaining in %s on __kmem_cache_shutdown()");}}spin_unlock_irq(&n->list_lock);list_for_each_entry_safe(page, h, &discard, slab_list)discard_slab(s, page);//[1] 调用discard_slab销毁page
}

[1] 该函数认为node->partial中的slab page到这时已经大概率都为空,所以遍历node->partial中的slab page,如果为空,则加入discard列表在末尾统一调用discard_slab 释放掉。如果还有没空的slab page,那就报错。

discard_slab

这一部分在之前章节kfree中分析过

discard_slab用于释放一个slab page,在前面的函数中涉及到slab page的释放都有调用:

linux\\mm\\slub.c : discard_slab

static void discard_slab(struct kmem_cache *s, struct page *page)
{dec_slabs_node(s, page_to_nid(page), page->objects);//更新统计信息free_slab(s, page);//free_slab 释放slab
}

discard_slab中先调用dec_slabs_node进行一些统计,更新一下slab page被释放后node中的slab page数量和object 数量。然后调用free_slab 进行正式的slab 释放:

linux\\mm\\slub.c : free_slab

static void free_slab(struct kmem_cache *s, struct page *page)
{if (unlikely(s->flags & SLAB_TYPESAFE_BY_RCU)) {//RCU延迟释放,默认不开启call_rcu(&page->rcu_head, rcu_free_slab);} else__free_slab(s, page);
}

如果开启了SLAB_TYPESAFE_BY_RCU 则使用RCU延迟释放slab,不太关心,默认不开启。正常流程调用__free_slab释放slab:

linux\\mm\\slub.c : __free_slab

static void __free_slab(struct kmem_cache *s, struct page *page)
{int order = compound_order(page); //获取slab所属page阶数int pages = 1 << order; //page 页数if (kmem_cache_debug_flags(s, SLAB_CONSISTENCY_CHECKS)) {//如果开启了debug,则进行一些检测void *p;slab_pad_check(s, page);//slab 检测for_each_object(p, s, page_address(page),page->objects)check_object(s, page, p, SLUB_RED_INACTIVE);//slab中的object 检测}__ClearPageSlabPfmemalloc(page);//这两个都是修改page->flags的宏,清除slab相关标志位__ClearPageSlab(page);/* In union with page->mapping where page allocator expects NULL */page->slab_cache = NULL;//取消page指向slab 的指针if (current->reclaim_state)current->reclaim_state->reclaimed_slab += pages;//更新数据unaccount_slab_page(page, order, s);//如果开启了memcg相关会进行memcg去注册__free_pages(page, order); //释放掉page
}

逻辑很简单,在__free_slab 中进行一些常规的检查和统计更新,最后调用__free_pages将页面释放。

slab_kmem_cache_release

slab_kmem_cache_release 分别释放cpu_slab 结构体和kmem_cache_node结构体、 kmem_cache->name字符串和kmem_cache自己:

linux\\mm\\slab_common.c : slab_kmem_cache_release

void slab_kmem_cache_release(struct kmem_cache *s)//释放管理结构
{__kmem_cache_release(s);//释放cpu_slab 和nodekfree_const(s->name);//释放name,name不是rodata只读kmem_cache_free(kmem_cache, s);//释放掉slab管理结构s
}

__kmem_cache_release

void __kmem_cache_release(struct kmem_cache *s)
{cache_random_seq_destroy(s);//[1] freelist random相关free_percpu(s->cpu_slab);//[2]释放所有cpu_slabfree_kmem_cache_nodes(s);//[3]释放每个node
}

[1] 开启CONFIG_SLAB_FREELIST_RANDOM 宏则释放掉cachep->random_seq,因为这个也是kamlloc申请的内存:

void cache_random_seq_destroy(struct kmem_cache *cachep)
{kfree(cachep->random_seq);cachep->random_seq = NULL;
}

[2] 调用free_percpu 在cpu缓存中释放掉cpu_slab

[3] 释放kmem_cache_node结构体,将自己的node指针置空之后直接调用kmem_cache_free 释放掉

linux\\mm\\slub.c : free_kmem_cache_nodes

static void free_kmem_cache_nodes(struct kmem_cache *s)//释放每个node
{int node;struct kmem_cache_node *n;for_each_kmem_cache_node(s, node, n) {s->node[node] = NULL;kmem_cache_free(kmem_cache_node, n); //释放每个node}
}

kmem_cache_free

其实就是调用正常free逻辑中的slab_free 函数释放内存,slab_freekfree那一章节分析过了,这里不继续分析了node和kmem_cache都是在这里释放(因为他们也是普通kmalloc分配的):

linux\\mm\\slub.c : kmem_cache_free

void kmem_cache_free(struct kmem_cache *s, void *x)
{s = cache_from_obj(s, x);if (!s)return;slab_free(s, virt_to_head_page(x), x, NULL, 1, _RET_IP_);//调用slab_free正常free掉xtrace_kmem_cache_free(_RET_IP_, x, s->name);
}

kmem_cache销毁操作总结

整体逻辑比较简单,先释放所有slab page,然后释放slab的各种结构体,如果slab 中还有没被释放的堆块,则说明还繁忙,则无法销毁:

  • 首先slab 引用次数减1,如果引用归零则正式销毁
  • 先将该slab中的各个page链表中的page处理一下
    • 将所有cpu_slab中的page下架
    • 将所有cpu_slab中的partial page放到node中的partial列表中
    • 释放node->partial中所有slab page
      • 将空闲的page 调用discard_slab释放掉
      • 不空闲的不释放
    • 如果还有slab page未释放,说明还有堆块正在被使用,返回busy无法销毁
  • 将slab从slab_caches全局列表中删除
  • 释放slab相关的各种结构体
    • 释放kmem_cache_cpukmem_cache_node结构体
    • 释放name
    • 释放kmem_cache结构体