AI智能摘要
内核启动的内存初始化分为Fixmap、线性映射、Buddy分配器和Slab分配器四大阶段,逐步实现高效的物理与虚拟内存管理。vmalloc通过vmap机制,将非连续物理内存映射为连续虚拟地址,极大提升内存分配灵活性。文中详解vmalloc区域管理与关键数据结构,介绍空闲及已分配管理策略,并列举常用分配释放API,并解释代码流程
此摘要由AI分析文章内容生成,仅供参考。

前言

我们先总结一下之前的内核启动早期的内存的流程:

第一个阶段:Fixmap 固定映射阶段

setup_arch() → early_fixmap_init()

  • 建立 fixmap 区域

    • 通过 fixmap 可以动态建立更多页表项

    • 此时内存管理: memblock(物理内存位图管理)

第二阶段:线性映射阶段

paging_init() → map_memory()

  • 将所有物理内存线性映射到内核虚拟地址空间

    • 映射关系: virt_addr = phys_addr + PAGE_OFFSET

    • 此后,内核就可以通过虚拟地址访问所有的物理内存

第三阶段:Buddy 分配器初始化

mem_init() → free_area_init()

  • 遍历 memblock 获取所有空闲物理内存信息

  • 建立 free_area[] 数据结构

  • 将可用页面加入 buddy 空闲链表

  • 输出: 页面级分配器 ready,可以 alloc_pages()/free_pages()

第四阶段:Slab 分配器初始化

kmem_cache_init

  • 静态变量: boot_kmem_cache, boot_kmem_cache_node

  • 用 buddy 分配页面给第一个 slab

  • 创建核心缓存:建立 kmem_cachekmem_cache_node 的对象缓存

  • 构建缓存体系:创建完整的 kmalloc 缓存族

  • 输出:对象级缓存分配器 ready,可以kmalloc

各层职责:

  • Slab:管理小对象缓存,快速分配/释放

  • Buddy:管理物理页面,按 2^order 分配连续页面

  • 线性映射:提供物理到虚拟地址的直接转换

  • Fixmap:保留用于特殊场景的动态映射

不管是buddy还是slab,我们分配的内存都是物理上连续的。当内存碎片后,连续物理内存的分配就变得尤为的苦难。此时我们就需vmap机制,将不连续的物理内存页框映射到连续的虚拟地址空间中,vmalloc 的分配就是基于这个机制实现的。

此时我们的内存初始化流程已经来到了 vmalloc_init

vmalloc的初始化

void __init vmalloc_init(void)
{
	struct vmap_area *va;
	struct vm_struct *tmp;
	int i;

	/*
	 * Create the cache for vmap_area objects.
	 */
    // 创建vmap_area的kmem cache
	vmap_area_cachep = KMEM_CACHE(vmap_area, SLAB_PANIC);

	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. */
	for (tmp = vmlist; tmp; tmp = tmp->next) {
		va = kmem_cache_zalloc(vmap_area_cachep, GFP_NOWAIT);
		if (WARN_ON_ONCE(!va))
			continue;

		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);
	}

	/*
	 * Now we can initialize a free vmap space.
	 */
	vmap_init_free_space();
	vmap_initialized = true;
}

这段代码我们需要了解的点如下:

  1. vmap_area的slab cache的创建,后续所有的vmalloc所需要申请的vmap_area,均从此slab cache中申请

  2. 插入已存在的vma,这些vma是在map_kernel阶段被创建出来放在了vmlist

vmap_init_free_space 函数是用来初始化空闲的vmalloc区域

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链表,每个元素都是已分配的vmap_area(busy表示忙碌的/已分配的)
	list_for_each_entry(busy, &vmap_area_list, list) {
        // 检查当前vmap_start(上一个区域的结束)到当前区域开始之间是否有空隙,如果大于0就说明存在空隙
		if (busy->va_start - vmap_start > 0) {
            // 为这段空闲的区域申请slab内存块,也就对应上图中的F区域
			free = kmem_cache_zalloc(vmap_area_cachep, GFP_NOWAIT);
			if (!WARN_ON_ONCE(!free)) {
                // 封装free的vma
				free->va_start = vmap_start;
				free->va_end = busy->va_start;
                // 将这个free的vma插入到全局的free_vmap_area_root红黑树以及free_vmap_area_list列表中
				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);
		}
	}
}

已分配区域管理vmap_area_root/vmap_area_list):

  • 用于快速查找某个地址是否已分配

  • 用于释放时快速定位

  • 按地址排序,便于查找相邻区域

注意:LRDP2/crash/trace32能够解析出所有的vmalloc的信息,就是通过这个vmap_area_list

空闲区域管理free_vmap_area_root/free_vmap_area_list):

  • 用于快速找到合适大小的空闲区域

  • 支持多种查找策略(最佳匹配、首次匹配等)

  • 维护最大空闲块信息,快速判断是否有足够空间

vma相关的数据结构

vmap_area/vm_struct

struct vm_struct {
	struct vm_struct	*next;
	void			*addr;
	unsigned long		size;
	unsigned long		flags;
	struct page		**pages;
#ifdef CONFIG_HAVE_ARCH_HUGE_VMALLOC
	unsigned int		page_order;
#endif
	unsigned int		nr_pages;
	phys_addr_t		phys_addr;
	const void		*caller;
};

struct vmap_area {
	unsigned long va_start;
	unsigned long va_end;

	struct rb_node rb_node;         /* address sorted rbtree */
	struct list_head list;          /* address sorted list */

	/*
	 * The following two variables can be packed, because
	 * a vmap_area object can be either:
	 *    1) in "free" tree (root is free_vmap_area_root)
	 *    2) or "busy" tree (root is vmap_area_root)
	 */
	union {
		unsigned long subtree_max_size; /* in "free" tree */
		struct vm_struct *vm;           /* in "busy" tree */
	};
};

struct vmap_area用于描述一段虚拟地址的区域,从结构体中va_start/va_end也能看出来。同时该结构体会通过rb_node挂在红黑树上,通过list挂在链表上。

struct vmap_areavm字段是struct vm_struct结构,用于管理虚拟地址和物理页之间的映射关系,可以将struct vm_struct构成一个链表,维护多段映射。

vmalloc的内存分配API

常用API

API接口

底层函数

备注

vmalloc

__vmalloc_node_range

gfp_mask 为 GFP_KERNEL

prot 为 PAGE_KERNEL

vzalloc

__vmalloc_node_range

在 vmalloc 的基础上多了 __GFP_ZERO 的 gfp_mask 限制,即最终内存都会用 0 填充

__vmalloc

__vmalloc_node_range

手动指定gfp_mask和prot

vmalloc_node

__vmalloc_node_range

和vmalloc一致

void *__vmalloc_node(unsigned long size, unsigned long align,
			    gfp_t gfp_mask, int node, const void *caller)
{
	return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END,
				gfp_mask, PAGE_KERNEL, 0, node, caller);
}

最终调用到的函数为 __vmalloc_node_range(),其中 start 为 VMALLOC_START ,end 为 VMALLOC_END,这也是限制了后续的分配的vmalloc申请的内存是位于vmalloc区域

下面将重点分析 __vmalloc_node_range() 函数

vmalloc

都是通过__vmalloc_node_range分配

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 *ret;
	kasan_vmalloc_flags_t kasan_flags = KASAN_VMALLOC_NONE;
	unsigned long real_size = size;
	unsigned long real_align = align;
	unsigned int shift = PAGE_SHIFT;
    // size 为 0 则警告并返回 NULL
	if (WARN_ON_ONCE(!size))
		return NULL;
    // 若请求页数 > 系统总页数(totalram_pages())则报错并返回 NULL(防止超大分配)
	if ((size >> PAGE_SHIFT) > totalram_pages()) {
		warn_alloc(gfp_mask, NULL,
			"vmalloc error: size %lu, exceeds total pages",
			real_size);
		return NULL;
	}
    // 当 vmap_allow_huge 且 VM_ALLOW_HUGE_VMAP 标志允许时,尝试使用大页(PMD/更大)
	if (vmap_allow_huge && (vm_flags & VM_ALLOW_HUGE_VMAP)) {
		unsigned long size_per_node;

		/*
		 * Try huge pages. Only try for PAGE_KERNEL allocations,
		 * others like modules don't yet expect huge pages in
		 * their allocations due to apply_to_page_range not
		 * supporting them.
		 */
        // 计算每节点大小 size_per_node(若不限节点则除以在线节点数),并根据架构能力选择 shift(页粒度:PAGE_SHIFT、PMD_SHIFT 等)。
		size_per_node = size;
		if (node == NUMA_NO_NODE)
			size_per_node /= num_online_nodes();
		if (arch_vmap_pmd_supported(prot) && size_per_node >= PMD_SIZE)
			shift = PMD_SHIFT;
		else
			shift = arch_vmap_pte_supported_shift(size_per_node);
        // 调整 align 与 size 为大页边界对齐
		align = max(real_align, 1UL << shift);
		size = ALIGN(real_size, 1UL << shift);
	}

again:
    // 获取 vm_struct(地址空间元数据)
	area = __get_vm_area_node(real_size, align, shift, VM_ALLOC |
				  VM_UNINITIALIZED | vm_flags, start, end, node,
				  gfp_mask, caller);
	if (!area) {
		bool nofail = gfp_mask & __GFP_NOFAIL;
		warn_alloc(gfp_mask, NULL,
			"vmalloc error: size %lu, vm_struct allocation failed%s",
			real_size, (nofail) ? ". Retrying." : "");
		if (nofail) {
			schedule_timeout_uninterruptible(1);
			goto again;
		}
		goto fail;
	}

	/*
	 * Prepare arguments for __vmalloc_area_node() and
	 * kasan_unpoison_vmalloc().
	 */
	if (pgprot_val(prot) == pgprot_val(PAGE_KERNEL)) {
		if (kasan_hw_tags_enabled()) {
			/*
			 * Modify protection bits to allow tagging.
			 * This must be done before mapping.
			 */
			prot = arch_vmap_pgprot_tagged(prot);

			/*
			 * Skip page_alloc poisoning and zeroing for physical
			 * pages backing VM_ALLOC mapping. Memory is instead
			 * poisoned and zeroed by kasan_unpoison_vmalloc().
			 */
			gfp_mask |= __GFP_SKIP_KASAN_UNPOISON | __GFP_SKIP_ZERO;
		}

		/* Take note that the mapping is PAGE_KERNEL. */
		kasan_flags |= KASAN_VMALLOC_PROT_NORMAL;
	}

	/* Allocate physical pages and map them into vmalloc space. */
    // 分配物理页并映射
	ret = __vmalloc_area_node(area, gfp_mask, prot, shift, node);
	if (!ret)
		goto fail;

	/*
	 * Mark the pages as accessible, now that they are mapped.
	 * The condition for setting KASAN_VMALLOC_INIT should complement the
	 * one in post_alloc_hook() with regards to the __GFP_SKIP_ZERO check
	 * to make sure that memory is initialized under the same conditions.
	 * Tag-based KASAN modes only assign tags to normal non-executable
	 * allocations, see __kasan_unpoison_vmalloc().
	 */
	kasan_flags |= KASAN_VMALLOC_VM_ALLOC;
	if (!want_init_on_free() && want_init_on_alloc(gfp_mask) &&
	    (gfp_mask & __GFP_SKIP_ZERO))
		kasan_flags |= KASAN_VMALLOC_INIT;
	/* KASAN_VMALLOC_PROT_NORMAL already set if required. */
	area->addr = kasan_unpoison_vmalloc(area->addr, real_size, kasan_flags);

	/*
	 * 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.
	 */
    // 移除 VM_UNINITIALIZED 标志,表明 vm_struct 现在已完全初始化
	clear_vm_uninitialized_flag(area);

	size = PAGE_ALIGN(size);
	if (!(vm_flags & VM_DEFER_KMEMLEAK))
		kmemleak_vmalloc(area, size, gfp_mask);

	return area->addr;

fail:
	if (shift > PAGE_SHIFT) {
		shift = PAGE_SHIFT;
		align = real_align;
		size = real_size;
		goto again;
	}

	return NULL;
}

函数目的__vmalloc_node_range 在指定 NUMA 节点或地址范围内为 vmalloc 分配虚拟地址并映射物理页,返回分配后的虚拟地址(或 NULL)。

参数简要

  • size/align:请求的字节大小与对齐。

  • start/end:可分配的虚拟地址范围。

  • gfp_mask:页分配标志(如 __GFP_NOFAIL)。

  • prot:页保护/权限(例如 PAGE_KERNEL)。

  • vm_flags:vmalloc 相关标志(如允许 huge vmap)。

  • node:NUMA 节点或 NUMA_NO_NODE

  • caller:调用方地址(用于调试/跟踪)。

主要函数:

  • __get_vm_area_node : 获取 vm_struct

  • __vmalloc_area_node : 分配物理页并映射

__get_vm_area_node

static struct vm_struct *__get_vm_area_node(unsigned long size,
		unsigned long align, unsigned long shift, 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;
	unsigned long requested_size = size;

    // 不能在中断上下文调用
	BUG_ON(in_interrupt());
	size = ALIGN(size, 1ul << shift);
	if (unlikely(!size))
		return NULL;

	if (flags & VM_IOREMAP)
		align = 1ul << clamp_t(int, get_count_order_long(size),
				       PAGE_SHIFT, IOREMAP_MAX_ORDER);
    // 分配并清零 vm_struct
	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);

	/*
	 * Mark pages for non-VM_ALLOC mappings as accessible. Do it now as a
	 * best-effort approach, as they can be mapped outside of vmalloc code.
	 * For VM_ALLOC mappings, the pages are marked as accessible after
	 * getting mapped in __vmalloc_node_range().
	 * With hardware tag-based KASAN, marking is skipped for
	 * non-VM_ALLOC mappings, see __kasan_unpoison_vmalloc().
	 */
	if (!(flags & VM_ALLOC))
		area->addr = kasan_unpoison_vmalloc(area->addr, requested_size,
						    KASAN_VMALLOC_PROT_NORMAL);

	return area;
}

目的:分配并初始化一个 vm_struct(虚拟地址区元数据),为后续 vmalloc/vmap 映射预留虚拟区间并返回 vm_struct 指针。

参数

  • size/align/shift:请求大小、对齐与页粒度(shift 表示尝试的大页级别)。

  • flags:控制行为(例如 VM_IOREMAPVM_NO_GUARDVM_ALLOC)。

  • node/gfp_mask:NUMA 节点与分配标志。

  • caller:调用者地址用于调试/追踪。

分配vm_struct
area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);

vmalloc分配虚拟内存时,需要先找到一个虚拟地址区域,并用一个vm_struct结构体来描述它。这个结构体通过kmalloc_node分配

找到vmap_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;
	unsigned long freed;
	unsigned long addr;
	int purged = 0;
	int ret;

	BUG_ON(!size);
	BUG_ON(offset_in_page(size));
	BUG_ON(!is_power_of_2(align));

	if (unlikely(!vmap_initialized))
		return ERR_PTR(-EBUSY);

	might_sleep();
	gfp_mask = gfp_mask & GFP_RECLAIM_MASK;
    // 从vmap_area_cachep的slab缓存中分配一个vmap_area
	va = kmem_cache_alloc_node(vmap_area_cachep, gfp_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);

retry:
	preload_this_cpu_lock(&free_vmap_area_lock, gfp_mask, node);
    // 从free_vmap_area_root这条红黑树上找到符合要求的虚拟地址空间,返回起始地址
	addr = __alloc_vmap_area(&free_vmap_area_root, &free_vmap_area_list,
		size, align, vstart, vend);
	spin_unlock(&free_vmap_area_lock);

	/*
	 * If an allocation fails, the "vend" address is
	 * returned. Therefore trigger the overflow path.
	 */
	if (unlikely(addr == vend))
		goto overflow;
    
    // 填充vmap_area的成员
	va->va_start = addr;
	va->va_end = addr + size;
	va->vm = NULL;

	spin_lock(&vmap_area_lock);
    // 将这个vmap area 从free红黑树和free列表中去除,加入到已分配红黑树和列表中
	insert_vmap_area(va, &vmap_area_root, &vmap_area_list);
	spin_unlock(&vmap_area_lock);

	BUG_ON(!IS_ALIGNED(va->va_start, align));
	BUG_ON(va->va_start < vstart);
	BUG_ON(va->va_end > vend);

	ret = kasan_populate_vmalloc(addr, size);
	if (ret) {
		free_vmap_area(va);
		return ERR_PTR(ret);
	}

	return va;

overflow:
	if (!purged) {
		purge_vmap_area_lazy();
		purged = 1;
		goto retry;
	}

	freed = 0;
	blocking_notifier_call_chain(&vmap_notify_list, 0, &freed);

	if (freed > 0) {
		purged = 0;
		goto retry;
	}

	if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit())
		pr_warn("vmap allocation for size %lu failed: use vmalloc=<size> to increase size\n",
			size);

	kmem_cache_free(vmap_area_cachep, va);
	return ERR_PTR(-EBUSY);
}

alloc_vmap_area 它先通过vmap_area_root二叉树来查找第一个区域first vm_area,然后根据这个first vm_area去查找vmap_area_list链表中满足大小的空间区域。这个函数不会展开继续讲解,具体可以自行查看。我们需要知道的就是这个函数的目的就是为了free_vmap_area_root这条红黑树上找到符合size的虚拟地址空间,并返回符合要求vmap_area

__vmalloc_area_node

static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
				 pgprot_t prot, unsigned int page_shift,
				 int node)
{
	const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
	bool nofail = gfp_mask & __GFP_NOFAIL;
	unsigned long addr = (unsigned long)area->addr;
	unsigned long size = get_vm_area_size(area);
	unsigned long array_size;
	unsigned int nr_small_pages = size >> PAGE_SHIFT;
	unsigned int page_order;
	unsigned int flags;
	int ret;

	array_size = (unsigned long)nr_small_pages * sizeof(struct page *);
	gfp_mask |= __GFP_NOWARN;
	if (!(gfp_mask & (GFP_DMA | GFP_DMA32)))
		gfp_mask |= __GFP_HIGHMEM;

	/* Please note that the recursion is strictly bounded. */
    // 分配pages数组空间
	if (array_size > PAGE_SIZE) {
		area->pages = __vmalloc_node(array_size, 1, nested_gfp, node,
					area->caller);
	} else {
		area->pages = kmalloc_node(array_size, nested_gfp, node);
	}

	if (!area->pages) {
		warn_alloc(gfp_mask, NULL,
			"vmalloc error: size %lu, failed to allocated page array size %lu",
			nr_small_pages * PAGE_SIZE, array_size);
		free_vm_area(area);
		return NULL;
	}

	set_vm_area_page_order(area, page_shift - PAGE_SHIFT);
	page_order = vm_area_page_order(area);
    // 为pages数组进行页分配
	area->nr_pages = vm_area_alloc_pages(gfp_mask | __GFP_NOWARN,
		node, page_order, nr_small_pages, area->pages);

	atomic_long_add(area->nr_pages, &nr_vmalloc_pages);
	if (gfp_mask & __GFP_ACCOUNT) {
		int i;

		for (i = 0; i < area->nr_pages; i++)
			mod_memcg_page_state(area->pages[i], MEMCG_VMALLOC, 1);
	}

	/*
	 * If not enough pages were obtained to accomplish an
	 * allocation request, free them via __vfree() if any.
	 */
	if (area->nr_pages != nr_small_pages) {
		/* vm_area_alloc_pages() can also fail due to a fatal signal */
		if (!fatal_signal_pending(current))
			warn_alloc(gfp_mask, NULL,
				"vmalloc error: size %lu, page order %u, failed to allocate pages",
				area->nr_pages * PAGE_SIZE, page_order);
		goto fail;
	}

	/*
	 * page tables allocations ignore external gfp mask, enforce it
	 * by the scope API
	 */
	if ((gfp_mask & (__GFP_FS | __GFP_IO)) == __GFP_IO)
		flags = memalloc_nofs_save();
	else if ((gfp_mask & (__GFP_FS | __GFP_IO)) == 0)
		flags = memalloc_noio_save();

	do {
		ret = vmap_pages_range(addr, addr + size, prot, area->pages,
			page_shift);
		if (nofail && (ret < 0))
			schedule_timeout_uninterruptible(1);
	} while (nofail && (ret < 0));

	if ((gfp_mask & (__GFP_FS | __GFP_IO)) == __GFP_IO)
		memalloc_nofs_restore(flags);
	else if ((gfp_mask & (__GFP_FS | __GFP_IO)) == 0)
		memalloc_noio_restore(flags);

	if (ret < 0) {
		warn_alloc(gfp_mask, NULL,
			"vmalloc error: size %lu, failed to map pages",
			area->nr_pages * PAGE_SIZE);
		goto fail;
	}

	return area->addr;

fail:
	__vfree(area->addr);
	return NULL;
}

目的:为已预留的 vm_struct::addr 区域分配物理 page 并建立内核虚拟地址映射

// pages 数组:存储指向每个物理页面的 struct page* 指针
struct page **pages;
unsigned int nr_small_pages = size >> PAGE_SHIFT;  // 计算需要多少页面
unsigned long array_size = nr_small_pages * sizeof(struct page *);

// 根据数组大小选择不同的分配方式
if (array_size > PAGE_SIZE) {
    area->pages = __vmalloc_node(array_size, 1, nested_gfp, node, area->caller);
} else {
    area->pages = kmalloc_node(array_size, nested_gfp, node);
}

vmalloc 分配大块内存时:

  • 需要分配多个物理页面(可能不连续)

  • 需要记录每个物理页面对应的 struct page*

  • pages 数组就是用来保存这些指针的

示例:分配 1MB 内存(假设 PAGE_SIZE=4KB)

需要页面数:1MB / 4KB = 256 个页面
pages 数组大小:256 * 8(64位系统指针大小)= 2048 字节 = 2KB
大pages数组分配策略

为什么这样做?

  1. 避免大块连续物理内存的压力

    • pages 数组本身可能很大(比如分配 1GB 内存,pages 数组需要 2MB)

    • kmalloc 需要物理连续的 2MB 内存,可能失败

    • vmalloc 只需要虚拟连续,更容易成功

  2. 递归但有限

    • 注释说 "recursion is strictly bounded"(递归是严格有界的)

    • 因为 pages 数组用于管理用户内存,而用户内存比 pages 数组本身大得多

    • 不会无限递归:pages 数组的大小远小于它管理的实际内存大小

// 分配 1GB 内存的递归情况:
vmalloc(1GB)
├── 分配 pages 数组(2MB)
│   └── __vmalloc_node(2MB)  // 第一次递归
│       ├── 分配 pages 数组(4KB)// 第二次递归
│       │   └── kmalloc(4KB)  // 停止递归
│       ├── 分配 512 个物理页面(用于存储2MB的pages数组)
│       └── 建立映射
├── 分配 262,144 个物理页面(用于1GB用户内存)
小pages数组分配策略

为什么这样做?

  1. 小数组更高效

    • 小数组用 kmalloc 分配更快,开销更小

    • 不需要建立复杂的页表映射

  2. 减少碎片

    • 小内存分配用 slab 分配器更合适

    • vmalloc 会有额外的元数据开销

页分配
	area->nr_pages = vm_area_alloc_pages(gfp_mask | __GFP_NOWARN,
		node, page_order, nr_small_pages, area->pages);

这个函数的目的是:批量分配物理页面,尽可能分配连续的物理内存块

vm_area_alloc_pages(要分配16个页面)
├── 尝试分配16个连续页面 (order=4, 2^4=16)
│   └── 失败(内存碎片化)
├── 尝试分配8个连续页面 (order=3)
│   ├── 成功!分配第一个8页块 → 填充pages[0..7]
│   └── 剩余8个页面
├── 尝试分配4个连续页面 (order=2)
│   ├── 成功!分配两个4页块 → 填充pages[8..11], [12..15]
│   └── 完成!
└── 如果都失败,最终使用16次单页分配

这里不展开讲述这个算法的实现,有兴趣的自行探索

这里以一个案例说明算法的实现:

// 调用参数
nr_small_pages = 2048;  // 8MB / 4KB
page_order = 9;         // 尝试分配 2^9 = 512 个连续页面(2MB)

// vm_area_alloc_pages 内部可能:
// 1. 尝试 order=9(2MB连续块)
//    - 成功:分配4个2MB块,填充 pages[0..2047]
//    - 完成!只用了4次分配
// 2. 如果 order=9 失败,尝试 order=8(1MB块)
//    - 成功:分配8个1MB块,填充所有页面
// 3. 以此类推,最差情况2048次单页分配
页映射

我们现在已经申请到了对应的物理空间,存在了vm_struct的pages数组内,同时虚拟地址空间的地址也得到了,也就是vm_area的va_start和va_end。那么就到了两者映射的过程了

	do {
		ret = vmap_pages_range(addr, addr + size, prot, area->pages,
			page_shift);
		if (nofail && (ret < 0))
			schedule_timeout_uninterruptible(1);
	} while (nofail && (ret < 0));

vmap_pages_range 最终会调__vmap_pages_range_noflush 函数,遍历所有的页,一页一页的进行映射

int __vmap_pages_range_noflush(unsigned long addr, unsigned long end,
		pgprot_t prot, struct page **pages, unsigned int page_shift)
{
    // 计算虚拟地址的页数
	unsigned int i, nr = (end - addr) >> PAGE_SHIFT;

	WARN_ON(page_shift < PAGE_SHIFT);

	if (!IS_ENABLED(CONFIG_HAVE_ARCH_HUGE_VMALLOC) ||
			page_shift == PAGE_SHIFT)
		return vmap_small_pages_range_noflush(addr, end, prot, pages);
    // 遍历所有的页
	for (i = 0; i < nr; i += 1U << (page_shift - PAGE_SHIFT)) {
		int err;
        // 单页进行映射
		err = vmap_range_noflush(addr, addr + (1UL << page_shift),
					page_to_phys(pages[i]), prot,
					page_shift);
		if (err)
			return err;

		addr += 1UL << page_shift;
	}

	return 0;
}

页进行映射的过程不细看了,其实就是PGD/P4D/PUD/PMD/PTE的层级映射

vmap_range_noflush()  // 建立映射但不刷新TLB
├── vmap_p4d_range()
│   ├── vmap_pud_range()
│   │   ├── vmap_pmd_range()
│   │   │   ├── vmap_pte_range()  // 实际设置PTE
│   │   │   └── vmap_huge_pmd()   // 尝试大页映射
│   │   └── vmap_huge_pud()       // 尝试超大页映射
│   └── vmap_huge_p4d()           // 尝试更大的页映射
  • 有vm_struct 区域的起始地址 start 通过 pgd_offset_k() 可以找到 PGD页表项,然后遍历 PGD页表。

  • vmap_p4d_range() 函数中,由 p4d_alloc() 函数找到 P4D页表项,然后遍历 P4D;

  • vmap_pud_range() 函数中,由pud_alloc() 函数找到 PUD 页表项,然后遍历PUD;

  • vmap_pmd_range() 函数中,由 pmc_alloc() 函数知道哦啊哦PMD 页表项,然后遍历PMD;

  • vmap_pte_range() 函数中,由pte_alloc_kernel() 宏函数找到 PTE页表项,然后根据 area->pages 保存的每个物理页面来创建页表项。mk_pte() 宏利用刚分配的页面和页面属性prot 来生成一个页表项。最后通过 set_pte_at() 函数设置到实际的 PTE 中。

vmalloc分配内存流程图

vmap

vmap函数,完成的工作是,在vmalloc虚拟地址空间中找到一个空闲区域,然后将page页面数组对应的物理内存映射到该区域,最终返回映射的虚拟起始地址。

vmap() vmalloc() 的操作,大部分的逻辑是一样的,例如从 VMALLOC_START ~ VMALLOC_END 区域查找并分配 vmap_area,例如对虚拟地址和物理页框进行映射管理的建立。 不同之处 ,在于 vmap() 的参数是直接将 pages传入,而 vmalloc() 需要通过 alloc_page()向 buddy 申请。

void *vmap(struct page **pages, unsigned int count,
	   unsigned long flags, pgprot_t prot)
{
	struct vm_struct *area;
	unsigned long addr;
	unsigned long size;		/* In bytes */

    // 函数会 sleep,因此不得在原子或中断上下文使用
	might_sleep();

	/*
	 * Your top guard is someone else's bottom guard. Not having a top
	 * guard compromises someone else's mappings too.
	 */
	if (WARN_ON_ONCE(flags & VM_NO_GUARD))
		flags &= ~VM_NO_GUARD;

	if (count > totalram_pages())
		return NULL;

	size = (unsigned long)count << PAGE_SHIFT;
    // 最终调用__get_vm_area_node分配vm_struct
	area = get_vm_area_caller(size, flags, __builtin_return_address(0));
	if (!area)
		return NULL;

	addr = (unsigned long)area->addr;
    // 建立映射
	if (vmap_pages_range(addr, addr + size, pgprot_nx(prot),
				pages, PAGE_SHIFT) < 0) {
		vunmap(area->addr);
		return NULL;
	}

	if (flags & VM_MAP_PUT_PAGES) {
		area->pages = pages;
		area->nr_pages = count;
	}
	return area->addr;
}
EXPORT_SYMBOL(vmap);

经历了 vmalloc() 的分配流程,我们看到 vmap() 会将 page 指针数组通过参数的形式直接传入。

  • pages: 页面指针数组,同 struct vm_struct 中的 pages 成员;

  • count: 需要映射的页面数量,同 struct vm_struct 中的 nr_pages 成员;

  • flags: 同 struct vm_struct 中的 flags 成员;

  • prot: 为映射进行页面保护,同 __vmalloc() 中的 prot;

vmalloc的内存释放API

vfree

void vfree(const void *addr)
{
    // 绝对禁止在不可屏蔽中断(NMI)中调用 vfree
	BUG_ON(in_nmi());

	kmemleak_free(addr);
    // 如果不是在中断上下文中,就可能会睡眠
	might_sleep_if(!in_interrupt());

	if (!addr)
		return;

	__vfree(addr);
}

static void __vfree(const void *addr)
{
    if (unlikely(in_interrupt()))
        __vfree_deferred(addr);  // 中断上下文:延迟释放
    else
        __vunmap(addr, 1);       // 进程上下文:立即释放
}

从这里我们可以看出vfree 函数是可以在中断上下文中使用的,这种情况下调__vfree_deferred 进行释放,而在进程上下文调__vunmap 进行释放。

__vfree_deferred

struct vfree_deferred {
	struct llist_head list;
	struct work_struct wq;
};
static DEFINE_PER_CPU(struct vfree_deferred, vfree_deferred);

static inline void __vfree_deferred(const void *addr)
{
	/*
	 * Use raw_cpu_ptr() because this can be called from preemptible
	 * context. Preemption is absolutely fine here, because the llist_add()
	 * implementation is lockless, so it works even if we are adding to
	 * another cpu's list. schedule_work() should be fine with this too.
	 */
	struct vfree_deferred *p = raw_cpu_ptr(&vfree_deferred);

	if (llist_add((struct llist_node *)addr, &p->list))
		schedule_work(&p->wq);
}

通过 per-CPU 无锁链表避免锁争用,并利用 workqueue 机制在中断上下文中实现安全延迟释放,确保内核稳定性。

在进程上下文使__vunmap ,这个流程和我们另一个释放函vunmap 流程基本一致,所以vunmap 的章节叙述。

vunmap

void vunmap(const void *addr)
{
	BUG_ON(in_interrupt());
	might_sleep();
	if (addr)
		__vunmap(addr, 0);
}

vunmap执行的是跟vmap相反的过程:从vmap_area_root/vmap_area_list中查找vmap_area区域,取消页表映射,再从vmap_area_root/vmap_area_list中删除掉vmap_area,页面返还给伙伴系统等。由于映射关系有改动,因此还需要进行TLB的刷新,频繁的TLB刷新会降低性能,因此将其延迟进行处理,因此称为lazy tlb

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;
    // 从vmap_area_root红黑树中查到vmap_area,从而获取vm_struct
	area = find_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(area->addr, get_vm_area_size(area));
	debug_check_no_obj_freed(area->addr, get_vm_area_size(area));

	kasan_poison_vmalloc(area->addr, get_vm_area_size(area));
    // 取消内存映射关系
	vm_remove_mappings(area, deallocate_pages);

    // 对于 vfree,需要将vmalloc时申请的内存释放掉
	if (deallocate_pages) {
		int i;

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

			BUG_ON(!page);
			mod_memcg_page_state(page, MEMCG_VMALLOC, -1);
			/*
			 * High-order allocs for huge vmallocs are split, so
			 * can be freed as an array of order-0 allocations
			 */
			__free_pages(page, 0);
			cond_resched();
		}
		atomic_long_sub(area->nr_pages, &nr_vmalloc_pages);
        // 释放vm_struct所占用的内存
		kvfree(area->pages);
	}

	kfree(area);
}

find_vm_area

函数最终调__find_vmap_area

static struct vmap_area *__find_vmap_area(unsigned long addr, struct rb_root *root)
{
	struct rb_node *n = root->rb_node;

	addr = (unsigned long)kasan_reset_tag((void *)addr);

	while (n) {
		struct vmap_area *va;

		va = rb_entry(n, struct vmap_area, rb_node);
		if (addr < va->va_start)
			n = n->rb_left;
		else if (addr >= va->va_end)
			n = n->rb_right;
		else
			return va;
	}

	return NULL;
}

没啥好说的,就是vmap_area_root 这个红黑树中找到符合要求的vm_area,并返回vm_area的成员vm_struct结构

vm_remove_mappings

static void vm_remove_mappings(struct vm_struct *area, int deallocate_pages)
{
    //...
	remove_vm_area(area->addr);
    //...
    _vm_unmap_aliases(start, end, flush_dmap);
    //...
}

vfree/vunmap释放内存流程图

总结

至此,vmalloc 分配、释放流程已基本剖析完成,下面来总结下:

  • vmalloc() 与 kmalloc() 性质不同的两种分配内存的函数,kmalloc() 基于 slab分配器,分配的内存物理地址连续,虚拟地址也连续;而vmalloc() 是从 VMALLOC_START ~ VMALLOC_END 区域中选择一个适合 size 的空的子区域,虚拟地址连续而物理地址可能不连续(每次物理page只能申请1个);

  • vmalloc() 是 页对齐申请内存 ,通过 alloc_page() 向 buddy申请内存时的 order 为0;

  • kmalloc() 可以申请物理连续内存,且映射是线性的,所以分配的速度上会更快,但是这样会造成碎片化问题,让碎片化管理变得困难;而 vmalloc() 是虚拟内存连续,所以每次需要跟物理page 一页一页的进行映射,造成vmalloc() 的效率不是很高;

  • kmalloc() 一般在小内存时使用,vmalloc() 一般在大内存是使用;

  • vmalloc() 分配过程中可以睡眠,因此不能用于中断上下文中;

  • vmalloc() 与 vmap() 相比只多了个向buddy 申请物理page的过程,而vmap() 是将物理pages作为参数带入;

  • 因为vmalloc() 与 vmap() 申请的流程有所差异,所以vfree() 和 vunmap() 的流程略微有区别,主要是体现在vfree() 需要对从buddy 申请来的pages 进行释放;

  • vunmap()是不允许在中断上下文中使用

  • vfree()是可以在中断上下文中使用