内核杂谈——vmalloc分配器的进化故事

从懵逼到不懵逼

vmalloc机制在最近的内核中做了大改,很有意思。通过对比的角度来看看最近一次的进化史。对比版本5.0和5.4

patch: commitID 68ad4a3304335358f95a417f2a2b0c909e5119c4

    mm/vmalloc.c: keep track of free blocks for vmap allocation
    
    Patch series "improve vmap allocation", v3.

5.1中提出的,5.2中合入。本文侧重点是空洞的处理,笔者第一次读vmalloc分配器的时候是比较懵逼的,因为早期vmalloc记录的是已分配出去的对象,而空闲块是隐蔽描述的,正常理解的思维像伙伴系统一样,查找空闲对象然后获取,但vmlloac不是。

直到上述patch出现,理解不再晦涩。
   

vmalloc的建立

在vmalloc_init之前使用vm_struct来记录分配出去的vmalloc区域,然后会链接在vmlist全局链表中。早期的vmalloc区域有per cpu使用的(见pcpu_page_first_chunk),还有内核使用的代码段数据段等(见map_kernel)。这些区域比较零散,在vmalloc_init中会将这些区域从vmlist组织到vmap_area红黑树中。讲到树,肯定离不开查找 删除 插入这些让人产生条件反射的想法。

上述过程没啥变化,变化主要在vmalloc_init中,该函数建立了vmalloc机制。在vmalloc_init中引入了一个新的小内存cache——vmap_area_cachep。不用多想,看到kmem_cache也要产生条件反射——vmap_area是一个频繁的操作对象,建立对应的小内存可以加快内存获取的速度。

void __init vmalloc_init(void)
{
	struct vmap_area *va;
	struct vm_struct *tmp;
	int i
	vmap_area_cachep = KMEM_CACHE(vmap_area, SLAB_PANIC);

	for (tmp = vmlist; tmp; tmp = tmp->next) {
		va = kmem_cache_zalloc(vmap_area_cachep, GFP_NOWAIT);
		va->va_start = (unsigned long)tmp->addr;
		va->va_end = va->va_start + tmp->size;
		va->vm = tmp;
		insert_vmap_area(va, &vmap_area_root, &vmap_area_list);
	}
}

va的作用就是接管vmlist里的已被使用的vmalloc区域,然后存放到红黑树中。将已用的va使用vmap_area红黑树管理,同时使用链表将这些va按顺序排序,红黑树方便查找区间,链表方便获取下一个对象。 这里使用两种结构,使得查找操作快一点,这个是保留的,改进的是区间的更新及查找。

进化一:空洞实体化

在vmalloc_init中添加了vmap_init_free_space。函数中出现了一个新的对象描述空洞。也就是free va。vmalloc_init之前链入vmlist的vmalloc区域不一定是连续的,如代码下的KVA space描述,B(usy)代表已被分配出去的vmalloc区域,F(ree)代表未被分配出去的vmalloc区域。

5.0代码中没有这个函数,也就是并未将free区域单独拎出来。

static void vmap_init_free_space(void)
{
	unsigned long vmap_start = 1;
	const unsigned long vmap_end = ULONG_MAX;
	struct vmap_area *busy, *free;

	/*
	 *     B     F     B     B     B     F
	 * -|-----|.....|-----|-----|-----|.....|-
	 *  |           The KVA space           |
	 *  |<--------------------------------->|
	 */
//拎出vmap_area_list中的空洞区域。
	list_for_each_entry(busy, &vmap_area_list, list) {
		if (busy->va_start - vmap_start > 0) {
			free = kmem_cache_zalloc(vmap_area_cachep, GFP_NOWAIT);
			if (!WARN_ON_ONCE(!free)) {
				free->va_start = vmap_start;
				free->va_end = busy->va_start;
				insert_vmap_area_augment(free, NULL,
					&free_vmap_area_root,
						&free_vmap_area_list);
			}
		}

		vmap_start = busy->va_end;
	}
//拎出剩余的区域,这块区域是最大的。
	if (vmap_end - vmap_start > 0) {
		free = kmem_cache_zalloc(vmap_area_cachep, GFP_NOWAIT);
		if (!WARN_ON_ONCE(!free)) {
			free->va_start = vmap_start;
			free->va_end = vmap_end;
			insert_vmap_area_augment(free, NULL,
				&free_vmap_area_root,
					&free_vmap_area_list);
		}
	}
}

如上图描述了vmalloc区域,可以分为两块区域,一块支离破碎的,还一块待开发的。如何拎出来未开发的区域呢?看下图,其中的关键点:vmap_start的更新以及差值比较。

名字上是list,其实是一颗红黑树(便于理解画成了链表形式)。vmap_area_list树上挂了很多已被使用的vmalloc区域节点,各个节点间不连续的地方就是hole,然后找出来挂到free_vmap_area_list这颗树上。后面的获取空闲区域就从free树上获取。

进化二:空闲块的更新及查找

5.0:利用已分配区域找空闲区域

先看5.0中的。通过找空洞来获取空间,空洞代表没有使用的区域。

alloc_vmap_area:
        if (free_vmap_cache) {
	} else {
		addr = ALIGN(vstart, align);
		n = vmap_area_root.rb_node;
		first = NULL;

		while (n) {
			struct vmap_area *tmp;
			tmp = rb_entry(n, struct vmap_area, rb_node);
//rbtree中寻找tmp->va_start <= addr <= tmp->va_end的区域
			if (tmp->va_end >= addr) {
				first = tmp;
				if (tmp->va_start <= addr)
					break;
				n = n->rb_left;
			} else
				n = n->rb_right;
		}
	}
//节点使用list成员组成的链表,按地址顺序排列
	while (addr + size > first->va_start && addr + size <= vend) {
		addr = ALIGN(first->va_end, align);
//如果找到的节点是最后一个节点,那就不用找了,直接拿后面没有开发的hole
		if (list_is_last(&first->list, &vmap_area_list))
			goto found;
		first = list_next_entry(first, list);
	}
found:
	va->va_start = addr;
	va->va_end = addr + size;
	va->flags = 0;
	__insert_vmap_area(va);

第一次找到addr最接近的区间位置

while条件满足说明找到的区域不是hole,更新addr和first位置

进入while判断,发现first前有一块空洞适合获取,退出循环,直接拿前一个first的后面一块区间使用,然后加入红黑树和list链表。

5.4:在空闲区域中查找

看5.4的。核心在free_vmap_area树中查找。

find_vmap_lowest_match:
	struct vmap_area *va;
	struct rb_node *node;
	unsigned long length;

//从free树中查找
	node = free_vmap_area_root.rb_node;
	length = size + align - 1;

	while (node) {
		va = rb_entry(node, struct vmap_area, rb_node);
//首先判断左区间最大空闲区域是否满足申请需求,然后判断地址是否在左节点中
		if (get_subtree_max_size(node->rb_left) >= length &&
				vstart < va->va_start) {
			node = node->rb_left;
		} else {
//区间坐落在当前节点中
			if (is_within_this_va(va, size, align, vstart))
				return va;
//右区间最大空闲区域是否满足申请需求
			if (get_subtree_max_size(node->rb_right) >= length) {
				node = node->rb_right;
				continue;
			}

	return NULL;
}
__alloc_vmap_area:
        va = find_vmap_lowest_match(size, align, vstart); //free va中找到合适area
	if (va->va_start > vstart)        //获得待分配的起始地址
		nva_start_addr = ALIGN(va->va_start, align);
	else
		nva_start_addr = ALIGN(vstart, align);
//根据地址判断在当前free va中所处的匹配类型
        type = classify_va_fit_type(va, nva_start_addr, size);
//根据匹配类型,更新free vmap_area中节点的空闲区域
        ret = adjust_va_to_fit_type(va, nva_start_addr, size, type); 

找到合适的地址区间的思想是先判断是否有合适的长度去拿,满足长度后再决定从哪个区间去获取。其中的get_subtree_max_size是由argumemt_tree_propagate_from计算的,记录以某节点为头结点形成的树的最大空闲长度,保存在该节点的subtree_max_size中,这个变量是依据修改在vmap_area中新增的。

核心思想在type和subtree_max_size中。

不变的

想法都是不变的,都是为了提高vmalloc分配器的速度。在5.0之前有一笔patch

patch: commitID    89699605fe7cfd8611900346f61cb6cbf179b10a

mm: vmap area cache

引入了cache减少vmalloc分配器中对红黑树的查找操作。而最新的修改使用了type和subtree_max_size减少对红黑树的操作。可以去看作者描述。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值