Linux内存管理(三十三):vmalloc 详解

news/2025/3/14 17:20:57/

源码基于:Linux 5.4

0. 前言

kmalloc()、vmalloc()、malloc() 这三个函数是常用的内存分配函数,但有着本质的区别。

kmalloc() 基于slab 分配器,是建立在一个物理地址连续的大内存块上,所以 kmalloc() 分配的内存物理是上连续的,而且 kmalloc() 映射的虚拟内存在线性区域,也是连续的。详细可以查看《slub 分配器之kmem_cache_alloc》一文第 5.1.2 节。

相比于 kmalloc(),vmalloc() 实现是为了虚拟内存连续,而物理内存可以不用连续。正是因为物理内存不连续,而虚拟内存需要连续,所以,需要对每一个页进行映射,因此vmalloc() 的效率不是很高,只会在不得已的时候使用(例如,需要使用大内存)。另外,vmalloc() 申请内存的过程中可以睡眠,因此不能用于中断上下文中。

相对于上面两个函数,malloc() 是用户层的内存分配函数,最终会通过 brk() 和 mmap() 进行系统调用。

1. vmalloc 初始化

init/main.cstatic void __init mm_init(void)
{...mem_init();kmem_cache_init();...vmalloc_init();...
}

在 buddy初始化完成会进行slab 分配器的初始化,之后会通过 vmalloc_init() 对vmalloc 相关内存进行初始化:

mm/vmalloc.cvoid __init vmalloc_init(void)
{struct vmap_area *va;struct vm_struct *tmp;int i;/** Create the cache for vmap_area objects.*/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;
}
  • 通过 KMEM_CACHE,创建 vmap_area 这个 slab cache 描述符,并存到全局变量 vmap_area_cachep 中;
  • 遍历每个 CPU 的vmap_block_queue 和 vfree_deferred 变量进行初始化。其中vmap_block_queue 是非连续内存块队列管理结构,主要是队列以及对应的保护锁。而 vfee_deferred 是vmalloc 的内存延迟释放管理,除了队列初始化外,还创建了一个 free_work() 的工作队列用以异步释放内存;
  • 将已经存在的 vmlist 链表中的各项,分别从 slab中分配内存并进行初始化,最终insert 到 vmap_area_root 红黑树和 vmap_area_list 这个全局链表中;
  • 在初始化完成,需要将全局变量 vmap_initialized 这个变量设为 true;

2. vmalloc 的分配接口

2.1 vmalloc()

mm/vmalloc.cvoid *vmalloc(unsigned long size)
{return __vmalloc_node_flags(size, NUMA_NO_NODE, GFP_KERNEL);
}
EXPORT_SYMBOL(vmalloc);

最终调用 __vmalloc_node_flags(),这里的 gfp_mask 为 GFP_KERNEL。

mm/vmalloc.cstatic 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));
}

最终调用的是 __vmalloc_node(),注意 prot 默认为 PAGE_KERNEL,详细看第 2.5 节。

2.2 vzalloc()

mm/vmalloc.cvoid *vzalloc(unsigned long size)
{return __vmalloc_node_flags(size, NUMA_NO_NODE,GFP_KERNEL | __GFP_ZERO);
}
EXPORT_SYMBOL(vzalloc);

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

2.3 __vmalloc()

mm/vmalloc.cvoid *__vmalloc(unsigned long size, gfp_t gfp_mask, pgprot_t prot)
{return __vmalloc_node(size, 1, gfp_mask, prot, NUMA_NO_NODE,__builtin_return_address(0));
}
EXPORT_SYMBOL(__vmalloc);

有些驱动更喜欢使用 __vmalloc() 这个接口,直接指定gfp_mask 和 prot,最终也还是会调用到 __vmalloc_node(),详细看第 2.5 节。

2.4 vmalloc_node()

mm/vmalloc.cvoid *vmalloc_node(unsigned long size, int node)
{return __vmalloc_node(size, 1, GFP_KERNEL, PAGE_KERNEL,node, __builtin_return_address(0));
}
EXPORT_SYMBOL(vmalloc_node);

在特定的node 中申请内存,最终调用 __vmalloc_node(),注意 prot 默认为 PAGE_KERNEL,详细看第 2.5 节。

上面几个函数流程大致如下:

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

需要注意的是:通过这些函数调用到的 __vmalloc_node(),参数 align 默认为1,gfp_mask 默认为 GFP_KERNEL 或 GFP_KERNEL | __GFP_ZERO,prot 默认为 PAGE_KERNEL,node 默认为 -1,caller 默认为内建函数 __builtin_return_address()。

2.5 __vmalloc_node()

mm/vmalloc.cstatic 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);
}

最终调用到的函数为 __vmalloc_node_range(),其中 start 为 VMALLOC_START,end 为VMALLOC_END,vm_flags 为 0。详细的 __vmalloc_node_range() 分析流程见下一节。

按照 VA_BITS=39,PAGE_SHIFT=12(4K) 来看下ARM64内存的内存布局: 

VA_BITS=39, PAGE_SHIFT=12(4K页表)

内核空间内存分布如上图,其中专门给 vmalloc 分配一个区域(大概 250G),从 VMALLOC_START 到 VMALLOC_END。

dmesg 中还有虚拟地址的划分(4.16 版本之后这部分的打印被去掉,详细查看《内存与内存分布》一文第 6节):

[    0.000000] Memory: 2669108K/2904448K available (19772K kernel code, 3026K rwdata, 14348K rodata, 3200K init, 21716K bss, 214860K reserved, 20480K cma-reserved)
[    0.000000] Virtual kernel memory layout:
[    0.000000] modules : 0xffffffc008000000 - 0xffffffc010000000   (   128 MB)
[    0.000000] vmalloc : 0xffffffc010000000 - 0xfffffffebfff0000   (   250 GB)
[    0.000000]   .text : 0x(____ptrval____) - 0x(____ptrval____)   ( 19776 KB)
[    0.000000] .rodata : 0x(____ptrval____) - 0x(____ptrval____)   ( 14528 KB)
[    0.000000]   .init : 0x(____ptrval____) - 0x(____ptrval____)   (  3200 KB)
[    0.000000]   .data : 0x(____ptrval____) - 0x(____ptrval____)   (  3027 KB)
[    0.000000]    .bss : 0x(____ptrval____) - 0x(____ptrval____)   ( 21717 KB)
[    0.000000] fixed   : 0xfffffffefe5f9000 - 0xfffffffefea00000   (  4124 KB)
[    0.000000] PCI I/O : 0xfffffffefec00000 - 0xfffffffeffc00000   (    16 MB)
[    0.000000] vmemmap : 0xfffffffeffe00000 - 0xffffffffffe00000   (     4 GB maximum)
[    0.000000]           0xffffffff00002000 - 0xffffffff02e00000   (    45 MB actual)
[    0.000000] memory  : 0xffffff8008080000 - 0xffffff80c0000000   (  2943 MB)

详细看《内存与内存分布》6 节。

3. __vmalloc_node_range()

这个函数是vmalloc() 系列的分配函数最终处理的核心,本节将细化剖析。

mm/vmalloc.cvoid *__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; //暂存初始size,后续出错时打印需要//step1,//vmalloc申请的size 都按照页对齐size = PAGE_ALIGN(size);//检查size正确性,不能为0且不能大于totalram_pages,totalram_pages是bootmem分配器移交给//伙伴系统的物理内存页数总和if (!size || (size >> PAGE_SHIFT) > totalram_pages())goto fail;//step2,//vmalloc的核心处理函数之一//主要是创建vm_struct和vmap_area,并在VMALLOC区域找到一个可以包含请求大小的空位置,//  并使用该虚拟地址配置vmap_area 和 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;//step3,//分配内存,建立页面映射关系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);//kmemleak_vmalloc()记录分配信息kmemleak_vmalloc(area, size, gfp_mask);return addr;fail:warn_alloc(gfp_mask, NULL,"vmalloc: allocation failure: %lu bytes", real_size);return NULL;
}

这里重点流程归纳为:

  • step1,要求分配的内存大小按照页对齐,且申请大小不能超过最大值;
  • step2,__get_vm_area_node() 在vmalloc 区域找到一个包含请求大小的子区域,并使用vm_struct 和 vmap_area 进行配置保存,详细看第 4 节;
  • step3,__vmalloc_area_node() 进行实际的页面分配,并建立页表映射,更新页表 cache,详细看第 5 节;

注意:

对于 vmalloc() 调用,在进入 __get_vm_area_node() 时,会将 vm flags 添加上 VM_ALLOC | VM_UNINITIALIZED,并最终保存到 vm_struct 结构体中的 flags 成员中。 

3.1 数据结构和宏定义

3.1.1 vmalloc() 中的flag

include/linux/vmalloc.h#define VM_IOREMAP		0x00000001	/* ioremap() and friends */
#define VM_ALLOC		0x00000002	/* vmalloc() */
#define VM_MAP			0x00000004	/* vmap()ed pages */
#define VM_USERMAP		0x00000008	/* suitable for remap_vmalloc_range */
#define VM_DMA_COHERENT		0x00000010	/* dma_alloc_coherent */
#define VM_UNINITIALIZED	0x00000020	/* vm_struct is not fully initialized */
#define VM_NO_GUARD		0x00000040      /* don't add guard page */
#define VM_KASAN		0x00000080      /* has allocated kasan shadow memory */

3.1.2 struct vm_struct

include/linux/vmalloc.hstruct vm_struct {struct vm_struct	*next;void			*addr;unsigned long		size;unsigned long		flags;struct page		**pages;unsigned int		nr_pages;phys_addr_t		phys_addr;const void		*caller;
};

对于 vmalloc() 分配的一段连续的虚拟地址空间区域,都会用一个 vm_struct 结构来管理。

  • next:用于串联一个vm_struct 的链表使用;
  • addr:存放该虚拟地址空间的起始地址,与size 可知该区域的大小;
  • size:虚拟地址空间区域的大小;
  • flags:存储了该内存区域的内存类型;
  • pages:指向一个指针数组,该数组中的每个指针都表示一个映射到该虚拟区域的物理page;
  • nr_pages:存储 pages指向的指针数组的成员个数,即物理页面数目;
  • phys_addr:仅当使用 ioremap 映射了由物理地址描述的物理内存区域时才需要;
  • caller:调用vmalloc() 函数的函数地址;

/proc/vmallocinfo 中:

130|shift:/ # cat /proc/vmallocinfo
0x0000000000000000-0x0000000000000000    8192 bpf_jit_binary_alloc+0x94/0x1f4 pages=1 vmalloc
0x0000000000000000-0x0000000000000000    8192 bpf_jit_binary_alloc+0x94/0x1f4 pages=1 vmalloc
0x0000000000000000-0x0000000000000000    8192 bpf_jit_binary_alloc+0x94/0x1f4 pages=1 vmalloc
0x0000000000000000-0x0000000000000000    8192 bpf_jit_binary_alloc+0x94/0x1f4 pages=1 vmalloc
0x0000000000000000-0x0000000000000000    8192 bpf_jit_binary_alloc+0x94/0x1f4 pages=1 vmalloc
0x0000000000000000-0x0000000000000000    8192 bpf_jit_binary_alloc+0x94/0x1f4 pages=1 vmalloc
0x0000000000000000-0x0000000000000000    8192 bpf_jit_binary_alloc+0x94/0x1f4 pages=1 vmalloc

我们可以看到 vmalloc  区域大小都是页的整数倍,pages 表示使用了多少个物理页,显示的数目要 +1,例如pages为4,表示占用了 5个物理页。

当使用 ioremap 映射了由物理地址描述的物理内存区域时,会将物理地址保存在 phys_addr 成员中,ioremap 是把 IO 地址映射到内核空间,也就是不会使用系统 RAM 中的内存,也就不会调用 buddy 中的alloc_pages(),这部分不属于buddy,因此在 /proc/vmallocinfo 中看不到 pages 成员信息:

0x0000000000000000-0x0000000000000000    8192 jlq_pll_sha_setup+0x13c/0x380 phys=0x0000000034510000 ioremap
0x0000000000000000-0x0000000000000000    8192 jlq_pll_sha_setup+0x16c/0x380 phys=0x0000000034500000 ioremap
0x0000000000000000-0x0000000000000000    8192 jlq_clk_get_base+0xf8/0x2a8 phys=0x0000000034103000 ioremap
0x0000000000000000-0x0000000000000000    8192 jlq_clk_get_base+0x1fc/0x2a8 phys=0x00000000340ea000 ioremap
0x0000000000000000-0x0000000000000000    8192 jlq_clk_get_base+0xc4/0x2a8 phys=0x0000000030340000 ioremap
0x0000000000000000-0x0000000000000000    8192 jlq_clk_get_base+0x194/0x2a8 phys=0x0000000030090000 ioremap
0x0000000000000000-0x0000000000000000    8192 jlq_clk_get_base+0x1c8/0x2a8 phys=0x0000000030501000 ioremap
0x0000000000000000-0x0000000000000000    8192 jlq_clk_get_base+0x12c/0x2a8 phys=0x0000000030621000 ioremap
0x0000000000000000-0x0000000000000000    8192 jlq_clk_get_base+0x160/0x2a8 phys=0x0000000034459000 ioremap

3.1.3 vmap_area

include/linux/vmalloc.hstruct 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 three variables can be packed, because* a vmap_area object is always one of the three states:*    1) in "free" tree (root is vmap_area_root)*    2) in "busy" tree (root is free_vmap_area_root)*    3) in purge list  (head is vmap_purge_list)*/union {unsigned long subtree_max_size; /* in "free" tree */struct vm_struct *vm;           /* in "busy" tree */struct llist_node purge_list;   /* in purge list */};
};
  • va_start:vmalloc区域中的子区域的起始地址;
  • va_end:与va_start 配对,指定该子区域的结束地址;
  • rb_node:用以插入红黑树使用;
  • list:用以插入list 链表使用;
  • union:一个vmap_area 对象总是处于三种状态中的一种,用以记录当前的状态值;

4. __get_vm_area_node()

mm/vmalloc.cstatic 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;//vmalloc 不能再中断中被调用BUG_ON(in_interrupt());//vmalloc 要求页对齐size = PAGE_ALIGN(size);if (unlikely(!size))return NULL;if (flags & VM_IOREMAP)align = 1ul << clamp_t(int, get_count_order_long(size),PAGE_SHIFT, IOREMAP_MAX_ORDER);//step1//使用 kmalloc 分配一个vm_struct内存area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);if (unlikely(!area))return NULL;//step2//如果vmalloc 没有设置VM_NO_GUARD这个flag,表示需要多加一页为安全区间if (!(flags & VM_NO_GUARD))size += PAGE_SIZE;//step3//申请一个vmap_area并将其插入vmap_area_root中//注意,通过ioremap申请时,align需要单独计算va = alloc_vmap_area(size, align, start, end, node, gfp_mask);if (IS_ERR(va)) {  //----如果获取区域失败,则将slab area 进行free掉kfree(area);return NULL;}//step4//填充vmalloc描述符vm_struct中的成员setup_vmalloc_vm(area, va, flags, caller);return area;
}

重点有四个逻辑处理:

  • step1,kzalloc_node():最终会通过 kmalloc() 从slab 中分配一个vm_struct 的内存,最终也会将该内存作为 vmalloc() 描述符进行返回;
  • step2,如果vmalloc() 的flag 没有指定VM_NO_GUARD,都会多分配一个PAGE;
  • step3,alloc_vmap_area():申请一个vmap_area,详细见第 4.1 节;
  • step4,setup_vmalloc_vm():对vmalloc() 描述符vm_struct 进行填充,详细见第 4.2 节;

4.1 alloc_vmap_area()

mm/vmalloc.cstatic 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, *pva;unsigned long addr;int purged = 0;//进行一些提前判定,如果出错,则会进入panic//size不能为0//size不是以页对齐//align不是2的整数次幂BUG_ON(!size);BUG_ON(offset_in_page(size));BUG_ON(!is_power_of_2(align));//vmap是否已经初始化过if (unlikely(!vmap_initialized))return ERR_PTR(-EBUSY);might_sleep();//从 vmap_area_cachep 这个slab cache中分配出一个vmap_area 的内存va = kmem_cache_alloc_node(vmap_area_cachep,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:/** Preload this CPU with one extra vmap_area object to ensure* that we have it available when fit type of free area is* NE_FIT_TYPE.** The preload is done in non-atomic context, thus it allows us* to use more permissive allocation masks to be more stable under* low memory condition and high memory pressure.** Even if it fails we do not really care about that. Just proceed* as it is. "overflow" path will refill the cache we allocate from.*/preempt_disable();if (!__this_cpu_read(ne_fit_preload_node)) {preempt_enable();pva = kmem_cache_alloc_node(vmap_area_cachep, GFP_KERNEL, node);preempt_disable();if (__this_cpu_cmpxchg(ne_fit_preload_node, NULL, pva)) {if (pva)kmem_cache_free(vmap_area_cachep, pva);}}spin_lock(&vmap_area_lock);preempt_enable();//分配 vmap_area,并在虚拟地址范围内找到映射空间//如果分配失败,则返回 vendaddr = __alloc_vmap_area(size, align, vstart, vend);if (unlikely(addr == vend))goto overflow;//分配成功,addr就是该区域的起始地址va->va_start = addr;va->va_end = addr + size;va->vm = NULL;//将该vmap_area插入到红黑树vmap_area_root和全局链表vmap_area_list中insert_vmap_area(va, &vmap_area_root, &vmap_area_list);spin_unlock(&vmap_area_lock);//确认起始地址是否符合align对齐方式BUG_ON(!IS_ALIGNED(va->va_start, align));//起始地址不能比VMALLOC_START小BUG_ON(va->va_start < vstart);//同样,区域的结尾地址不能比VMALLC_END 大BUG_ON(va->va_end > vend);return va;overflow:spin_unlock(&vmap_area_lock);if (!purged) {purge_vmap_area_lazy();purged = 1;goto retry;}if (gfpflags_allow_blocking(gfp_mask)) {unsigned long 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);
}

如上面代码注释,主要是从 vmalloc区域找到匹配该大小的子区域,成功找到就返回虚拟空间的起始地址,如果失败则返回 vend。

4.1.1 __alloc_vmap_area()

mm/vmalloc.cstatic __always_inline unsigned long
__alloc_vmap_area(unsigned long size, unsigned long align,unsigned long vstart, unsigned long vend)
{unsigned long nva_start_addr;struct vmap_area *va;enum fit_type type;int ret;va = find_vmap_lowest_match(size, align, vstart);if (unlikely(!va))return vend;if (va->va_start > vstart)nva_start_addr = ALIGN(va->va_start, align);elsenva_start_addr = ALIGN(vstart, align);/* Check the "vend" restriction. */if (nva_start_addr + size > vend)return vend;/* Classify what we have found. */type = classify_va_fit_type(va, nva_start_addr, size);if (WARN_ON_ONCE(type == NOTHING_FIT))return vend;/* Update the free vmap_area. */ret = adjust_va_to_fit_type(va, nva_start_addr, size, type);if (ret)return vend;#if DEBUG_AUGMENT_LOWEST_MATCH_CHECKfind_vmap_lowest_match_check(size);
#endifreturn nva_start_addr;
}

4.2 setup_vmalloc_vm()

mm/vmalloc.cstatic void setup_vmalloc_vm(struct vm_struct *vm, struct vmap_area *va,unsigned long flags, const void *caller)
{spin_lock(&vmap_area_lock);vm->flags = flags;vm->addr = (void *)va->va_start;vm->size = va->va_end - va->va_start;vm->caller = caller;va->vm = vm;spin_unlock(&vmap_area_lock);
}

该函数主要是用来配置 vm_struct,同时将 vm_struct 和 vmap_area 进行关联。

5. __vmalloc_area_node()

mm/vmalloc.cstatic void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,pgprot_t prot, int node)
{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;//当 gfp_mask没有指定 DMA 或DMA32时,设置为__GFP_HIGHMEM,优先使用高端内存const gfp_t highmem_mask = (gfp_mask & (GFP_DMA | GFP_DMA32)) ?0 :__GFP_HIGHMEM;//确认申请的实际size,不包括guard pagenr_pages = get_vm_area_size(area) >> PAGE_SHIFT;//pages 数组的空间大小array_size = (nr_pages * sizeof(struct page *));//如果pages数组占用空间超过PAGE_SIZE,则使用vmalloc区域的虚拟地址(一个page大概64字节,也就是申请超过256KB)//如果pages数组占用的空间没有超过,则使用kmalloc 从slab中申请if (array_size > PAGE_SIZE) {pages = __vmalloc_node(array_size, 1, nested_gfp|highmem_mask,PAGE_KERNEL, node, area->caller);} else {pages = kmalloc_node(array_size, nested_gfp, node);}//如果没有多余空间了,那么此次的vmalloc申请就此结束if (!pages) {remove_vm_area(area->addr);kfree(area);return NULL;}//成功申请到了pages数组的内存,进一步初始化vmap_areaarea->pages = pages;area->nr_pages = nr_pages;for (i = 0; i < area->nr_pages; i++) {struct page *page;//直接通过alloc_page()一次申请一个页面if (node == NUMA_NO_NODE)page = alloc_page(alloc_mask|highmem_mask);elsepage = alloc_pages_node(node, alloc_mask|highmem_mask, 0);if (unlikely(!page)) {/* Successfully allocated i pages, free them in __vunmap() */area->nr_pages = i;atomic_long_add(area->nr_pages, &nr_vmalloc_pages);goto fail;}area->pages[i] = page;if (gfpflags_allow_blocking(gfp_mask|highmem_mask))cond_resched();}//申请到的虚拟页面数量atomic_long_add(area->nr_pages, &nr_vmalloc_pages);//建立页面映射,该函数会调用到vmap_page_range_noflush()遍历页表和填充对应的页表if (map_vm_area(area, prot, pages))goto fail;return area->addr;fail:warn_alloc(gfp_mask, NULL,"vmalloc: allocation failure, allocated %ld of %ld bytes",(area->nr_pages*PAGE_SIZE), area->size);__vfree(area->addr);return NULL;
}

 5.1 map_vm_area()

mm/vmalloc.cint map_vm_area(struct vm_struct *area, pgprot_t prot, struct page **pages)
{unsigned long addr = (unsigned long)area->addr;unsigned long end = addr + get_vm_area_size(area);int err;err = vmap_page_range(addr, end, prot, pages);return err > 0 ? 0 : err;
}

这里映射的是实际的分配大小,最终通过 vmap_page_range()

mm/vmalloc.cstatic int vmap_page_range(unsigned long start, unsigned long end,pgprot_t prot, struct page **pages)
{int ret;ret = vmap_page_range_noflush(start, end, prot, pages);flush_cache_vmap(start, end);return ret;
}

----> vmap_page_range_noflush()

mm/vmalloc.cstatic int vmap_page_range_noflush(unsigned long start, unsigned long end,pgprot_t prot, struct page **pages)
{pgd_t *pgd;unsigned long next;unsigned long addr = start;int err = 0;int nr = 0;BUG_ON(addr >= end);pgd = pgd_offset_k(addr);do {next = pgd_addr_end(addr, end);err = vmap_p4d_range(pgd, addr, next, prot, pages, &nr);if (err)return err;} while (pgd++, addr = next, addr != end);return nr;
}
  • 有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 中。

6. vmalloc() 的分配流程

7. vfree()

mm/vmalloc.cvoid vfree(const void *addr)
{BUG_ON(in_nmi());kmemleak_free(addr);might_sleep_if(!in_interrupt()); //如果不是在中断上下文中调用//则sleepif (!addr) //如果传入的虚拟地址为NULL,则不做任何处理return;__vfree(addr);  //最终调用__vfree()接口
}
EXPORT_SYMBOL(vfree);

7.1 __vfree()

mm/vmalloc.cstatic void __vfree(const void *addr)
{if (unlikely(in_interrupt()))__vfree_deferred(addr);else__vunmap(addr, 1);
}

如果处于中断中调用 __vfree_deferred(),如果没有则调用 __vunmap() 进行回收。

在分析 __vunmap() 函数之前,需要了解 vmap() 和 vunmap()函数,这是一对用来对连续虚拟内存的分配、回收。

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

vmap() 和vunmap() 下面会继续剖析,这里着重看下在中断处理中调用时的 __vfree_deferred():

mm/vmalloc.cstatic 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* nother 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);
}

当通过中断处理程序调用时,映射的连续虚拟地址内存无法立即释放,因此在工作队列中注册 free_work() 函数添加到每个 CPU vfree_deferred 中以进行延迟处理。

free_work() 这部分的设置是 vmalloc_init() 中完成,详细看第 1 节。 

8. vmap()

mm/vmalloc.cvoid *vmap(struct page **pages, unsigned int count,unsigned long flags, pgprot_t prot)
{struct vm_struct *area;unsigned long size;		/* In bytes *///如果有任务紧急请求重新安排作为抢占点,则需要休眠might_sleep();//如果请求的页数比total的页数多,则放弃处理并返回 NULLif (count > totalram_pages())return NULL;size = (unsigned long)count << PAGE_SHIFT;area = get_vm_area_caller(size, flags, __builtin_return_address(0));if (!area)return NULL;if (map_vm_area(area, prot, pages)) {vunmap(area->addr);return NULL;}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;

8.1 get_vm_area_caller()

mm/vmalloc.cstruct vm_struct *get_vm_area_caller(unsigned long size, unsigned long flags,const void *caller)
{return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END,NUMA_NO_NODE, GFP_KERNEL, caller);
}

同 vmalloc() 流程,同样指定从 VMALLOC_START ~ VMALLOC_END 中分配连续虚拟地址,gfp_mask 同样为 GFP_KERNEL。

函数 __get_vm_area_node() 详细见第 4 节。

8.2 map_vm_area()

详见上文第 5.1 节。

8.3 比较vmalloc()和vmap()

通过流程剖析之后我们发现 vmalloc() 和vmap() 的流程基本上是一样的。

  • 需要经过 __get_vm_area_node() 创建 struct vm_struct 和 struct vmap_area,并从 VMALLOC区域根据 size 查找到一个空的子区域;
  • 需要经过map_vm_area() 对连续虚拟地址和物理地址进行映射;

不同点在于:

  • vmalloc() 需要从 buddy 系统每次申请 1个页面,放进struct vm_struct 中pages成员中;
  • vmap() 参数自带pages,无需从 buddy 中申请;

9. vunmap()

mm/vmalloc.cvoid vunmap(const void *addr)
{BUG_ON(in_interrupt());might_sleep();if (addr)__vunmap(addr, 0);
}
EXPORT_SYMBOL(vunmap);

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

注意,

  • vunmap() 是不允许在中断处理程序中调用。
  • vunmap() 进行回收时,无需释放物理页面,而vfree() 是需要释放页面的;

9.1 __vunmap()

mm/vmalloc.cstatic 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_structarea = 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));//去掉映射关系的核心函数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);__free_pages(page, 0);}atomic_long_sub(area->nr_pages, &nr_vmalloc_pages);kvfree(area->pages);}//释放vm_struct所占用的内存kfree(area);return;
}

该函数是vunmap() 和vfree() 函数的实现函数,主要分四个部分:

  • 通过find_vm_area() 函数,根据虚拟地址从 vmap_area_root 红黑树中查询到该子区域 vmap_area,并根据该子区域得到 vm_struct;
  • 通过vm_remove_mappings() 函数,解除一切关系。如映射关系、vmap_area_root / vmap_area_list 中的区域印记等;
  • 通过变量 deallocate_pages,确认源头是从vunmap() 还是 vfree(),进而确认是否将area->pages 内存释放,对于 vfree() 调用,是需要释放内存的;
  • 释放 vm_struct 所占用内存;

 

10. 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 进行释放;

 

 


http://www.ppmy.cn/news/46981.html

相关文章

c++引用

目录 C 引用的概念&#xff1a; c引用在工程中的好处&#xff1a; c引用的用法 c引用的场景&#xff1a; c引用的场景1&#xff1a;作为函数参数 c引用的场景2&#xff1a;作为函数返回值 引用的优点包括&#xff1a; 在 C 中使用引用时需要注意以下几点&#xff1a; C…

【JUC】volatile和JMM

【JUC】volatile和JMM 文章目录 【JUC】volatile和JMM1. volatile1.1 特点1.2 内存语义 2. 内存屏障2.1 分类2.2 什么叫保证有序性&#xff1f;2.3 内存屏障的4种插入策略 3. volatile特性3.1 保证可见性3.2 volatile读写过程3.3 没有原子性3.4 指令禁重排(有序性) 4. 正确使用…

【rustdesk 】rfc5128 :跨NATs的P2P通信技术 和rdserver

rfc5128rustdesk 的服务器是基于rfc5128 的 RendezvousServer // https://tools.ietf.org/rfc/rfc5128.txt // https://blog.csdn.net/bytxl/article/details/44344855use flexi_logger::*; use hbb_common::{bail, config::RENDEZVOUS_PORT, ResultType}; use hbbs

【vue】Vue 开发技巧:

文章目录 1.路由参数解耦2.功能组件3.样式范围4.watch的高级使用5.watch监听多个变量6.事件参数$event7.程序化事件监听器8.监听组件生命周期 1.路由参数解耦 通常在组件中使用路由参数&#xff0c;大多数人会做以下事情。 export default {methods: {getParamsId() {return …

TensorFlow GPU不可用,WSL2安装

这个帖子写给23年刚买电脑、系统是win11&#xff0c;tensorflow版本是2.10以上的兄弟们。不符合的可以去看其他答案了。 这是以我三天来的安装经历来写的&#xff0c;希望能给后来的兄弟们减少时间的浪费。 win11&#xff0c;安装的tensorflow的版本都是2.12的&#xff0c;但…

ARM汇编第一次上机(顺序、分支、单重循环)【嵌入式系统】

ARM汇编第一次上机&#xff08;顺序、分支、单重循环&#xff09;【嵌入式系统】 前言推荐说明ARM汇编第一次上机&#xff08;顺序、分支、单重循环&#xff09;内容1 sum1流程图代码编写结果分析 2 sum2流程图代码编写结果分析 3 numbers流程图代码编写结果分析 最后 前言 20…

基于C#编程建立泛型Vector数据类型及对应处理方法

目录 一、简介 二、方法 2.1 建立Vector类 2.2 Vector成员 2.3 Vector属性 2.4 Vector方法 2.4.1 构造函数 2.4.2 Vector元素操作方法 2.4.3 Vector 运算 三、调用方法 3.1 方法 3.1.1 Append 3.1.2 this[] 3.1.3 Insert 3.1.4 DelLen 3.1.5 FindNumber 3.1.6 …

觉非科技发布:基于BEV的数据闭环融合智驾解决方案

2023年上海车展期间&#xff0c;觉非科技基于BEV的数据闭环融合智能驾驶解决方案正式发布。 该方案可通过量产车BEV的实时感知结果&#xff0c;提供完整的城市Map-lite及Map-free数据闭环融合解决方案&#xff0c;并满足城市NOA、记忆通勤/泊车以及感知大模型训练的需要。 车…