Linux内存管理——非连续区内存管理

1.vmalloc原理

随着运行时间增长,物理内存的碎片可能会越来越多,分配连续的物理内存尤其是大尺寸连续的物理内存将越来越费劲;为了尽可能避免这种情况或者在出现这种情况下能够缓解进一步费劲,对于某些不频繁的分配释放的内存申请,可以采用一种方式,即所谓的不连续内存分配。

#define VMALLOC_OFFSET		(8*1024*1024)
#define VMALLOC_START		(((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1))
#define VMALLOC_END		0xff000000UL

非连续内存区的线性地址:

最后看看到底哪些场合使用vmalloc:

  • 1、swap;
  • 2、为module分配空间,见函数module_alloc就明白了;
  • 3、为IO驱动程序分配缓冲区(ioremap);

非连续区的描述符:vm_struct

struct vm_struct {
	struct vm_struct	*next; //指向下一个vm_struct结构的指针
	void			*addr; //内存区内第一个内存单元的线性地址
	unsigned long		size; //内存区的大小加4096(内存区之间的安全间大小)
	unsigned long		flags;//非连续区映射的内存类型
	struct page		**pages; //指向nr-pages数组的指针,指向页描述符的指针组成
	unsigned int		nr_pages; //内存区填充页的个数
	phys_addr_t		phys_addr; //该字段设置为0,除非内存已经被创建来映射一个硬件设备的I/O共享内存
	const void		*caller;
};

get_vm_area()函数在线性地址vmalloc_start和vmalloc_end之间查找一个空闲区域:

struct vm_struct *get_vm_area(unsigned long size, unsigned long flags)
{
	return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END,
				  NUMA_NO_NODE, GFP_KERNEL,
				  __builtin_return_address(0));
}

static struct vm_struct *__get_vm_area_node(unsigned long size,
		unsigned long align, unsigned long flags, unsigned long start,
		unsigned long end, int node, gfp_t gfp_mask, const void *caller)
{
	struct vmap_area *va;
	struct vm_struct *area;

	BUG_ON(in_interrupt());
	if (flags & VM_IOREMAP)
		align = 1ul << clamp_t(int, fls_long(size),
				       PAGE_SHIFT, IOREMAP_MAX_ORDER);

	size = PAGE_ALIGN(size);
	if (unlikely(!size))
		return NULL;

	area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);
	if (unlikely(!area))
		return NULL;

	if (!(flags & VM_NO_GUARD))
		size += PAGE_SIZE;

	va = alloc_vmap_area(size, align, start, end, node, gfp_mask);
	if (IS_ERR(va)) {
		kfree(area);
		return NULL;
	}

	setup_vmalloc_vm(area, va, flags, caller);

	return area;
}

 2.vmalloc模块初始化

vmalloc初始化: start_kernel()->mm_init()->vmalloc_init()

void __init vmalloc_init(void)
{
	struct vmap_area *va;
	struct vm_struct *tmp;
	int i;
	/* 
	 * 遍历每CPU的vmap_block_queue和vfree_deferred变量并进行初始化;
	 * vmap_block_queue是非连续内存块队列管理结构,主要是队列以及对应的保护锁
	 * vfree_deferred是vmalloc的内存延迟释放管理
     */
	for_each_possible_cpu(i) {
		struct vmap_block_queue *vbq;
		struct vfree_deferred *p;

		vbq = &per_cpu(vmap_block_queue, i);
		spin_lock_init(&vbq->lock);
		INIT_LIST_HEAD(&vbq->free);
		p = &per_cpu(vfree_deferred, i);
		init_llist_head(&p->list);
		INIT_WORK(&p->wq, free_work);
	}

	/* Import existing vmlist entries. */
	/* 将挂接在vmlist链表的各项__insert_vmap_area()输入到非连续内存块的管理中 */
	for (tmp = vmlist; tmp; tmp = tmp->next) {
		va = kzalloc(sizeof(struct vmap_area), GFP_NOWAIT);
		va->flags = VM_VM_AREA;
		va->va_start = (unsigned long)tmp->addr;
		va->va_end = va->va_start + tmp->size;
		va->vm = tmp;
		__insert_vmap_area(va);
	}

	vmap_area_pcpu_hole = VMALLOC_END;

	vmap_initialized = true;
}
static void __insert_vmap_area(struct vmap_area *va)
{
	struct rb_node **p = &vmap_area_root.rb_node;/* 获取红黑树根节点指针 */
	struct rb_node *parent = NULL;
	struct rb_node *tmp;
	/* 遍历红黑树,找到va合适的插入点 */
	while (*p) {
		struct vmap_area *tmp_va;

		parent = *p;
		/* 找到parent节点所属的struct vmap_area结构体指针 */
		tmp_va = rb_entry(parent, struct vmap_area, rb_node);
		/* 找插入点,总的原则就是红黑树的左侧地址小 */
		if (va->va_start < tmp_va->va_end)
			p = &(*p)->rb_left;
		else if (va->va_end > tmp_va->va_start)
			p = &(*p)->rb_right;
		else
			BUG();
	}
	/* 找到合适的插入点后,把节点塞入树中;p为最终位置,parent为p的父节点 */
	rb_link_node(&va->rb_node, parent, p);
	/* 调整颜色,翻转后使其变成标准红黑树 */
	rb_insert_color(&va->rb_node, &vmap_area_root);

	/* address-sort this list */
	/* 查找插入的内存块管理树的父节点,有则插入到该节点链表的后面位置,否则作为链表头插入到vmap_area_list链表中 */
	tmp = rb_prev(&va->rb_node);
	if (tmp) {
		struct vmap_area *prev;
		prev = rb_entry(tmp, struct vmap_area, rb_node);
		list_add_rcu(&va->list, &prev->list);
	} else
		/* 作为链表头插入到vmap_area_list链表中 */
		list_add_rcu(&va->list, &vmap_area_list);
}

3.分配非连续内存区

vmalloc()函数给内核分配一个非连续内存区。参数size表示所请求内存区的大小。如果这个函数能够满足请求,就返回新内存区的起始地址;否则,返回一个NULL指针:

void *vmalloc(unsigned long size)
{
	return __vmalloc_node_flags(size, NUMA_NO_NODE,
				    GFP_KERNEL | __GFP_HIGHMEM);
}
EXPORT_SYMBOL(vmalloc);

static inline void *__vmalloc_node_flags(unsigned long size,
					int node, gfp_t flags)
{
	return __vmalloc_node(size, 1, flags, PAGE_KERNEL,
					node, __builtin_return_address(0));
}

static void *__vmalloc_node(unsigned long size, unsigned long align,
			    gfp_t gfp_mask, pgprot_t prot,
			    int node, const void *caller)
{
	return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END,
				gfp_mask, prot, 0, node, caller);
}
void *__vmalloc_node_range(unsigned long size, unsigned long align,
			unsigned long start, unsigned long end, gfp_t gfp_mask,
			pgprot_t prot, unsigned long vm_flags, int node,
			const void *caller)
{
	struct vm_struct *area;
	void *addr;
	unsigned long real_size = size;
	/* 首先对申请内存的大小做对齐后,如果大小为0或者大于总内存,则返回失败 */
	size = PAGE_ALIGN(size);
	if (!size || (size >> PAGE_SHIFT) > totalram_pages)
		goto fail;
	/* 使用kmalloc,在slub中分配vm_struct数据结构体,并返回 */
	area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED |
				vm_flags, start, end, node, gfp_mask, caller);
	if (!area)
		goto fail;
	/* 根据vm_struct的信息进行物理空间申请,建立页表映射 */
	addr = __vmalloc_area_node(area, gfp_mask, prot, node);
	if (!addr)
		return NULL;

	/*
	 * In this function, newly allocated vm_struct has VM_UNINITIALIZED
	 * flag. It means that vm_struct is not fully initialized.
	 * Now, it is fully initialized, so remove this flag here.
	 */
	 /*
	 * 标示内存空间已经初始化
	 */
	clear_vm_uninitialized_flag(area);

	/*
	 * A ref_count = 2 is needed because vm_struct allocated in
	 * __get_vm_area_node() contains a reference to the virtual address of
	 * the vmalloc'ed block.
	 */
	 /*
	 * 调用kmemleak_alloc()进行内存分配泄漏调测
	 */
	kmemleak_alloc(addr, real_size, 2, gfp_mask);

	return addr;

fail:
	warn_alloc_failed(gfp_mask, 0,
			  "vmalloc: allocation failure: %lu bytes\n",
			  real_size);
	return NULL;
}
static struct vm_struct *__get_vm_area_node(unsigned long size,
		unsigned long align, unsigned long flags, unsigned long start,
		unsigned long end, int node, gfp_t gfp_mask, const void *caller)
{
	struct vmap_area *va;
	struct vm_struct *area;

	BUG_ON(in_interrupt());
	if (flags & VM_IOREMAP)
		align = 1ul << clamp_t(int, fls_long(size),
				       PAGE_SHIFT, IOREMAP_MAX_ORDER);

	size = PAGE_ALIGN(size);
	if (unlikely(!size))
		return NULL;
	/*
	 * 核心函数:kmalloc_node()->__kmalloc_node()申请管理结构体内存
	 * 1. 通过kmalloc_slab()寻找合适的slub申请内存
	 * 2. 通过slab_alloc_node()从slub中分配内存
	 */
	area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);
	if (unlikely(!area))
		return NULL;
	/* 如果没有标记,size要多算一个guard page用于分隔  */
	if (!(flags & VM_NO_GUARD))
		size += PAGE_SIZE;
	/* 申请指定的虚拟地址范围内的未映射空白;即申请不连续的物理内存 */
	va = alloc_vmap_area(size, align, start, end, node, gfp_mask);
	if (IS_ERR(va)) {
		kfree(area);
		return NULL;
	}
	/* 将vmap_area结构体中数据同步到vm_struct结构体中  */
	setup_vmalloc_vm(area, va, flags, caller);

	return area;
}
static struct vmap_area *alloc_vmap_area(unsigned long size,
				unsigned long align,
				unsigned long vstart, unsigned long vend,
				int node, gfp_t gfp_mask)
{
	struct vmap_area *va;
	struct rb_node *n;
	unsigned long addr;
	int purged = 0;
	struct vmap_area *first;

	BUG_ON(!size); // 长度为0不行
	BUG_ON(size & ~PAGE_MASK); // 长度不是页面整数倍不行
	BUG_ON(!is_power_of_2(align)); // 长度不是2的n次方不行
	/* 先通过kmalloc申请管理结构体 struct vmap_area 内存,过程类似kzalloc_node(),少了一个内存初始化要求 */
	va = kmalloc_node(sizeof(struct vmap_area),
			gfp_mask & GFP_RECLAIM_MASK, node);
	if (unlikely(!va))
		return ERR_PTR(-ENOMEM);

	/*
	 * Only scan the relevant parts containing pointers to other objects
	 * to avoid false negatives.
	 */
	kmemleak_scan_area(&va->rb_node, SIZE_MAX, gfp_mask & GFP_RECLAIM_MASK);

retry:
	spin_lock(&vmap_area_lock);
	/*
	 * Invalidate cache if we have more permissive parameters.
	 * cached_hole_size notes the largest hole noticed _below_
	 * the vmap_area cached in free_vmap_cache: if size fits
	 * into that hole, we want to scan from vstart to reuse
	 * the hole instead of allocating above free_vmap_cache.
	 * Note that __free_vmap_area may update free_vmap_cache
	 * without updating cached_hole_size or cached_align.
	 */
	 /* 
	 * free_vmap_cache记录着最近释放的或最近注册使用的不连续内存页面空间,是用以加快空间的搜索速度
	 * 1. 在红黑树中根据指定区间寻找合适的区间,找到一个区间正好包含vstart
	 * 2. 在该区间的链表后面寻找合适的空洞进行分配(也有可能在红黑书上找到的就是空洞就不用接着遍历了)
	 * 3. 记录本次分配情况
	 */
	if (!free_vmap_cache ||
			size < cached_hole_size ||
			vstart < cached_vstart ||
			align < cached_align) {
nocache:
		cached_hole_size = 0;
		free_vmap_cache = NULL;
	}
	/* record if we encounter less permissive parameters */
	cached_vstart = vstart;
	cached_align = align;

	/* find starting point for our search */
	/* 寻找符合条件的struct vmap_area区域, 如果上次搜索的节点非空,则从上次搜索的节点开始 */
	if (free_vmap_cache) {
		/* 找到上一次分配节点的struct vmap_area结构体指针 */
		first = rb_entry(free_vmap_cache, struct vmap_area, rb_node);
		/* 上次分配的结束地址向上对齐,得到本次可用的地址 */
		addr = ALIGN(first->va_end, align);
	/* 要找的是: va_begin< vstart <va_end; 缓存的末尾对齐之后超出了范围从头开始搜索 */
		if (addr < vstart)
			goto nocache;
		if (addr + size < addr)
			goto overflow;

	} else {
		addr = ALIGN(vstart, align);
		if (addr + size < addr)
			goto overflow;
		/* 第一次搜索,缓存free_vmap_cache为空,从全局vmap_area_root开始搜索 */
		n = vmap_area_root.rb_node;
		first = NULL;
		/* 要找对齐之后的va_start<=vstart<=va_end的节点 */
		while (n) {
			struct vmap_area *tmp;
			tmp = rb_entry(n, struct vmap_area, rb_node);
			if (tmp->va_end >= addr) {
				first = tmp;
				if (tmp->va_start <= addr)
					break; /* 找到要找的区间 */
				n = n->rb_left;/* 否则,说明该节点还位于较高端,我们移动到树的左节点上,查找更小的区域 */
			} else /* 否则说明该节点代表的区域还比较小,我们需要移动到右节点上,查找更大的区域 */
				n = n->rb_right;
		}

		if (!first)/* 在first为空的情况下,表示vstart为起始的虚拟地址空间未被使用过,将会直接对该虚拟地址空间进行分配 */
			goto found;
	}

	/* from the starting point, walk areas until a suitable hole is found */
	/* 
	 * 若first不为空,意味着该空间曾经分配过,进入while分支进行处理
	 * 该循环是从first为起点遍历vmap_area_list链表管理的虚拟地址空间链表进行查找
	 * 如果找合适的未使用的虚拟地址空间或者遍历到了链表末尾,都表示找到了该空间 
	 */
	while (addr + size > first->va_start && addr + size <= vend) {
		if (addr + cached_hole_size < first->va_start)
			cached_hole_size = first->va_start - addr;
		addr = ALIGN(first->va_end, align);
		if (addr + size < addr)
			goto overflow;
		/* 如果已经是该区间中链表的最后一个节点,则返回该节点 */
		if (list_is_last(&first->list, &vmap_area_list))
			goto found;
		/* 检查链表的下一个节点是否满足 */
		first = list_entry(first->list.next,
				struct vmap_area, list);
	}

found:
	if (addr + size > vend)
		goto overflow;
	/* 运行到这里,说明找到合适的地址区间,初始化KVA数据结构 */
	va->va_start = addr;
	va->va_end = addr + size;
	va->flags = 0;
	__insert_vmap_area(va);/* 插入到红黑书和链表当中 */
	free_vmap_cache = &va->rb_node;/* 保存本次分配到的区间信息 */
	spin_unlock(&vmap_area_lock);

	BUG_ON(va->va_start & (align-1));
	BUG_ON(va->va_start < vstart);
	BUG_ON(va->va_end > vend);

	return va;

overflow:/* 运行到这里,说明地址空间不足 */
	spin_unlock(&vmap_area_lock);
	if (!purged) {/* 如果我们还没有进行KVA地址空间清理(如果幸运的话,我们可以清理出一些可用空间) */
		purge_vmap_area_lazy();
		purged = 1;
		goto retry;
	}
	if (printk_ratelimit())
		pr_warn("vmap allocation for size %lu failed: "
			"use vmalloc=<size> to increase size.\n", size);
	kfree(va);
	return ERR_PTR(-EBUSY);
}
/* 根据vm_struct的信息进行物理空间申请,建立页表映射 */
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
				 pgprot_t prot, int node)
{
	const int order = 0;
	struct page **pages;
	unsigned int nr_pages, array_size, i;
	const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
	const gfp_t alloc_mask = gfp_mask | __GFP_NOWARN;

	nr_pages = get_vm_area_size(area) >> PAGE_SHIFT; /* 计算本次申请的vmalloc共需要多少页内存 */
	array_size = (nr_pages * sizeof(struct page *));

	area->nr_pages = nr_pages;
	/* Please note that the recursion is strictly bounded. */
	if (array_size > PAGE_SIZE) {/* 根据需要的内存页数,申请管理结构体pages内存空间. */
		pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
				PAGE_KERNEL, node, area->caller);/* 如果需要多余1页内存,则通过vmalloc申请. */
		area->flags |= VM_VPAGES;
	} else {/* 否则从slub中申请. */
		pages = kmalloc_node(array_size, nested_gfp, node);
	}
	area->pages = pages;
	if (!area->pages) {
		remove_vm_area(area->addr);
		kfree(area);
		return NULL;
	}

	for (i = 0; i < area->nr_pages; i++) {
		struct page *page;
		/* 从伙伴系统中申请内存. */
		if (node == NUMA_NO_NODE)
			page = alloc_page(alloc_mask);
		else
			page = alloc_pages_node(node, alloc_mask, order);

		if (unlikely(!page)) {
			/* Successfully allocated i pages, free them in __vunmap() */
			area->nr_pages = i;
			goto fail;
		}
		area->pages[i] = page;
		if (gfp_mask & __GFP_WAIT)
			cond_resched();
	}
	/* 
	 * 物理内存申请成功后,进行内存映射.
	 * vmap_page_range_noflush: 建立映射;
	 * set_pte_at(&init_mm, addr, pte, mk_pte(page, prot)): 将页描述符对应的页框和页表项进行关联,映射关系被建立
	 * flush_cache_vmap:刷新缓存;
	 */
	if (map_vm_area(area, prot, pages))
		goto fail;
	return area->addr;

fail:
	warn_alloc_failed(gfp_mask, order,
			  "vmalloc: allocation failure, allocated %ld of %ld bytes\n",
			  (area->nr_pages*PAGE_SIZE), area->size);
	vfree(area->addr);
	return NULL;
}

4.释放非连续内存区

vfree()函数释放 vmalloc()或vmalloc_32()创建的非连续内存区,而 vunmap()函数释放vmap()创建的内存区。两个函数都使用同一个参数—将要释放的内存区的起始线性地址 address;它们都依赖于 unmap()函数来做实质性的工作。

void vfree(const void *addr)
{
	BUG_ON(in_nmi());

	kmemleak_free(addr);

	if (!addr)
		return;
	if (unlikely(in_interrupt())) {
		/* 
	 	* 如果当前释放操作在中断中,则将释放的内存空间信息加入到当前CPU的vfree_deferred管理链表中
	 	* 继而通过schedule_work()唤醒工作队列,对内存进行一步释放操作
		*/
		struct vfree_deferred *p = this_cpu_ptr(&vfree_deferred);
		if (llist_add((struct llist_node *)addr, &p->list))
			schedule_work(&p->wq);
	} else
		/* 如果不在中断中,则直接释放 */
		__vunmap(addr, 1);
}
static void __vunmap(const void *addr, int deallocate_pages)
{
	struct vm_struct *area;

	if (!addr)
		return;

	if (WARN(!PAGE_ALIGNED(addr), "Trying to vfree() bad address (%p)\n",
			addr))
		return;
	/* 
	 * remove_vm_area()->find_vmap_area()->__find_vmap_area()通过addr在红黑树中找到对应节点对应的struct vmap_area结构体 
	 * 再通过free_unmap_vmap_area()清除指定的虚拟地址
	 */
	area = remove_vm_area(addr);
	if (unlikely(!area)) {
		WARN(1, KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
				addr);
		return;
	}

	debug_check_no_locks_freed(addr, area->size);
	debug_check_no_obj_freed(addr, area->size);

	if (deallocate_pages) {
		int i;

		for (i = 0; i < area->nr_pages; i++) {
			struct page *page = area->pages[i];

			BUG_ON(!page);
			__free_page(page);
		}

		if (area->flags & VM_VPAGES)
			vfree(area->pages);
		else
			kfree(area->pages);
	}

	kfree(area);
	return;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小吴伴学者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值