前言
我们先总结一下之前的内核启动早期的内存的流程:
第一个阶段: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_cache和kmem_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;
}这段代码我们需要了解的点如下:
vmap_area的slab cache的创建,后续所有的vmalloc所需要申请的vmap_area,均从此slab cache中申请插入已存在的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_area中vm字段是struct vm_struct结构,用于管理虚拟地址和物理页之间的映射关系,可以将struct vm_struct构成一个链表,维护多段映射。

vmalloc的内存分配API
常用API
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_IOREMAP、VM_NO_GUARD、VM_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数组分配策略
为什么这样做?
避免大块连续物理内存的压力:
pages数组本身可能很大(比如分配 1GB 内存,pages 数组需要 2MB)用
kmalloc需要物理连续的 2MB 内存,可能失败用
vmalloc只需要虚拟连续,更容易成功
递归但有限:
注释说 "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数组分配策略
为什么这样做?
小数组更高效:
小数组用
kmalloc分配更快,开销更小不需要建立复杂的页表映射
减少碎片:
小内存分配用 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()是可以在中断上下文中使用