vmalloc的理解

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 !