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

背景

省流

如果对代码细节不感兴趣,可以直接跳转底部内存释放逻辑总结

前情回顾

关于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) 细节操作以及安全加固

描述方法约定

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

kfree 操作总览

简介

kfree 是用来回收kmalloc 分配的内存的函数,这里详细分析流程。

kfree之中同样有很多设计cpu抢占、NUMA架构node相关的逻辑,这里不详细分析了,因为我们分析的目的是为了搞内核安全,而写漏洞利用的时候可以通过将程序绑定在一个cpu 来避免涉及这些逻辑的代码发生。

还有一些和Kasan、slub_debug 等检测内存越界等的操作,也不过多分析。

逻辑图预览

释放逻辑

在这里插入图片描述

slab page各个状态转化

在这里插入图片描述

调用栈

  • kfree
    • __free_pages 释放页面(大块)
    • slab_free 释放slab 的内存
      • slab_free_freelist_hook
      • do_slab_free 入口,和cpu_slab快速释放
        • memcg_slab_free_hook memcg相关计数操作
        • __slab_free 慢速释放
          • kfence_freekmem_cache_debugfree_debug_processing 检查
          • 释放逻辑
          • put_cpu_partial
            • unfreeze_partials
          • discard_slab slab为空则销毁该slab page
            • free_slab->__free_slab

详细分析

kfree

kfree总入口:

linux\mm\slub.c : kfree

void kfree(const void *x)
{
	struct page *page;
	void *object = (void *)x;

	trace_kfree(_RET_IP_, x);

	if (unlikely(ZERO_OR_NULL_PTR(x)))
		return;

	page = virt_to_head_page(x);//[1]根据地址找到对应的page结构体
	if (unlikely(!PageSlab(page))) {//[2]如果该page 不是slab,那么就是大块内存了,调用free_page释放
		unsigned int order = compound_order(page);

		BUG_ON(!PageCompound(page));
		kfree_hook(object);
		mod_lruvec_page_state(page, NR_SLAB_UNRECLAIMABLE_B,
				      -(PAGE_SIZE << order));
		__free_pages(page, order);
		return;
	}
	slab_free(page->slab_cache, page, object, NULL, 1, _RET_IP_);//[3]释放slab 分配的内存块
}

[1] 调用kfree 的都是要释放的内存地址,所以首先要根据内存虚拟地址找到它所在页的页结构体。

[2] 判断该页是不是slab page,如果不是,说明这个要释放的内存不是slab切割分配的,而是一个大内存块,当初是直接分配的页,所以这里页调用__free_pages来释放内存页。

[3] 否则说明这个内存块是slab page分配的内存块,进入slab 的释放流程slab_free。参数是通过page 获得page 的slab管理结构体kmem_cache

slab_free

释放slab 内存的入口slab_free

linux\mm\slub.c : slab_free

static __always_inline void slab_free(struct kmem_cache *s, struct page *page,
				      void *head, void *tail, int cnt,
				      unsigned long addr)
{
	if (slab_free_freelist_hook(s, &head, &tail))
		do_slab_free(s, page, head, tail, cnt, addr);//[1]调用do_slab_free
}

[1] slab_free_freelist_hook 是些kasan 的东西没啥用,这里直接调用了do_slab_free,传入参数有headtail,由于slab_free设计上可以释放多个内存块,所以这里headtail 和释放多个内存块相关,如果只释放一个的话,不影响。

linux\mm\slub.c : do_slab_free

static __always_inline void do_slab_free(struct kmem_cache *s,
				struct page *page, void *head, void *tail,
				int cnt, unsigned long addr)
{
	void *tail_obj = tail ? : head;//[1]获得尾部,这里tail 传入是0, 所以tail_obj 就是head
	struct kmem_cache_cpu *c;
	unsigned long tid;

	memcg_slab_free_hook(s, &head, 1);//[2]memcg 相关
redo:
	/*
	 * Determine the currently cpus per cpu slab.
	 * The cpu may change afterward. However that does not matter since
	 * data is retrieved via this pointer. If we are on the same cpu
	 * during the cmpxchg then the free will succeed.
	 */
	do {//[3]获取cpu_slab
		tid = this_cpu_read(s->cpu_slab->tid);
		c = raw_cpu_ptr(s->cpu_slab);
	} while (IS_ENABLED(CONFIG_PREEMPTION) &&
		 unlikely(tid != READ_ONCE(c->tid)));

	/* Same with comment on barrier() in slab_alloc_node() */
	barrier();//同步一下

	if (likely(page == c->page)) {//[4]释放的对象正好属于当前cpu_slab正在使用的slab则快速释放
		void **freelist = READ_ONCE(c->freelist);//[4.1]获取freelist

		set_freepointer(s, tail_obj, freelist);//[4.2]将新释放内存块插入freelist 开头

		if (unlikely(!this_cpu_cmpxchg_double(//[4.3]this_cpu_cmpxchg_double()原子指令操作存放
				s->cpu_slab->freelist, s->cpu_slab->tid,
				freelist, tid,
				head, next_tid(tid)))) {

			note_cmpxchg_failure("slab_free", s, tid);//失败记录
			goto redo;
		}
		stat(s, FREE_FASTPATH);
	} else
		__slab_free(s, page, head, tail_obj, cnt, addr);//[5]否则说明释放的内存不是当前cpu_slab立马能释放的

}

[1] 获取一下尾部,由于我们传入的tail 指针为0,则获取head指针,也就是要释放的内存地址。换句话说,当只释放一个内存块的时候,head=tail

[2] memcg相关,这里不详细分析。

[3] 跟kmalloc 的时候一样,获取一下当前使用cpu的cpu_slab

[4] 如果要释放的内存块的所在page正好是当前cpu_slab管理的slab,那么可以快速释放。

​ [4.1] 获取cpu_slabfreelist

​ [4.2] 调用set_freepointer将要释放的内存块放到freelist 链表的开头,在前一章分析过set_freepointer了。

​ [4.3] 跟kmalloc时候相同的原子操作,这里等价于,也就是更新一下cpu_slab->freelist

freelist = s->cpu_slab->freelist;
s->cpu_slab->freelist=head;
tid=s->cpu_slab->tid;
s->cpu_slab->tid=next_tid(tid)

[5] 到这里说明释放的内存不是当前cpu_slab立马能释放的,那么调用__slab_free慢速分配。

__slab_free

linux\mm\slub.c : __slab_free

static void __slab_free(struct kmem_cache *s, struct page *page,
			void *head, void *tail, int cnt,
			unsigned long addr)

{
	void *prior;
	int was_frozen;
	struct page new;
	unsigned long counters;
	struct kmem_cache_node *n = NULL;
	unsigned long flags;

	stat(s, FREE_SLOWPATH);

	if (kfence_free(head))
		return;

	if (kmem_cache_debug(s) && //这两个里面就是各种检测,实际没有对free环节有影响的操作
	    !free_debug_processing(s, page, head, tail, cnt, addr))
		return; 

	do {//[1] 尝试释放内存块到所属page的freelist
		if (unlikely(n)) {//循环了多次才会走到这,加锁操作
			spin_unlock_irqrestore(&n->list_lock, flags);
			n = NULL;
		}
		prior = page->freelist;//[1.1]获取当前page的freelist
		counters = page->counters;//获取当前page的counters,这是一个联合体,包括inuse、frozen等
		set_freepointer(s, tail, prior);//将freelist 接到要释放内存块的后面*tail=prior
		new.counters = counters;
		was_frozen = new.frozen;
		new.inuse -= cnt;//正在使用内存块减掉要释放的数量
		if ((!new.inuse || !prior) && !was_frozen) {//[2]释放之后全空或释放之前为满并且不在cpu_slab中
            //释放前为满有2种情况:游离中,在cpu_slab->page中,但走到这里确定不在cpu_slab中
            //调试模式时可能在node->full中,默认不开启,不考虑

			if (kmem_cache_has_cpu_partial(s) && !prior) {//[2.1]释放前为满(不管你释放后空不空)
                //并且开启了cpu_slab->partial
				//走到这可以确定slab page为游离状态
				/*
				 * Slab was on no list before and will be
				 * partially empty
				 * We can defer the list move and instead
				 * freeze it.
				 */
				new.frozen = 1;//准备放入cpu_slab中

			} else { /* Needs to be taken off a list *///[2.2]释放之后就为空slab了

				n = get_node(s, page_to_nid(page));//获取page所在node
				/*
				 * Speculatively acquire the list_lock.
				 * If the cmpxchg does not succeed then we may
				 * drop the list_lock without any processing.
				 *
				 * Otherwise the list_lock will synchronize with
				 * other processors updating the list of slabs.
				 */
				spin_lock_irqsave(&n->list_lock, flags);

			}
		}

	} while (!cmpxchg_double_slab(s, page, //[1.2]成功之前一直尝试
		prior, counters,
		head, new.counters,
		"__slab_free"));
    /* if (page->freelist == prior && page->counters == counters)
     * page->freelist = head
     * page->counters = new.counters 完成释放
     */

	if (likely(!n)) {//[3]n为空,说明这里释放之前为满,或释放之前之后都半满

		if (likely(was_frozen)) {//[3.1]原本就在cpu_slab中,啥也不用操作
			/* 走到这里说明该slab page在当前cpu_slab->partial中或其他cpu_slab的page或partial中
			直接释放到page->freelist即可(已经释放完毕),其他cpu我们管不到
			 * The list lock was not taken therefore no list
			 * activity can be necessary.
			 */
			stat(s, FREE_FROZEN);
		} else if (new.frozen) {//[3.2]释放之前是满的也就是游离状态,放入cpu_slab->partial中
			/*
			 * If we just froze the page then put it onto the
			 * per cpu partial list.
			 */
			put_cpu_partial(s, page, 1);//放到cpu_slab->partial中,这里有一个bug,他没有考虑node->full的情况
			stat(s, CPU_PARTIAL_FREE);
		}

		return;//[3.3]释放之前在node->partial中,并且释放之后也不为空,直接返回啥也不用干
	}

	if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))//[4]释放之后为空 node 中的partial 满足最小要求
		goto slab_empty;//去释放这个page

	/*
	 * Objects left in the slab. If it was not on the partial list before
	 * then add it.
	 */
	if (!kmem_cache_has_cpu_partial(s) && unlikely(!prior)) {//[5]释放前为full,但没开启cpu_slab->partial
		remove_full(s, n, page);//从full 中移除,如果是游离状态不在链表中会直接返回
		add_partial(n, page, DEACTIVATE_TO_TAIL); //添加到partial中
		stat(s, FREE_ADD_PARTIAL);
	}
	spin_unlock_irqrestore(&n->list_lock, flags);
	return;

slab_empty:
	if (prior) {//[4.1]释放之前不是full,在partial中
		/*
		 * Slab on the partial list.
		 */
		remove_partial(n, page);//从partial 删除
		stat(s, FREE_REMOVE_PARTIAL);
	} else {//[4.2]释放之前在full 中
		/* Slab must be on the full list */
		remove_full(s, n, page);//从full 中删除
	}

	spin_unlock_irqrestore(&n->list_lock, flags);
	stat(s, FREE_SLAB);
	discard_slab(s, page);//[4]slab为空,则直接删除整个空slab
}

[1] 尝试释放内存块到所属pagefreelist,由于涉及到锁的操作/原子操作,这里会一直循环尝试直到成功为止

​ [1.1] 获取一下释放堆块之前pagefreelistcounters 等信息,counters是一个联合体,同时包括pagefrozen(代表page 是在cpu_slab中还是在node中)、inuse(代表正在使用的内存数量)等。然后将page->freelist 拼到准备释放的内存块后面,也就是新释放的放到freelist表头,但目前还没更新到page,然后计算新inuse等。

​ [1.2] 进行原子操作尝试完成分配,cmpxchg_double_slab 函数完成的操作等价于如下操作,即将新freelist 头更新到page中,再更新counters

if (page->freelist == prior && page->counters == counters)
{
     page->freelist = head;
     page->counters = new.counters;
}

目前已经将堆块释放到page->freelist中了,接下来要对page的种类进行判断。目前一共有6种(有一种是DEBUG场景)可能:

  • 在当前cpu_slab->partial
  • cpu_slab->page
  • 在其他cpu_slab->partial
  • 游离状态(分配满了),不在任何列表之中
  • 在所属nodepartial列表中
  • 如果开启CONFIG_SLUB_DEBUG,则可能会是分配满的状态而在所属node的full列表中,默认不开启

[2] !new.inuse代表释放之后已经没有inuse的内存了,说明释放之后该slab里面全是free 的堆块,也就是释放后整个page都空了;!prior说明释放之前没有freelist,即释放之前该slab 为分配满的slab page;!was_frozen说明释放之前该slab不在cpu_slab之中。也就是说,当该slab 不在是cpu_slab正在使用的slab 时,如果释放后为空slab或释放前为full slab,则说明游离状态、node->full中或node->partial中的page满足该分支条件

​ [2.1] 释放前为full slab,对应游离状态和node->full的slab pagekmem_cache_has_cpu_partial判断是否开启了cpu_slab->partial;若开启则new.frozen设置为1,打算将该slab 放入cpu_slab->partial中,因为释放后就变成了半满状态了,可以使用了。这里为什么放入cpu_slabpartial而不是放入node的partial,是因为cpu_slab永远都是最近在使用的slab。

​ [2.2] 接下来就是slab page在node->partial中且释放之后就为空slab page的情况,获取一下page 所属node。方便后续操作。

[3] n为空,说明之前没有获取node,也就是说不是释放之后就为空slab。满足这个分支的有三种cpu_slab中的情况和游离状态的slab page情况

​ [3.1] 原本就在cpu_slab中(的三种情况),什么也不用改变,因为我们已经将堆块释放到page->freelist中了,如果在本cpu_slab->partial 无论释放完是否为空都无关紧要,因为不需要释放page;其次在其他cpu_slab中的话,不管是在page还是在partial中我们都是无权干涉的,直接释放到page->freelist然后返回即可。

​ [3.2] 走到这里说明**page是申请满的游离状态或node->full中**,则释放后为可用状态,将其放入cpu_slab->partial中然后返回(put_cpu_partial函数逻辑不止于此,下面会详细分析)。这个地方有一个问题就是,他没有考虑如果开启了CONFIG_SLUB_DEBUG的情况下,满的page可能会存在于node->full列表中,如果补充node->full双向链表中把page删掉直接加入到cpu_slab->partial中会破坏node->full双链表,后续插入操作可能造成unlink崩溃,由于默认不开启该CONFIG,再加上page进入node->full需要触发强制下架的时候cpu_slab->page必须是满的,概率非常低,用户也不可控,一般不影响。不过内核最新版已经重写了开启CONFIG_SLUB_DEBUG的逻辑,不存在这样的问题了,我这里的代码是5.13 版本,不影响其他逻辑。

​ [3.3] 最后说明释放之前在node->partial中,并且释放之后也不为空,那么还继续呆在node->partial中就行,直接啥也不用干返回。

[4] 走到这里说明slab page在node->partial中(小概率在node->full中),且释放之后为空且node 中当前partial slab数量比slab 规定的最小partial数量多,可以释放空的slab page,则进入空slab处理流程:

​ [4.1] 释放之前的状态是半满,则调用remove_partialnode->partial 列表中删除,然后调用discard_slab释放该空slab page。

​ [4.2] 释放之前在node->full中,则调用remove_fullfull列表中删除,之所以会有释放之后直接从full 变为空,我猜是考虑到这个函数可以一次释放多个堆块的原因。还有一点就是,正常full状态的page在上面就会加入到cpu_slab->partial或者node->partial中了,无论释放完是否为空,所以是走不到这个分支的,可见这个版本的代码对CONFIG_SLUB_DEBUG的处理还是比较粗糙的,目前已经在后续版本都会更新完善。

[5] 否则说明**cpu_slab没开启partial列表,游离状态的slab page释放后仍需要添加到一个列表中**,则将其加入node->partial 中。

put_cpu_partial

put_cpu_partial 函数不止是将slab page 放到cpu_slab->partial链表中,如果cpu_slab->partial链表中的页面数量已经达到最大值,则还会进行洗牌操作:

mm\slub.c : put_cpu_partial

static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain)
{
#ifdef CONFIG_SLUB_CPU_PARTIAL
	struct page *oldpage;
	int pages; //[1] 代表这个slab 后面大概还有多少个page
	int pobjects;//代表这个slab 后面还有多少个object 可用

	preempt_disable();
	do {
		pages = 0;
		pobjects = 0;
		oldpage = this_cpu_read(s->cpu_slab->partial);//获取cpu->partial列表头部的那个

		if (oldpage) {//[1] 获取之前的pages 和pobjects 值用来之后更新
			pobjects = oldpage->pobjects;
			pages = oldpage->pages;
			if (drain && pobjects > slub_cpu_partial(s)) {//[2] 当前cpu_slab->partial中的页数量已经达到cpu_partial最大值
				unsigned long flags;
				/*
				 * partial array is full. Move the existing
				 * set to the per node partial list.
				 */
				local_irq_save(flags);
				unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));//把cpu_slab->partial中现存的slab page都放到node->partial中
				local_irq_restore(flags);
				oldpage = NULL;
				pobjects = 0;//cpu_slab->partial数量清0
				pages = 0;
				stat(s, CPU_PARTIAL_DRAIN);
			}
		}

		pages++;//[3]更新pages和pobjects数量
		pobjects += page->objects - page->inuse;

		page->pages = pages; //设置当前slab 的pages 和pobjects
		page->pobjects = pobjects;
		page->next = oldpage;//加入链表头部

	} while (this_cpu_cmpxchg(s->cpu_slab->partial, oldpage, page)//[4] 最后将当前页放入cpu_slab->partial中
								!= oldpage);
	if (unlikely(!slub_cpu_partial(s))) {
		unsigned long flags;

		local_irq_save(flags);
		unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));
		local_irq_restore(flags);
	}
	preempt_enable();
#endif	/* CONFIG_SLUB_CPU_PARTIAL */
}

[1] cpu_slab->partial 中每个页面中的pobjectspages 都代表这个页以及后面的单链表中有多少内存块数和页数,这样我们只访问第一个页即可了解全部信息。

[2] 如果cpu_slab->partial中的页数已经达到了slab 规定的cpu_partial 最大值,则要进行unfreeze_partial操作。下面分析该函数。主要逻辑就是将cpu_slab->partial中的slab page都放到node->partial中,然后更新统计信息,当前cpu_slab->partial为空。

[3] 将该slab page加入到cpu_slab->partial链表头部。

[4] 最后将整个链表更新到cpu_slab->partial

unfreeze_partials

unfreeze_partials函数主要是在cpu_slab->partial中slab page数量已经达到最大值的时候将cpu_slab->partial中的slab page都转移到node->partial中,如果遇到空的slab page,则会释放掉。

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中的page
		struct page new;
		struct page old;

		slub_set_percpu_partial(c, page);//取出头部的,更新新头部

		n2 = get_node(s, page_to_nid(page));//获取page所在node
		if (n != n2) {
			if (n)
				spin_unlock(&n->list_lock);

			n = n2;
			spin_lock(&n->list_lock);
		}

		do {

			old.freelist = page->freelist;
			old.counters = page->counters;//[2] 获取page 的基本信息联合体
			VM_BUG_ON(!old.frozen);

			new.counters = old.counters;
			new.freelist = old.freelist;

			new.frozen = 0;//要从cpu_slab->partial中拿出来,肯定要解冻

		} while (!__cmpxchg_double_slab(s, page,
				old.freelist, old.counters,//更新page 信息
				new.freelist, new.counters,
				"unfreezing slab"));

		if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {//[3] 如果page 为空,并且node->partial数量满足最小要求
			page->next = discard_page;//把page加入到要释放的page列表
			discard_page = page;
		} else {//[3.1] 否则就加入到node的的partial中
			add_partial(n, page, DEACTIVATE_TO_TAIL);
			stat(s, FREE_ADD_PARTIAL);
		}
	}

	if (n)
		spin_unlock(&n->list_lock);

	while (discard_page) {//[4] 如果释放page 列表不为空
		page = discard_page;
		discard_page = discard_page->next;

		stat(s, DEACTIVATE_EMPTY);
		discard_slab(s, page);//依次调用discard_slab 释放掉slab page
		stat(s, FREE_SLAB);
	}
#endif	/* CONFIG_SLUB_CPU_PARTIAL */
}

[1] 遍历cpu_slab->partial 中的每一个slab page,并从链表中依次取出

[2] 获取page 的基本信息,然后更新基本信息,这里是将frozen 设为0,也就是解冻,从cpu_slab中移除

[3] 如果slab page的inuse 为0,说明该slab page已经为空,并且如果node->partial页满足最小数量要求,则将其加入到待释放的slab page的列表

​ [3.1] 否则将其加入到node->partial

[4] 将所有空的可以释放的page调用discard_slab释放掉

discard_slab->free_slab

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);//更新node中的slab 和object数量,释放了要减少
	free_slab(s, page);//free_slab 释放slab
}

discard_slab中先调用dec_slabs_node进行一些统计,更新一下slab page被释放后node中的slab page数量和堆块数量。

然后调用free_slab 进行正式的slab page释放:

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 page,不太关心,默认不开启。正常流程调用__free_slab释放slab page:

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
}

逻辑很简单,因为在之前已经将该slab page从node中unlink 出来了,那么接下来只需要释放该page即可。在__free_slab 中进行一些常规的检查和统计更新,最后调用__free_pages将页面释放。

特殊slab的释放

特殊slab使用kmem_cache_free函数来释放堆块:

static void file_free_rcu(struct rcu_head *head)
{
	struct file *f = container_of(head, struct file, f_u.fu_rcuhead);

	put_cred(f->f_cred);
	kmem_cache_free(filp_cachep, f);//专属slab的释放函数
}

kmem_cache_free 虽然也是直接封装的slab_free ,但释放前会使用cache_from_obj函数校验一下,避免其他堆块的释放到错误的slab cache中:

void kmem_cache_free(struct kmem_cache *s, void *x)
{
	s = cache_from_obj(s, x);//先校验一下是不是这个slab的堆块
	if (!s)
		return;
    //调用slab_free正常free掉x
	slab_free(s, virt_to_head_page(x), x, NULL, 1, _RET_IP_);
	trace_kmem_cache_free(_RET_IP_, x, s->name);
}

校验逻辑无非就是根据堆块找page,根据page找slab_cache,不多赘述了。

内存释放逻辑总结

在这里插入图片描述

内存对象释放主要思路就是,如果是页面对象,则直接伙伴系统释放,如果是slab 对象,如果在cpu_slab中,在cpu_slab中释放,如果不是,则根据释放之前之后的状态(为空、为满、半满),进行不同的操作。

  • 首先找到释放的内存对象所在的page 的page结构体。如果该page不是slab,也就是说内存对象不是通过slab分配的,而是直接分配的页(大块内存),那么直接释放页。

    • 大块内存分配的时候就是从伙伴系统直接分配的页,释放的时候页通过伙伴系统释放页面(__free_pages)
  • 其他内存是通过slab分配,则通过slab释放(slab_free)

    • memcg相关处理(memcg_slab_free_hook)

    • 获取当前cpu_slab,如果要释放的内存对象正好属于当前cpu_slab(可以理解为是否是从当前cpu_slab分配的),则快速释放

      • 获取cpu_slabfreelist,将该内存对象插入freelist头部,刷新cpu_slab相关信息(do_slab_free)
    • 如果要释放的内存对象不属于当前cpu_slab,(当前slab page在cpu_slab->partial、别的cpu_slab->page、别的cpu_slab->partial、游离状态、node->partialnode->full6种情况),需要慢速释放(__slab_free)

      • 先把内存对象释放到slab page的freelist头部,更新slab page相关统计信息
      • 如果该slab page为冻结状态(说明是在cpu_slab中的三种情况)
        • 则直接结束(已经将object 放到page->freelist了,剩下的就不用管了)
      • 如果释放前该slab page是满的(freelist为空),则说明page目前是游离状态(不在任何列表中)或node->full
        • 如果开启cpu->partial,则将该slab page放到cpu_slab->partial中(从node->full中移除)
          • 如果cpu_slab->partial满了,则要将当前cpu_slab->partial中的所有slab page放到node->partial中,然后再将新的slab page放到cpu_slab->partial
        • 否则放入node->partial中(从node->full中移除)
      • 如果释放后为空,则说明目前该slab page一定处在node->partial列表中,因为如果在cpu_slab或者游离状态或node->full中不管释放完是否为空,都会在上面的步骤中处理完毕
        • 如果当前slab 管理的partial页面数量满足最小要求,则将该释放后为空的slab page释放掉(__free_pages)
        • 否则不变,继续呆在node->partial
      • 否则说明该page是本来就呆在node->partial中的半满page,并且释放后还是半满,则什么也不操作。

slab page状态转换关系图

结合kmallockfree的逻辑,可以画出slab page的状态转换关系图:

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值