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

背景

省流

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

前情回顾

关于slab几个结构体的关系和初始化请见:

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

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

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

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

描述方法约定

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

kmalloc 操作总览

简介

kmalloc 是slub内存分配的核心函数,还是挺复杂的,需要仔细分析。挺不爽的地方在于,kmalloc很多函数都是什么inline 要么就是allways_inline啥的,调试的时候很麻烦。必要时可以把这些inline 去掉再编译。

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

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

逻辑图预览

在这里插入图片描述

调用栈

kmalloc

  • kmalloc_large 大内存

    • kmalloc_order_trace
      • kmalloc_order
        • alloc_page 直接申请页用来分配
  • kmalloc_index 获取下标

  • kmalloc_type 根据flag 获取slab

  • kmem_cache_alloc_trace/__kmalloc

    • slab_alloc => slab_alloc_node (nodeNUMA_NO_NODE )

      • slab_pre_alloc_hook 分配前hook,里面有内存计数cache 相关操作,可能切换slab

      • kfence_alloc 类似检验内存破坏,每隔一段时间才会调用成功

      • this_cpu_read; raw_cpu_ptr 获取cpu_slab

      1. 当前cpu freelist 为空page 为空 或者pagenode 匹配(slab_alloc 调用nodeNUMA_NO_NODE) 时使用 __slab_alloc(封装___slab_alloc) 分配
        1. 从当前cpu_slabfreelist 分配

        2. cpu->partical 中分配

        3. 如果当前cpu_slab->pagecpu_slab->partical都空,则新申请slab

          • new_slab_objects
            • get_partial node有partial slab则使用node的
            • new_slab=>allocate_slab
              • alloc_slab_page
                • alloc_pages
                • shuffle_freelist 是否freelist随机
      2. 否则使用从cpu_slab->freelist 里分配,然后更新freelisttid
      • maybe_wipe_obj_freeptr & slab_want_init_on_alloc 初始化
      • slab_post_alloc_hook 分配结尾的动作

详细分析

kmalloc

kmalloc 的入口肯定是kmalloc啊,直接看函数:

linux\include\linux\slab.h : kmalloc

static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
	if (__builtin_constant_p(size)) { //编译之前确定size是常量或变量
#ifndef CONFIG_SLOB//这里分析SLUB,不考虑SLOB相关(不共存)
		unsigned int index;
#endif
		if (size > KMALLOC_MAX_CACHE_SIZE)//[1]大于slab 支持的最大分配大小直接调用页分配
			return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
		index = kmalloc_index(size);//[2]根据需要分配的大小获取slab 下标

		if (!index)
			return ZERO_SIZE_PTR;

		return kmem_cache_alloc_trace(//[3]进入分配流程,预编译是常量的
				kmalloc_caches[kmalloc_type(flags)][index],
				flags, size);
#endif
	}
	return __kmalloc(size, flags);// [4] 变量size走这里
}

[1] slab支持分配的大小有限,最大是两页也就是8k,再大的内存会调用kmalloc_large分配,里面是直接分配页。后面分析。

[2] 调用kmalloc_index根据申请的内存大小获取对应的slab管理结构下标。

static __always_inline unsigned int kmalloc_index(size_t size)
{
	if (!size)
		return 0;

	if (size <= KMALLOC_MIN_SIZE)
		return KMALLOC_SHIFT_LOW;

	if (KMALLOC_MIN_SIZE <= 32 && size > 64 && size <= 96)
		return 1;
	if (KMALLOC_MIN_SIZE <= 64 && size > 128 && size <= 192)
		return 2;
	if (size <=          8) return 3;
	if (size <=         16) return 4;
	··· ···
	if (size <=   4 * 1024) return 12;
	if (size <=   8 * 1024) return 13;
	··· ···

	/* Will never be reached. Needed because the compiler may complain */
	return -1;
}

[3] 如果预编译的时候确定size是不变量,会调用kmalloc_type根据申请flag 获取对应的slab 类型,和刚获取的slab 下标,得到最终的合适的slab 管理结构,传入

kmem_cache_alloc_trace来进行分配。

static __always_inline enum kmalloc_cache_type kmalloc_type(gfp_t flags)
{
#ifdef CONFIG_ZONE_DMA
	/*
	 * The most common case is KMALLOC_NORMAL, so test for it
	 * with a single branch for both flags.
	 */
	if (likely((flags & (__GFP_DMA | __GFP_RECLAIMABLE)) == 0))
		return KMALLOC_NORMAL;

	/*
	 * At least one of the flags has to be set. If both are, __GFP_DMA
	 * is more important.
	 */
	return flags & __GFP_DMA ? KMALLOC_DMA : KMALLOC_RECLAIM;
#else
	return flags & __GFP_RECLAIMABLE ? KMALLOC_RECLAIM : KMALLOC_NORMAL;
#endif
}

[4] 动态的size则会调用__kmalloc进行分配

__kmalloc…slab_alloc_node

不管是__kmalloc还是kmem_cache_alloc_trace都只是封装了slab_alloc函数:

linux\include\linux\slub.c : __kmalloc

void *__kmalloc(size_t size, gfp_t flags)
{
	struct kmem_cache *s;
	void *ret;

	if (unlikely(size > KMALLOC_MAX_CACHE_SIZE))
		return kmalloc_large(size, flags);

	s = kmalloc_slab(size, flags); //类似上面分析的,根据下标和type获取cache

	if (unlikely(ZERO_OR_NULL_PTR(s)))
		return s;

	ret = slab_alloc(s, flags, _RET_IP_, size);//调用slab_alloc完成内存分配

	trace_kmalloc(_RET_IP_, ret, size, s->size, flags);

	ret = kasan_kmalloc(s, ret, size, flags);

	return ret;
}

linux\include\linux\slub.c : kmem_cache_alloc_trace

#ifdef CONFIG_TRACING //[1]开启CONFIG_TRACING时执行如下函数
void *kmem_cache_alloc_trace(struct kmem_cache *s, gfp_t gfpflags, size_t size)
{
	void *ret = slab_alloc(s, gfpflags, _RET_IP_, size);//[2]直接调用slab_alloc
	trace_kmalloc(_RET_IP_, ret, size, s->size, gfpflags);
	ret = kasan_kmalloc(s, ret, size, gfpflags);
	return ret;
}

[1] 开启CONFIG_TRACING 的时候执行slub.c 中的kmem_cache_alloc_trace,默认开启。

[2] 这里直接调用slab_alloc

linux\include\linux\slub.c : slab_alloc

static __always_inline void *slab_alloc(struct kmem_cache *s,
		gfp_t gfpflags, unsigned long addr, size_t orig_size)
{
	return slab_alloc_node(s, gfpflags, NUMA_NO_NODE, addr, orig_size);//[1]NUMA_NO_NODE
}

slab_alloc 就是封装了一层slab_alloc_node

[1] node 设置为了NUMA_NO_NODE,代表从当前正在运行的cpu的node节点分配内存。

linux\include\linux\slub.c : slab_alloc_node

static __always_inline void *slab_alloc_node(struct kmem_cache *s,
		gfp_t gfpflags, int node, unsigned long addr, size_t orig_size)
{
	void *object;
	struct kmem_cache_cpu *c;
	struct page *page;
	unsigned long tid;
	struct obj_cgroup *objcg = NULL;
	bool init = false;

	s = slab_pre_alloc_hook(s, &objcg, 1, gfpflags);//[1]对内存分配进行预处理,不同版本实现不同
	if (!s)
		return NULL;

	object = kfence_alloc(s, orig_size, gfpflags);  //[2]内存抽样,每隔一段时间从kfence里分配一次
	if (unlikely(object))
		goto out;

redo:
	do {//[3]与CPU抢占模式有关,读取当前可用cpu_slab和tid,CONFIG_PREEMPTION默认不开启。
		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)));

	barrier();//内存屏障,刷新cpu 缓存

	object = c->freelist;//[4]获取cpu_slab当前的freelist的第一个对象
	page = c->page;//以及slab所在page
	if (unlikely(!object || !page || !node_match(page, node))) {//如果object和page任意为空则无法立刻申请
		object = __slab_alloc(s, gfpflags, node, addr, c);
	} else {//[5]不为空则说明可以直接用当前freelist 分配
		void *next_object = get_freepointer_safe(s, object);//[5.1]获取freelist的下一个空闲堆块

		if (unlikely(!this_cpu_cmpxchg_double(//[5.2]this_cpu_cmpxchg_double()原子指令操作取得该空闲对象
				s->cpu_slab->freelist, s->cpu_slab->tid,
				object, tid,
				next_object, next_tid(tid)))) {
            //操作等价于
            // object = s->cpu_slab->freelist;
            // s->cpu_slab->freelist=next_object;
            // tid=s->cpu_slab->tid;s->cpu_slab->tid=next_tid(tid)
			//[5.3]如果失败则经note_cmpxchg_failure()记录日志后重回redo标签再次尝试分配。
			note_cmpxchg_failure("slab_alloc", s, tid);
			goto redo;
		}
		prefetch_freepointer(s, next_object);//[5.4]预取操作,优化相关
		stat(s, ALLOC_FASTPATH);//设置状态,CONFIG_SLUB_STATS 默认不开启则没用
	}

	maybe_wipe_obj_freeptr(s, object);//这俩函数初始化相关
	init = slab_want_init_on_alloc(gfpflags, s);

out:
	slab_post_alloc_hook(s, objcg, gfpflags, 1, &object, init);//[6]分配后的一些乱七八糟的处理

	return object;
}

[1] 对slab分配的一些预处理hook,这里有CONFIG_MEMCG相关的操作,也就是内存分配的时候是否带有__GFP_ACCOUNTflag(内存申请计数)。为什么忽然提这个呢,因为在不同的linux 内核版本,这一块实现是不同的,在有些版本中(<5.9),MEMCG的实现方式是需要计数的申请统一从一个单独的slab 管理结构(struct kmem_cache)分配,这就导致会和不带__GFP_ACCOUNTflag 的内存分配不到一起去,对漏洞利用会造成影响。后续会独立分析。

[2] kfence相关,其实就是每隔一段时间会从一个叫做kfence的内存池中分配一块内存,主要是抽样检测内存越界等操作。没啥用不详细分析了。参考https://blog.csdn.net/pwl999/article/details/124494958

[3] 这里读取slab管理结构中的cpu_slab(当前cpu)和它对应的tid,之所以代码写成这样是涉及到cpu 的抢占模式,由于我们只分析内存分配逻辑,所以不考虑cpu抢占场景,默认就一直使用同一个cpu。值得一提的是CONFIG_PREEMPTION默认不开启。

[4] 获取当前cpu_slabfreelist的第一个内存堆块和freelist 所在slab page。并且进行判断这两个对象是否存在(指针是否为空),如果为空则说明当前cpu_slab还没有可以立刻分配的slab ,要么cpu_slab 分配完了(freelist为空page不为空),要么cpu_slab还没有slab(page为空)。则需要调用__slab_alloc申请新的slab。后面详细分析。

[5] 如果不为空则说明可以直接在这个freelist 进行分配(freelist的第一个对象object拿出来分配就行)。但操作复杂一些,涉及到抢占之类的。

​ [5.1] 先获取freelist 的第二个空闲堆块,也就是object的下一个对象。get_freepointer_safe操作在下一章单独分析,这里值分析大流程。

​ [5.2] 使用一个原子操作(单cpu时间)来完成以下操作,即this_cpu_cmpxchg_double函数里面有一些关于判断抢占的逻辑,我们不考虑,实际有意义的操作等价于如下四步:

object = s->cpu_slab->freelist;
s->cpu_slab->freelist=next_object;
tid=s->cpu_slab->tid;
s->cpu_slab->tid=next_tid(tid)

​ [5.3] 如果上面原子操作this_cpu_cmpxchg_double没执行成功,则note_cmpxchg_failure记录日志,然后redo重来一遍

​ [5.4] 把next_object的下一个对象放到快速缓存吧,反正优化相关,不重要;后面的stat是设置状态,跟CONFIG_SLUB_STATS有关,这编译选项默认不开启,则不用关心。

[6] 分配完内存之后,可能对内存进行一些kasanmemcg相关操作,比如擦除内存里的别的内容之类的。或者打标记什么乱七八糟的,但总之进行到这里内存已经分配完毕了,不太重要了。

__slab_alloc

走到这里说明当前cpu_slab还没有可以立刻分配的slab ,要么cpu_slab 分配完了(freelist为空),要么cpu_slab还没有slab(page为空),需要调用__slab_alloc分配新的slab。

linux\mm\slub.c : __slab_alloc

static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
			  unsigned long addr, struct kmem_cache_cpu *c)
{
	void *p;
	unsigned long flags;

	local_irq_save(flags);
#ifdef CONFIG_PREEMPTION//默认不开启,开启抢占则需要重新获取一下当前cpu_slab
	c = this_cpu_ptr(s->cpu_slab);
#endif

	p = ___slab_alloc(s, gfpflags, node, addr, c);//直接调用___slab_alloc
	local_irq_restore(flags);
	return p;
}

这里可以看出__slab_alloc就是封装了一下直接调用___slab_alloc:

linux\mm\slub.c : ___slab_alloc

static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
			  unsigned long addr, struct kmem_cache_cpu *c)
{
	void *freelist;
	struct page *page;

	stat(s, ALLOC_SLOWPATH);

	page = c->page;//[1]获取当前cpu 的page
	if (!page) {//page 为空则去new_slab 搞一个新slab
		if (unlikely(node != NUMA_NO_NODE &&//[1.1]如果传入不是NUMA_NO_NODE,说明指定了node
			     !node_isset(node, slab_nodes)))//并且该node还没分配kmem_cache_node的话
			node = NUMA_NO_NODE;//就设置为NUMA_NO_NODE任意node都可以
		goto new_slab;//然后去申请一个新的slab page
	}
redo:

	if (unlikely(!node_match(page, node))) {//[2] 一般不会走到这个分支,cpu_slab中的page不属于指定的node
		if (!node_isset(node, slab_nodes)) {//[2.1] 指定的node没分配kmem_cache_node
			node = NUMA_NO_NODE;//将node设定成任意node然后重来即可与过这里的判断了
			goto redo;
		} else {//[2.2] 虽然page和node不匹配,但可以从指定node分配,则将指定node换上来,page换下去
			stat(s, ALLOC_NODE_MISMATCH);
			deactivate_slab(s, page, c->freelist, c);//[2.3]强制下架,将指定node 的slab换到cpu_slab中
			goto new_slab;
		}
	}

	if (unlikely(!pfmemalloc_match(page, gfpflags))) {//[3]一般不会进入这个分支,检验page和gfpflags
		deactivate_slab(s, page, c->freelist, c);//下架
		goto new_slab;
	}
	//走到这里说明cpu_slab至少有page
    //(要么原本就有page,但分配满了所以走到这,要么是从node刚换上来的,大概率有可以分配的堆块),
	/* must check again c->freelist in case of cpu migration or IRQ */
	freelist = c->freelist;//[4]获取cpu_slab的空闲链表,有page不一定代表有free_list
	if (freelist)//[4.1]不为空则去load_freelist,直接从freelist 分配
		goto load_freelist;

	freelist = get_freelist(s, page);//[4.2]如果freelist为空则从page重新获取,可能有刚被其他CPU释放的堆块可用
    								//或者是是新拿到的page,则设置page状态

	if (!freelist) {//[4.3]如果还是为获取不到,说明page分配满了,那就new_slab
		c->page = NULL;//会将page设置为null,这里也说明在cpu_slab中分配满的page会直接将其"扔掉"
		stat(s, DEACTIVATE_BYPASS);
		goto new_slab;
	}

	stat(s, ALLOC_REFILL);

load_freelist://[5]直接从freelist分配
	VM_BUG_ON(!c->page->frozen);
	c->freelist = get_freepointer(s, freelist);//freelist 向后移动
	c->tid = next_tid(c->tid);//更新tid
	return freelist;//返回freelist第一个对象就是要分配的

new_slab://[6]申请新的slab
	if (slub_percpu_partial(c)) {//[6.1]如果cpu_slab partical 有page先从partical 取
		page = c->page = slub_percpu_partial(c);//[6.2]从partial中取出第一个page更新cpu_slab->page
		slub_set_percpu_partial(c, page);//partical指向下一个
		stat(s, CPU_PARTIAL_ALLOC);
		goto redo;//在上面重来,这样就可以从新partical page 的freelist里申请了
	}
	//[6.3]都没有就要申请一个全新slab page了然后分配
	freelist = new_slab_objects(s, gfpflags, node, &c);

	if (unlikely(!freelist)) {//这还失败就返回空,申请失败
		slab_out_of_memory(s, gfpflags, node);
		return NULL;
	}

	page = c->page;//[6.4]如果未开启调试且页面属性匹配pfmemalloc
	if (likely(!kmem_cache_debug(s) && pfmemalloc_match(page, gfpflags)))
		goto load_freelist;

	/* Only entered in the debug case */
	if (kmem_cache_debug(s) &&
			!alloc_debug_processing(s, page, freelist, addr))
		goto new_slab;	/* Slab failed checks. Next slab needed */

	deactivate_slab(s, page, get_freepointer(s, freelist), c);
	return freelist;
}

[1] 获取当前cpu_slab正在使用的page,如果为空则说明当前cpu_slab->page为空即没有slab page可以尝试立刻分配则会去new_slab分支申请新slab

​ [1.1] 这里进行了一个node 的判断,___slab_alloc函数的node参数是指定从特定node分配的意思,但这里kmalloc路径下来这里传入的参数是NUMA_NO_NODE,代表"从当前cpu 的node执行"的意思。如果当前指定node不是NUMA_NO_NODE,说明传入路径明确指定"从某个特定node分配"并且如果指定的node还没分配kmem_cache_node 结构的话,则会将node设置为NUMA_NO_NODE,也就是放弃指定node,从当前cpu 使用node中分配。

[2] 当前cpu_slabpage,这种情况下大概率是当前cpu_slab->page分配满了,需要先判断node是否和当前cpu 使用的slab page所在同一个node,如果传入是NUMA_NO_NODE,则相当于同一个,则不会进入该分支。

​ [2.1] 如果指定了node并且当前cpu使用内存node不是指定的node,并且该node还没分配kmem_cache_node结构的话,则将node重置为NUMA_NO_NODE继续(跟上面一样)。

​ [2.2] 否则说明虽然指定的node和当前cpu_slab->page所属node不一样,但指定的node可以正常分配,则调用deactivate_slab函数强制下架cpu_slab正在使用的slab page(也就是cpu_slab->page),后续会换上指定node的slab page。

​ [2.3] 所谓强制下架,就是将cpu_slabfreelist 和所在page 根据当前状态(部分空闲partical或满full)放入对应node 的kmem_cache_node 结构体的particalfull链表中。然后改变一些状态,目的就是*我不打算在你当前的cpu_slab中分配了,先把你cpu_slab中的page放到属于它的kmem_cache_node,细节在下一章节进行分析。

[3] 通常不会走到这个分支之中,如果page 开启了PF_MEMALLOC并且传入kmalloc 的分配flag也有特定设置的话,也会进行slab切换(强制下架然后new_slab)。这里不仔细分析了,反正unlikely。

[4] 走到这里说明不管怎样,这个cpu_slab至少是有slab page的(cpu_slab->page不为空),首先获取cpu_slabfreelist,也就是立马就能分配的内存

​ [4.1] 如果freelist不为空,那可以直接分配,走到load_freelist分支直接进行分配。

​ [4.2] **cpu_slabfreelist为空则尝试一下从page获取freelist。**虽然走到这个函数的大部分情况说明cpu_slab->freelist是空的,但却并不代表cpu_slab->page 无法完成分配,因为会出现"A CPU 释放了B CPU的cpu_slab->page中的堆块到B CPU的cpu_slab->page->freelist"的情况,这样虽然B CPU的cpu_slab->freelist为空,但B CPU的cpu_slab->page->freelist可能存在刚被A CPU释放的堆块可用,所以这里通过get_freelist 重新获取一下。除此之外,后面代码的逻辑会给cpu_slab 一个新的page(从node换上来或申请),然后redo重来,再到这里也会通过该函数获取freelist,同时还会根据page的状态进行不同的操作:

static inline void *get_freelist(struct kmem_cache *s, struct page *page)
{//传入的page可能为新申请的page或以前无法分配的page,新page则更新状态,老page解冻准备移除cpu_slab
	struct page new;
	unsigned long counters;
	void *freelist;

	do {
		freelist = page->freelist; //获取page 的freelist 
		counters = page->counters; //获取counters(联合体,里面包含frozen等状态信息)

		new.counters = counters; 
		VM_BUG_ON(!new.frozen);

		new.inuse = page->objects;//更新page counters
		new.frozen = freelist != NULL;
		//如果freelist 为空,则说明page不是新page,是已经分配满了的page,这里解冻,后续会从cpu_slab中移除
	} while (!__cmpxchg_double_slab(s, page,
		freelist, counters,
		NULL, new.counters,
		"get_freelist"));
     /* 原子操作
      * page->freelist = null; page->counters = new.counters
      * 只要被cpu_slab控制的page,freelist 都要设置为null,因为以cpu_slab->freelist为准
      */ 
	return freelist;
}

​ 如果传入get_freelistpage是在cpu_slab中分配满了的page,则会将其解冻(frozen字段设为0),后续会从cpu_slab->page中删除。

​ [4.3] 如果page->freelist 也为空的话,则说明当前cpu_slab 没可以直接分配内存的slab,那就new_slab分支申请新slab。这里之所以分别判断,是因为后面重新分配了cpu_slab->page后重新回到这里判断一遍。

这里会将cpu_slab->page直接置空,而不会将之前的page 先拿下来放到其他表中(如node的full列表中),这是因为如果cpu_slab->page如果还有,那也一定是分配满了才会走到这里,并且它已经刚从get_freelist函数中"解冻",现在将其置空会进入"不在任何列表中"的一种游离状态,它虽然不在任何列表中,但你释放这个slab page中的堆块的时候,会重新将其加入到某个列表之中。

[5] 直接把freelist 第一个内存块分配出去就行,然后freelist 往后移动一格,更新tid啥的。

[6] 这里是申请一个全新slab。走到这里的话cpu_slab->page一定是空的,要么之前的page已经被强制下架,要么就是分配满了被舍弃进入"游离状态"。

​ [6.1] 首先判断cpu_slabpartial链表是否为空,如果不为空,则从partial链表中取出一个slab page,该slab一定还有空闲内存块可以分配,然后partical 链表指向下一个page,然后重头开始重新判断一下,这样走到[4] 的时候就可以从新pagefreelist重新分配了。

#define slub_percpu_partial(c)		((c)->partial)
#define slub_set_percpu_partial(c, p)		\
({						\
	slub_percpu_partial(c) = (p)->next;	\
})

​ [6.2] 从cpu_slab->partial获取到新的page之后赋值给cpu_slab->page

​ [6.3] 如果partial 也没有slab page了,那就要完全重新分配一个了。调用new_slab_objects 完全分配一个全新slab page。

​ [6.4] 申请到新的slab了之后,如果没有什么其他乱七八糟的配置,那就回到load_freelist,从新slab page的freelist 分配。后面的就是一些乱七八糟的情况,比如新slab flag不匹配啥的。我们不详细分析了。

new_slab_objects

new_slab_objects函数尝试从node中拿一个可用slab或负责分配一个新的slab,全新的page:

linux\mm\slub.c : new_slab_objects

static inline void *new_slab_objects(struct kmem_cache *s, gfp_t flags,
			int node, struct kmem_cache_cpu **pc)
{
	void *freelist;
	struct kmem_cache_cpu *c = *pc;
	struct page *page;

	WARN_ON_ONCE(s->ctor && (flags & __GFP_ZERO));

	freelist = get_partial(s, flags, node, c);//[1]获取node 的partical slab

	if (freelist)
		return freelist;

	page = new_slab(s, flags, node);//[2]调用new_slab分配新page
	if (page) {
		c = raw_cpu_ptr(s->cpu_slab);//[2.1]获取当前cpu
		if (c->page)//[2.2]如果当前cpu有page则flush_slab刷新吗,这里会将page强制下架
			flush_slab(s, c);

		freelist = page->freelist;//[2.3]更新freelist、page
		page->freelist = NULL;//被cpu_slab控制的page 的freelist 都要设置为NULL

		stat(s, ALLOC_SLAB);
		c->page = page;//该page被cpu_slab控制
		*pc = c;
	}

	return freelist;//[3]返回freelist
}

[1] 在上一个函数___slab_alloc我们只确认了cpu_slab 没有可用patial slab了,但不代表对应的node 没有可用partial slab了,这里调用get_partial尝试从node 中获取一个可用slab返回。后续分析get_partial函数。

[2] 如果node也没有了,那就new_slab 重新申请一个全新(这次真是全新了)slab,也就是一个新page

​ [2.1] 重新获取一下正在使用的cpu,不考虑抢占可以忽略。

​ [2.2] 这里如果当前cpu是有cpu_slab->page的,则会调用flush_slabpage强制下架,让cpu_slab使用刚申请到的新page(否则新page没地方可放,总不能刚申请完就放到node中)。

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);//强制下架page

	c->tid = next_tid(c->tid);
}

​ [2.3] 然后更新一下cpu_slab->page、更新page->freelist等,注意所有被cpu_slab控制的pagefreelist 都要设置为NULL。

[3] 返回新slab 的freelist,可以回到上面函数立即参与分配。

get_partial部分

linux\mm\slub.c : get_partial

static void *get_partial(struct kmem_cache *s, gfp_t flags, int node,
		struct kmem_cache_cpu *c)
{
	void *object;
	int searchnode = node;

	if (node == NUMA_NO_NODE)//[1]NUMA_NO_NODE获取当前node
		searchnode = numa_mem_id();

	object = get_partial_node(s, get_node(s, searchnode), c, flags);//[2]从这个node获取partial 链表
	if (object || node != NUMA_NO_NODE)
		return object;

	return get_any_partial(s, flags, c);//[3]随便来一个
}

[1] 如果node是NUMA_NO_NODE则获取当前node就行了。

[2] 从指定node 获取partial slab 的freelist,返回的是freelist,除此之外,还会把freelist所属的page给cpu_slab->page 赋值并且从node->partial 中删除,除此之外还会从node->partial 中获取若干page补充到cpu_slab->partial中,详细操作在下一章节中分析,因为对kmalloc整体流程影响不大。

[3] 实在没有就调用get_any_partial随便来一个,get_any_partial的逻辑就是遍历所有node,找到一个有partial的node直接对齐调用上面说过的get_partial_node,获取这个node里的page资源。

new_slab

申请全新slab:

linux\mm\slub.c : new_slab

static struct page *new_slab(struct kmem_cache *s, gfp_t flags, int node)
{
	if (unlikely(flags & GFP_SLAB_BUG_MASK))
		flags = kmalloc_fix_flags(flags);

	return allocate_slab(s,
		flags & (GFP_RECLAIM_MASK | GFP_CONSTRAINT_MASK), node);
}

可以看到直接调用了allocate_slab,补充了一些flag

linux\mm\slub.c : allocate_slab

static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
	struct page *page;
	struct kmem_cache_order_objects oo = s->oo;
	gfp_t alloc_gfp;
	void *start, *p, *next;
	int idx;
	bool shuffle;

	··· ···

	page = alloc_slab_page(s, alloc_gfp, node, oo);//[1]分配一个新slab页
	if (unlikely(!page)) {//[1.1]如果分配失败,则尝试缩小order重新分配
		oo = s->min;
		alloc_gfp = flags;

		page = alloc_slab_page(s, alloc_gfp, node, oo);
		if (unlikely(!page))
			goto out;
		stat(s, ORDER_FALLBACK);
	}

	page->objects = oo_objects(oo);//[2]设置page中的内存块数

	account_slab_page(page, oo_order(oo), s, flags);//统计页数相关

	page->slab_cache = s;//[3]page指向自己的slab
	__SetPageSlab(page);//[3]page相关宏,修改page属性为slab
	if (page_is_pfmemalloc(page))
		SetPageSlabPfmemalloc(page);

	kasan_poison_slab(page);

	start = page_address(page);//[3]获取page 对应的虚拟地址

	setup_page_debug(s, page, start);

	shuffle = shuffle_freelist(s, page);//[4]CONFIG_SLAB_FREELIST_RANDOM相关

	if (!shuffle) {//[5]不随机,一般都开启随机
		start = fixup_red_left(s, start);//开启左边越界检测相关
		start = setup_object(s, page, start);
		page->freelist = start;//[5.1]设置freelist
		for (idx = 0, p = start; idx < page->objects - 1; idx++) {//[5.1]依次给page 分块,每块之间链表链接
			next = p + s->size;
			next = setup_object(s, page, next);
			set_freepointer(s, p, next);
			p = next;
		}
		set_freepointer(s, p, NULL);
	}

	page->inuse = page->objects;
	page->frozen = 1;//[6]frozen代表在cpu_slab 中,unfroze代表在partial队列或者full队列

out://退出环节
	if (gfpflags_allow_blocking(flags))
		local_irq_disable();
	if (!page)
		return NULL;

	inc_slabs_node(s, page_to_nid(page), page->objects);//增长node的slab和object计数

	return page;
}

[1] 调用alloc_slab_page 给slab分配一个新页,这里kmem_cache->oo 高2字节代表该slab 每次申请新页的阶数。后面分析alloc_slab_page 函数。

​ [1.1] 如果分配失败,则尝试缩小ookmem_cache->min 再次尝试分配。还不行就退出。

[2] 然后设定pageobject,也就是管理的内存块数量,即oo 的低两字节

#define OO_SHIFT	16
#define OO_MASK		((1 << OO_SHIFT) - 1) //16
static inline unsigned int oo_objects(struct kmem_cache_order_objects x)
{
	return x.x & OO_MASK;
}

[3] page要指向自己的slab,并通过改变page属性的宏将page 页面属性改为slab,这就代表这是一个slab 页。然后通过page_address获取page 结构体对应的实际页虚拟地址

[4] CONFIG_SLAB_FREELIST_RANDOM相关,开启了之后要把这个page 按照slab 规定大小分割成几块,这几块顺序随机化。后续分析。

[5] 如果没有开启freelist 随机化,那么就直接按顺序排列freelist

​ [5.1] 将这个页(不一定是一页,但肯定是连续的),按照slab 大小分割之后,依次按顺序组合成freelist,指针首尾相接。在没开启freelist 随机化的情况下,set_freepointer的操作如下,直接获取地址偏移s->offset 的地方就是指向下一个内存块的指针:

static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)
{
	unsigned long freeptr_addr = (unsigned long)object + s->offset;

#ifdef CONFIG_SLAB_FREELIST_HARDENED
	BUG_ON(object == fp); /* naive detection of double free or corruption */
#endif

	freeptr_addr = (unsigned long)kasan_reset_tag((void *)freeptr_addr);
	*(void **)freeptr_addr = freelist_ptr(s, fp, freeptr_addr);
}

[6] 将page->frozen 设置为frozen,再回顾一下:frozen代表该pagecpu_slab 中,unfroze代表该pagepartial队列或者full队列或者"游离状态"

alloc_slab_page

linux\mm\slub.c : alloc_slab_page

static inline struct page *alloc_slab_page(struct kmem_cache *s,
		gfp_t flags, int node, struct kmem_cache_order_objects oo)
{
	struct page *page;
	unsigned int order = oo_order(oo);//[1]获取申请页面的阶数

	if (node == NUMA_NO_NODE)
		page = alloc_pages(flags, order);//[2]申请页面
	else
		page = __alloc_pages_node(node, flags, order);//[2]在特定node中申请页面

	return page;
}

[1] 通过oo获取要申请的页面阶数,即oo的高两字节,阶数就是申请多少页,如阶数2那就申请22=4页,阶数1就申请21=2页

#define OO_SHIFT	16
#define OO_MASK		((1 << OO_SHIFT) - 1) //16
static inline unsigned int oo_order(struct kmem_cache_order_objects x)
{
	return x.x >> OO_SHIFT;
}

[2] 如果node为NUMA_NO_NODE则在当前node调用alloc_pages从伙伴系统申请页面。否则在指定node 申请页面。

kmalloc_large->…->kmalloc_order

对于大于slab可以最大可以分配的内存,这里调用kmalloc_large分配,其实就是分配页面:

linux\include\linux\slab.h : kmalloc_large

static __always_inline void *kmalloc_large(size_t size, gfp_t flags)
{
	unsigned int order = get_order(size);//计算该大小需要的页的阶数
	return kmalloc_order_trace(size, flags, order);
}

先根据size计算出需要的页数,也就是页的阶数,然后调用kmalloc_order_trace

linux\mm\slab_common.c : kmalloc_order_trace

#ifdef CONFIG_TRACING
void *kmalloc_order_trace(size_t size, gfp_t flags, unsigned int order)
{
	void *ret = kmalloc_order(size, flags, order);
	trace_kmalloc(_RET_IP_, ret, size, PAGE_SIZE << order, flags);
	return ret;
}

直接调用kmalloc_order

linux\mm\slab_common.c : kmalloc_order

void *kmalloc_order(size_t size, gfp_t flags, unsigned int order)
{
	void *ret = NULL;
	struct page *page;

	if (unlikely(flags & GFP_SLAB_BUG_MASK))
		flags = kmalloc_fix_flags(flags);

	flags |= __GFP_COMP;
	page = alloc_pages(flags, order);//[1]调用alloc_page 分配页面
	if (likely(page)) {
		ret = page_address(page);//[2]返回page结构体对应的地址
		mod_lruvec_page_state(page, NR_SLAB_UNRECLAIMABLE_B,
				      PAGE_SIZE << order);
	}
	ret = kasan_kmalloc_large(ret, size, flags);
	/* As ret might get tagged, call kmemleak hook after KASAN. */
	kmemleak_alloc(ret, size, 1, flags);
	return ret;
}

[1] 直接调用alloc_pages申请页面。

[2] 根据页结构体获得页面实际虚拟地址,返回给上面使用。

特殊slab的分配

对于上一节中提到的特殊slab,如filp_cachep,则使用kmem_cache_alloc (kmem_cache_zlloc就是封装的kmem_cache_alloc )来申请,如:

static struct file *__alloc_file(int flags, const struct cred *cred)
{
	struct file *f;
	int error;

	f = kmem_cache_zalloc(filp_cachep, GFP_KERNEL);//专属slab的申请函数
	if (unlikely(!f))
		return ERR_PTR(-ENOMEM);

	··· ···
}

申请逻辑其实就是封装了一下slab_alloc函数,上文提到过,不过多赘述了:

void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)
{
	void *ret = slab_alloc(s, gfpflags, _RET_IP_, s->object_size);
	··· ···
}

其他分配入口

除了上面分析的分配路径,其实内存分配有很多的入口,但后面的逻辑都是相同的,都试用slab cache进行内存分配,常见的一些入口的流程如下:

在这里插入图片描述

内存分配逻辑总结

在这里插入图片描述

其实slab 就是几级缓存机制,从cpu_slab 分配最快,能从cpu_slab当前freelist 分配就从cpu_slab当前freelist 分配,不能则从partial 列表中把一个slab page拿出来放到freelist,如果还没有就从node 里找slab page,都没有再新申请slab page。

  • 首先判断请求kmalloc分配内存的大小,大于slab可以分配的上限(通常8k)则调用kmalloc_large进行分配。
    • kmalloc_large 则直接根据大小调用伙伴系统分配适当的页数。
  • 其他大小可以使用slab申请,则根据申请flag 和申请大小计算出kmalloc_caches的类型和index,获取对应的slab管理结构体。
    • 如果flag 存在ACCOUNT相关可能要切换到计数后slab,即kmalloc-cg相关slab(slab_pre_alloc_hook)
    • 获得slab 之后获取当前cpu 的cpu_slab,如果当前cpu_slabfreelist 不为空,则直接将freelist 的第一个分配返回,然后freelist向后移动,更新tid等。
    • 如果cpu_slabfreelist 为空,则看cpu_slabpartial 链表是否为空,不为空则将partial 链表第一个slab page 切换给cpu_slab freelist,然后分配,partial指向下一个slab page。
    • 如果以上都失败,说明cpu_slab无法完成分配,需要新slab,则会找到合适当前运行cpu 的内存node所属kmem_cache_node结构,查询partial 链表是否有可用slab page有的话将这个slab page给cpu_slab ,然后也从这个node取出一些slab page补充到cpu_slab->partial,然后从freelist 分配一个,跟上面一样。
    • 如果还是没有,则调用new_slab申请一个全新slab page,从新slab page的freelist 分配。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值