kmalloc 是分配的物理地址是连续的,如果系统中需要分配大块的内存,这个时候使用kmalloc可能就不一定能分配到连续的物理内存,这个时候就需要使用vmalloc。
可以参考kmalloc的实现,如果要分配大小大于 KMALLOC_MAX_CACHE_SIZE 的内存,系统不再使用slab来进行分配,而是使用__kmalloc_large_node。 这个时候其实就是使用 struct folio 来记录分配的。
vmalloc的作用就是分配大块内存,这个大块内存是从虚拟地址的角度看的,而实际的对应的物理地址,可能是一些不连续的page组成的。 所以vmalloc主要是对虚拟地址进行管理,它物理地址的分配使用的就是buddy系统来分配一些pages:
可以看到arm64的linux 的内存布局里面有一大块就是给vmalloc来使用的:
Start End Size Use
-----------------------------------------------------------------------
0000000000000000 0000ffffffffffff 256TB user
ffff000000000000 ffff7fffffffffff 128TB kernel logical memory map
[ffff600000000000 ffff7fffffffffff] 32TB [kasan shadow region]
ffff800000000000 ffff80007fffffff 2GB modules
ffff800080000000 fffffbffefffffff 124TB vmalloc
fffffbfff0000000 fffffbfffdffffff 224MB fixed mappings (top down)
fffffbfffe000000 fffffbfffe7fffff 8MB [guard region]
fffffbfffe800000 fffffbffff7fffff 16MB PCI I/O space
fffffbffff800000 fffffbffffffffff 8MB [guard region]
fffffc0000000000 fffffdffffffffff 2TB vmemmap
fffffe0000000000 ffffffffffffffff 2TB [guard region]
同时也可以看到kernel整个运行的虚拟地址也是在vmalloc区域, 参考:
#define KIMAGE_VADDR (MODULES_END)
#define VMALLOC_START (MODULES_END)
所以kernel 所在image的虚拟地址也是要纳入vmalloc来进行管理。 下面会有提到。
关键数据结构
虚拟地址的管理分两部分,一部分是空闲虚拟地址的管理,另外一部分就是正在使用的虚拟地址的管理。
空闲虚拟地址有两个关键数据结构,一个是free_vmap_area_root 所指向的红黑树,另外一个就是free_vmap_area_list所指的双向链表。
这两个数据结构的节点都是struct vmap_area, 也就是对应的如下两个字段:
struct vmap_area {
//....
struct rb_node rb_node; /* address sorted rbtree */
struct list_head list; /* address sorted list */
//...
}
为什么需要这两个数据结构, free_vmap_area_root用来在分配的时候快速找到满足大小的可用虚拟地址. free_vmap_area_list是在需要把两个相邻区域合并的时候,使用这个free_vmap_area_list 就比较方便的找到他们相邻的节点,方便快速合并。 可以参考代码:
purge_vmap_node-> reclaim_list_global -> merge_or_add_vmap_area_augment
另一个就是分配出去的虚拟地址的管理,这个的关键的管理数据结构就是 static struct vmap_node *vmap_nodes。
static struct vmap_node {
/* Simple size segregated storage. */
struct vmap_pool pool[MAX_VA_SIZE_PAGES];
spinlock_t pool_lock;
bool skip_populate;
/* Bookkeeping data of this node. */
struct rb_list busy;
struct rb_list lazy;
/*
* Ready-to-free areas.
*/
struct list_head purge_list;
struct work_struct purge_work;
unsigned long nr_purged;
} single;
- struct rb_list busy 是一个红黑树,来记录哪些struct vmap_area 被分配出去了。它对应的节点就是struct vmap_area。
- struct rb_list lazy; 也是一个红黑树,来记录被释放的struct vmap_area 还没有被放入到空闲列表里面的。 这个目的就是加快释放,只有在满足一定条件的时候(nr_lazy > nr_lazy_max),才需要启动work queue(drain_vmap_work)来把这个lazy表里面的节点回收到空闲列表里去。 而下面的purge_list purge_work nr_purged 就是为了回收这个lazy list 而要使用的数据结构。 后面回收过程会详细记录。
- struct vmap_pool pool 相关的几个字段就是建立一个1-MAX_VA_SIZE_PAGES个Page 的虚拟地址pool, 在回收的时候不是立即放到全局的free list, 先放到这个pool里面。 分配的时候也是先看下这个pool 里面有没有,优先到这个pool里分配。
vmap_nodes 也是一个数组,这里是把整个虚拟地址空间打散,每 16个pages放在一个vmap_nodes 中。 比如系统有三个vmap_nodes, 第1-16个 page 放在vmap_nodes[0], 第17-32个page 放在vmap_nodes[1],第33-48个page放在vmap_nodes[2] ,第49-64个page放在vmap_nodes[0],一次类推。 这样做的好处是提高并行性。 并且现在的系统都是多CPU的,所以这个vmap_nodes的个数是跟cpu的个数相关的,参考函数:
static void vmap_init_nodes(void)
{
struct vmap_node *vn;
int i, n;
#if BITS_PER_LONG == 64
/*
* A high threshold of max nodes is fixed and bound to 128,
* thus a scale factor is 1 for systems where number of cores
* are less or equal to specified threshold.
*
* As for NUMA-aware notes. For bigger systems, for example
* NUMA with multi-sockets, where we can end-up with thousands
* of cores in total, a "sub-numa-clustering" should be added.
*
* In this case a NUMA domain is considered as a single entity
* with dedicated sub-nodes in it which describe one group or
* set of cores. Therefore a per-domain purging is supposed to
* be added as well as a per-domain balancing.
*/
n = clamp_t(unsigned int, num_possible_cpus(), 1, 128);
if (n > 1) {
vn = kmalloc_array(n, sizeof(*vn), GFP_NOWAIT | __GFP_NOWARN);
if (vn) {
/* Node partition is 16 pages. */
vmap_zone_size = (1 << 4) * PAGE_SIZE;
nr_vmap_nodes = n;
vmap_nodes = vn;
} else {
pr_err("Failed to allocate an array. Disable a node layer\n");
}
}
初始化
前面有提到kernel image本身运行的虚拟地址也是在这个vmalloc 的区域,在kernel 启动的阶段,这里定义了一个struct vm_struct 的的list来记录哪些VA地址空间已经被使用了。
static struct vm_struct *vmlist __initdata;
可以看到,在建立kernel页表的时候,会调用 paging_init -> declare_kernel_vmas -> declare_vma -> vm_area_add_early 把kernel使用过的空间放到vmlist 中。
当vmalloc_init 初始化的时候,会首先将这部分虚拟地址放入到 vmap_nodes中对应的busy 树里面去。
/*
* Setup nodes before importing vmlist.
*/
vmap_init_nodes();
/* 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;
vn = addr_to_node(va->va_start);
insert_vmap_area(va, &vn->busy.root, &vn->busy.head);
}
然后根据这些使用的va地址列表vmlist,建立free_vmap_area_root 和free_vmap_area_list,参考函数: vmalloc_init-> vmap_init_free_space 。
这里会使用:
vmap_area_cachep = KMEM_CACHE(vmap_area, SLAB_PANIC);
来建立 slub cache pool,然后vmap_area 都是在这个cache pool里面来分配,加快分配速度。
内存分配
所有分配函数最后都是调用 __vmalloc_node_range 来进行实际的虚拟地址和物理page的分配。
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)
{
//分配虚拟地址
area = __get_vm_area_node(real_size, align, shift, VM_ALLOC |
VM_UNINITIALIZED | vm_flags, start, end, node,
gfp_mask, caller);
//分配物理page,并且把这个page和VA在MMU页表里面给对应起来
ret = __vmalloc_area_node(area, gfp_mask, prot, shift, node);
}
先看__get_vm_area_node -> alloc_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,
unsigned long va_flags)
{
/*
* If a VA is obtained from a global heap(if it fails here)
* it is anyway marked with this "vn_id" so it is returned
* to this pool's node later. Such way gives a possibility
* to populate pools based on users demand.
*
* On success a ready to go VA is returned.
*/
// 首先尝试到vmap_node 的vmap_pool里面去分配(node_pool_del_va)
// 并且如果是VMALLOC_START-END的地址,就设置vn_id,
// 后面释放的时候就直接当道vmap_pool中,不直接放入global free list 中
va = node_alloc(size, align, vstart, vend, &addr, &vn_id);
retry:
if (addr == vend) {
//如果上面的没有分配成功,就到全局的free list里面去分配。
preload_this_cpu_lock(&free_vmap_area_lock, gfp_mask, node);
addr = __alloc_vmap_area(&free_vmap_area_root, &free_vmap_area_list,
size, align, vstart, vend);
spin_unlock(&free_vmap_area_lock);
}
//分配成功之后,再把这个va插入到vmap_node 的对应的busy树里面
spin_lock(&vn->busy.lock);
insert_vmap_area(va, &vn->busy.root, &vn->busy.head);
spin_unlock(&vn->busy.lock);
return va;
}
执行完上面的步骤之后,vmap_area 就跟一个分配的vm_struct 给对应起来了。
通过上面的步骤,已经找到满足要求的虚拟地址,接下来就是给他们分配物理地址,这个时候不是必须连续的。 因为只有vm_struct 里面才有物理地址的信息。
在函数__vmalloc_area_node -> vm_area_alloc_pages 会去buddy系统里面去申请真正的物理内存。
在分配好了物理内存之后,就需要把VA和这些分配的page给对应起来。参考函数:
__vmalloc_area_node-> vmap_pages_range -> vmap_pages_range_noflush -> __vmap_pages_range_noflush -> vmap_range_noflush
内存释放
释放的流程大概是调用remove_vm_area 从前面讲的vmap_nodes中对应的busy list 里面删除,然后再调用__free_page 回收到buddy 系统里面去。
void vfree(const void *addr)
{
// 在中断中,调用vfree_deferred 的workqueue来进行回收,在workqueue里面再调用vfree函数进行回收。 本质是一样的。
if (unlikely(in_interrupt())) {
vfree_atomic(addr);
return;
}
//从vmap_nodes中对应的busy list 里面删除, 且会清除MMU页表
vm = remove_vm_area(addr);
if (unlikely(!vm)) {
WARN(1, KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
addr);
return;
}
for (i = 0; i < vm->nr_pages; i++) {
//回收到buddy系统中
__free_page(page);
cond_resched();
}
}
在remove_vm_area -> find_unlink_vmap_area 会在addr 对应的vmap_node中找到vmap_area 并且会把它从busy list里面删去。
在remove_vm_area-> free_unmap_vmap_area-> vunmap_range_noflush ->__vunmap_range_noflush 里面修改对应的MMU页表,删除对应的页表项。
在函数remove_vm_area -> free_unmap_vmap_area -> free_vmap_area_noflush -> insert_vmap_area 把对应的vmap_area 插入到vmap_node 的lazy队列。根据lazy队列的情况启动workqueue来回收虚拟地址:
static void free_vmap_area_noflush(struct vmap_area *va)
{
/*
* If it was request by a certain node we would like to
* return it to that node, i.e. its pool for later reuse.
*/
//这里的代码和函数node_alloc 里面对应
vn = is_vn_id_valid(vn_id) ?
id_to_node(vn_id):addr_to_node(va->va_start);
//放入lazy树中
spin_lock(&vn->lazy.lock);
insert_vmap_area(va, &vn->lazy.root, &vn->lazy.head);
spin_unlock(&vn->lazy.lock);
trace_free_vmap_area_noflush(va_start, nr_lazy, nr_lazy_max);
/* After this point, we may free va at any time */
if (unlikely(nr_lazy > nr_lazy_max))
schedule_work(&drain_vmap_work);
}
在workqueue 会运行函数__purge_vmap_area_lazy 这个函数首先会把vmap_node 中的lazy list 放入对应vmap_node 的purge_list:
static bool __purge_vmap_area_lazy(unsigned long start, unsigned long end,
bool full_pool_decay)
{
for (i = 0; i < nr_vmap_nodes; i++) {
vn = &vmap_nodes[i];
INIT_LIST_HEAD(&vn->purge_list);
//有可能vmap_node-> pool 已经有很多空闲地址了,这里就是要把这个pool给缩小一下
//如何full_pool_decay 为true就是全部清楚掉,再调用reclaim_list_global 放到global free list里面
decay_va_pool_node(vn, full_pool_decay);
if (RB_EMPTY_ROOT(&vn->lazy.root))
continue;
//把lazy list 放入对应vmap_node 的purge_list
spin_lock(&vn->lazy.lock);
WRITE_ONCE(vn->lazy.root.rb_node, NULL);
list_replace_init(&vn->lazy.head, &vn->purge_list);
spin_unlock(&vn->lazy.lock);
}
__purge_vmap_area_lazy接下来就是根据vn->purge_list来启动vn->purge_work 来工作:
for_each_cpu(i, &purge_nodes) {
vn = &vmap_nodes[i];
if (nr_purge_helpers > 0) {
INIT_WORK(&vn->purge_work, purge_vmap_node);
if (cpumask_test_cpu(i, cpu_online_mask))
schedule_work_on(i, &vn->purge_work);
else
schedule_work(&vn->purge_work);
nr_purge_helpers--;
} else {
vn->purge_work.func = NULL;
purge_vmap_node(&vn->purge_work);
nr_purged_areas += vn->nr_purged;
}
}
然后每个CPU执行函数 purge_vmap_node,在函数purge_vmap_node 中,会把回收的一些 vmap_area 放到vmap_node-> pool 里面,以便可以快速的重新利用:
static void purge_vmap_node(struct work_struct *work)
{
LIST_HEAD(local_list);
vn->nr_purged = 0;
list_for_each_entry_safe(va, n_va, &vn->purge_list, list) {
list_del_init(&va->list);
vn->nr_purged++;
//加入到vmap_node-> pool
if (is_vn_id_valid(vn_id) && !vn->skip_populate)
if (node_pool_add_va(vn, va))
continue;
/* Go back to global. */
list_add(&va->list, &local_list);
}
//把这些要真正要回收的放入到free list中。
reclaim_list_global(&local_list);
}
最后调用reclaim_list_global 放入全局的free_vmap_area_root中。
调试
使用如下命令查看vmalloc的分配情况:
# cat /proc/vmallocinfo
0xffff800080000000-0xffff800080005000 20480 start_kernel+0x258/0x614 pages=4 vmalloc N0=4
0xffff800080005000-0xffff800080007000 8192 gen_pool_add_owner+0x4c/0xc4 pages=1 vmalloc N0=1
0xffff800080008000-0xffff80008000d000 20480 start_kernel+0x258/0x614 pages=4 vmalloc N0=4
0xffff80008000d000-0xffff80008000f000 8192 gen_pool_add_owner+0x4c/0xc4 pages=1 vmalloc N0=1
0xffff800082b00000-0xffff800082b05000 20480 kernel_clone+0x68/0x368 pages=4 vmalloc N0=4
0xffff800082b0b000-0xffff800082b0d000 8192 ioremap_prot+0x50/0x78 phys=0x000000001c130000 ioremap
0xffff800082b0d000-0xffff800082b0f000 8192 ioremap_prot+0x50/0x78 phys=0x000000001c140000 ioremap
0xffff800082b80000-0xffff800082b85000 20480 kernel_clone+0x68/0x368 pages=4 vmalloc N0=4
0xffff800082b85000-0xffff800082b87000 8192 ioremap_prot+0x50/0x78 phys=0x000000005019e000 ioremap
0xffff800082b88000-0xffff800082b8d000 20480 kernel_clone+0x68/0x368 pages=4 vmalloc N0=4
0xffff800082b8d000-0xffff800082b8f000 8192 ioremap_prot+0x50/0x78 phys=0x0000000050008000 ioremap
0xffff800082c00000-0xffff800082e01000 2101248 ioremap_prot+0x50/0x78 phys=0x000000002f100000 ioremap
0xffff800082e02000-0xffff800082e05000 12288 ioremap_prot+0x50/0x78 phys=0x0000000050196000 ioremap
0xffff800082e08000-0xffff800082e0d000 20480 kernel_clone+0x68/0x368 pages=4 vmalloc N0=4
0xffff800082e80000-0xffff800082e85000 20480 kernel_clone+0x68/0x368 pages=4 vmalloc N0=4
0xffff800082e88000-0xffff800082e8d000 20480 kernel_clone+0x68/0x368 pages=4 vmalloc N0=4
0xffff800082f00000-0xffff800082f05000 20480 kernel_clone+0x68/0x368 pages=4 vmalloc N0=4
0xffff800082f08000-0xffff800082f0d000 20480 kernel_clone+0x68/0x368 pages=4 vmalloc N0=4
0xffff800083300000-0xffff800083305000 20480 kernel_clone+0x68/0x368 pages=4 vmalloc N0=4
0xffff800083308000-0xffff80008330d000 20480 kernel_clone+0x68/0x368 pages=4 vmalloc N0=4
comments