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

背景

省流

如果对代码细节不感兴趣,可以直接跳转底部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--;//引用-1
	if (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_SYSFS
		sysfs_slab_unlink(s);
#endif
		list_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);
#else
		slab_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的partial
		free_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_PARTIAL
	struct 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));//获取node
		if (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列表,后面会直接释放该slab
		if (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 page
		stat(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 page
	struct 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 和node
	kfree_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_slab
	free_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掉x
	trace_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结构体
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值