分享

linux vmalloc

 waston 2022-09-05 发布于上海

参考:https://zhuanlan.zhihu.com/p/452710310
内核版本:4.4.0

1 主要接口

主要接口就2个, 1个是申请虚拟内存 vmalloc, 一个是释放虚拟内存 vfree


/*
* 申请虚拟内存, 虚拟地址空间连续, 但是物理地址不连续
* size: 大小
* 成功返回地址, 失败返回NULL
*/

void *vmalloc(unsigned long size)

/*
 * 释放有vmalloc申请的虚拟内存
 * addr: 要释放的虚拟内存的地址
 */
void vfree(const void *addr)

2 主要结构体

申请虚拟内存涉及的结构体比较简单,主要就2个,因此可以先全部过一下

2.1 struct vm_struct

// 用于存储vmalloc区间
struct vm_struct {
    struct vm_struct    *next; // 下个vm_struct
    void            *addr; // 虚拟地址
    unsigned long        size; // 虚拟空间大小
    unsigned long        flags; // 标志位
    struct page        **pages; // 当指针数组用,存放物理页
    unsigned int        nr_pages; // 有多个物理页
    phys_addr_t        phys_addr; // 当使用ioremap 才会使用到
    const void        *caller; //  保存调用函数的地址
};

flags标志位的主要内容如下

/* bits in flags of vmalloc's vm_struct below */
#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_VPAGES        0x00000010    /* buffer for pages was vmalloc'ed */
#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 */
/* bits [20..32] reserved for arch specific ioremap internals */

2.2 struct vmap_area

struct vmap_area {
    unsigned long va_start; // 虚拟起始地址
    unsigned long va_end; // 虚拟结束地址
    unsigned long flags; // 标志位
    // 链接全局的红黑树
    struct rb_node rb_node;         /* address sorted rbtree */
    // 链接到全链表
    struct list_head list;          /* address sorted list */
    // 当要删除时链接到 purge_list
    struct list_head purge_list;    /* "lazy purge" list */
    struct vm_struct *vm; // 指向vm_struct
    struct rcu_head rcu_head;
};

2.3 数据结构之间的关系

  1. 有一个全局的链表vmap_area_list, 所有的struct vmap_area->list 都链接到这里链表上, 按照起始地址从小到大排列
    同时还有一个全局的红黑树根结点 vmap_area_root , 所有的struct vmap_area->rb_node 也都链接在这个红黑树上,同样按照起始地址来排列

  2. 这里之所有把每个struct vmap_area 同时链接到链表和红黑树的原因如下:
    2.1 红黑树方便找到某个地址是否存在,具体函数见find_vm_area()
    2.2 链表方便遍历所有的struct vmap_area

  3. 数据结构之间的关系如下图
    请添加图片描述

3 源码分析

3.1 初始化

  1. 初始化有2处地方,一个是vmalloc_init(),函数里面会遍历 vmlist, 把里面的虚拟空间调用 __insert_vmap_area 链接起来

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

    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. */
    // 将依据存在的vmalloc空间进来
    for (tmp = vmlist; tmp; tmp = tmp->next) {
        va = kzalloc(sizeof(struct vmap_area), GFP_NOWAIT);
        va->flags = VM_VM_AREA;
        va->va_start = (unsigned long)tmp->addr;
        va->va_end = va->va_start + tmp->size;
        va->vm = tmp;
        __insert_vmap_area(va);
    }

    vmap_area_pcpu_hole = VMALLOC_END;

    vmap_initialized = true;
}

vmlist 是一个全局的指针, 通过函数vm_area_add_early() 把虚拟空间链接到上面

// 全局vmlist,早期初始化会将虚拟地址空间链接到这上面来
static struct vm_struct *vmlist __initdata;

// 链接到 vmlist的函数
void __init vm_area_add_early(struct vm_struct *vm)
{
    struct vm_struct *tmp, **p;

    BUG_ON(vmap_initialized);
    for (p = &vmlist; (tmp = *p) != NULL; p = &tmp->next) {
        if (tmp->addr >= vm->addr) {
            BUG_ON(tmp->addr < vm->addr + vm->size);
            break;
        } else
            BUG_ON(tmp->addr + tmp->size > vm->addr);
    }
    vm->next = *p;
    *p = vm;
}

其中的一个调用流程如下
在这里插入图片描述

  1. 另一个是在模块初始化注册了一个/proc/vmallocinfo调试节点, 用于查看申请的虚拟内存空间

static const struct file_operations proc_vmalloc_operations = {
    .open        = vmalloc_open,
    .read        = seq_read,
    .llseek        = seq_lseek,
    .release    = seq_release_private,
};

static int __init proc_vmalloc_init(void)
{
    proc_create("vmallocinfo", S_IRUSR, NULL, &proc_vmalloc_operations);
    return 0;
}
module_init(proc_vmalloc_init);

在这里插入图片描述

3.2 vmalloc

申请虚拟区间分为2大步骤

  1. 找到一块可用的虚拟区间

  2. 申请物理内存并建立映射

创建一个虚拟内存区间需要调用4次申请内存的函数
1)给 struct vm_struct 申请空间
2)给 struct vmap_area 申请空间
3)给 struct vmap_area->pages 申请空间用于存放物理页
4)申请物理页, 存放到 struct vmap_area->pages 中

下面开始源码分析

  1. 前面几个函都是简单的封装 vmalloc->__vmalloc_node_flags->__vmalloc_node->__vmalloc_node_range

void *vmalloc(unsigned long size)
{
     // 没有指定node
     // __GFP_HIGHMEM 可以走高端内存分配
     return __vmalloc_node_flags(size, NUMA_NO_NODE,  GFP_KERNEL | __GFP_HIGHMEM);
}

static inline void *__vmalloc_node_flags(unsigned long size,
                    int node, gfp_t flags)
{
    // align = 1
    // __builtin_return_address 是返回函数的调用地址
    return __vmalloc_node(size, 1, flags, PAGE_KERNEL,
                    node, __builtin_return_address(0));
}
void *__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));
}

内核启动时专门分配了一块区间给vmalloc,这里的区间为0xf0800000~0xff800000, 因此下面的宏
VMALLOC_START = 0xf0800000
VMALLOC_END = 0xff800000
在这里插入图片描述

static void *__vmalloc_node(unsigned long size, unsigned long align,
                gfp_t gfp_mask, pgprot_t prot,
                int node, const void *caller)
{
    // VMALLOC_START = 0xf0800000
    // VMALLOC_END = 0xff800000
    return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END,
                gfp_mask, prot, 0, node, caller);
}
  1. __vmalloc_node_range函数就是申请虚拟内存的主要实现

注:后面代码都会删除了一些不用的注释和非关键函数,方便主要代码的阅读

/*
 * 传进来的参数如下
 * size: 需要申请的空间大小
 * align:为1
 * start: 传进来的宏 VMALLOC_START-0xf0800000
 * end: 传进来的宏 VMALLOC_END-0xff800000
 * gfp_mask:GFP_KERNEL | __GFP_HIGHMEM
 * prot:PAGE_KERNEL
 * vm_flags: 0
 * node: NUMA_NO_NODE
 * caller: 函数调用地址
*/
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 *addr;
    unsigned long real_size = size;

    // 因为后面会走伙伴系统申请物理页, 因此需要按页对齐
    size = PAGE_ALIGN(size);
    if (!size || (size >> PAGE_SHIFT) > totalram_pages)
        goto fail;

    // start 和 end 分别为 VMALLOC_START, VMALLOC_END
    // 申请一块vmalloc区域
    area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED |
                vm_flags, start, end, node, gfp_mask, caller);
    if (!area)
        goto fail;

    // 申请物理空间并进行映射
    addr = __vmalloc_area_node(area, gfp_mask, prot, node);
    if (!addr)
        return NULL;

    return addr;

fail:
    warn_alloc_failed(gfp_mask, 0,
              "vmalloc: allocation failure: %lu bytes\n",
              real_size);
    return NULL;
}

3.2.1 查找可用的虚拟空间

  1. __get_vm_area_node

static 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;

    // 不能在中断中运行
    BUG_ON(in_interrupt());
    if (flags & VM_IOREMAP)
        align = 1ul << clamp_t(int, fls_long(size),
                       PAGE_SHIFT, IOREMAP_MAX_ORDER);

    // 按页对齐
    size = PAGE_ALIGN(size);
    if (unlikely(!size))
        return NULL;

    // 给 vm_struct 申请空间, 第1次调用申请函数
    area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);
    if (unlikely(!area))
        return NULL;

    // 空间加大一个物理页大小
    if (!(flags & VM_NO_GUARD))
        size += PAGE_SIZE;

    // 创建 vmap_area, 找到一段没有使用的vmalloc区间
    va = alloc_vmap_area(size, align, start, end, node, gfp_mask);
    if (IS_ERR(va)) {
        kfree(area);
        return NULL;
    }

    // 参数赋值到vm_struct
    setup_vmalloc_vm(area, va, flags, caller);

    return area;
}
  1. __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)
{
    struct vmap_area *va;
    struct rb_node *n;
    unsigned long addr;
    int purged = 0;
    struct vmap_area *first;

    // ...
    // 给 vmap_area 申请空间, 第2次调用申请函数
    va = kmalloc_node(sizeof(struct vmap_area),
            gfp_mask & GFP_RECLAIM_MASK, node);
    if (unlikely(!va))
        return ERR_PTR(-ENOMEM);
retry:
    spin_lock(&vmap_area_lock);

    if (!free_vmap_cache ||
            size < cached_hole_size ||
            vstart < cached_vstart ||
            align < cached_align) {
nocache:
        cached_hole_size = 0;
        free_vmap_cache = NULL;
    }
    /* record if we encounter less permissive parameters */
    cached_vstart = vstart;
    cached_align = align;

    /* find starting point for our search */
    /*
     * 上次添加或删除vmalloc区域会更新 free_vmap_cache
     * 一般认为虚拟内存是连续的,因此从上次操作完的位置开始方便快速找到内存区间
     *
     * 比如 现在存在区间 A[10,20]->B[20,30]->C[30,40]
     * 如果先删除区间B[20,30], 此时free_vmap_cache会指向区间A[10,20]
     * 下次在分配需要大小为5的空间就可以快速找到合适区间[20,5]
     */
    if (free_vmap_cache) {
        first = rb_entry(free_vmap_cache, struct vmap_area, rb_node);
        addr = ALIGN(first->va_end, align);
        if (addr < vstart)
            goto nocache;
        if (addr + size < addr)
            goto overflow;

    } else {
        addr = ALIGN(vstart, align);
        if (addr + size < addr)
            goto overflow;

        // 红黑树根结点
        n = vmap_area_root.rb_node;
        first = NULL;

        while (n) {
            // 遍历红黑树上的成员, 查找符合的区间
            struct vmap_area *tmp;
            tmp = rb_entry(n, struct vmap_area, rb_node);
            if (tmp->va_end >= addr) {
                first = tmp;
                if (tmp->va_start <= addr)
                // 找到 addr在[tmp->va_start, tmp->va_end]的区域
                    break;
                n = n->rb_left;
            } else
                n = n->rb_right;
        }
        // first为空, 说明没有找到, 直接跳到后面
        if (!first)
            goto found;
    }

    /* from the starting point, walk areas until a suitable hole is found */
    // while循环, 需要找到   addr在[first->end, firsr->next->start]的区间
    while (addr + size > first->va_start && addr + size <= vend) {
        if (addr + cached_hole_size < first->va_start)
            cached_hole_size = first->va_start - addr;
        addr = ALIGN(first->va_end, align);
        if (addr + size < addr)
            goto overflow;

        // 遍历到链表的结尾就跳出
        if (list_is_last(&first->list, &vmap_area_list))
            goto found;

        first = list_entry(first->list.next,
                struct vmap_area, list);
    }

found:
    if (addr + size > vend)
        goto overflow;

    /*
     * 走到这里有2种情况
     *   1 找到了空闲的区间
     *   2 没有找到空闲区间
     */
    va->va_start = addr;
    va->va_end = addr + size;
    va->flags = 0;
    // 添加到 vmap_area_root 红黑树中
    __insert_vmap_area(va);
    // 更新 free_vmap_cache, 方便下一次查找
    free_vmap_cache = &va->rb_node;
    spin_unlock(&vmap_area_lock);
    return va;

    // 内存溢出
overflow:
    spin_unlock(&vmap_area_lock);
    if (!purged) {
        // 回收已经不用的vmalloc区间
        purge_vmap_area_lazy();
        purged = 1;
        // 再次尝试
        goto retry;
    }
    // ...
    return ERR_PTR(-EBUSY);
}
  1. __get_vm_area_node->alloc_vmap_area->__insert_vmap_area
    该函数功能比较简单,就是把 vmap_area 插入到全局的红黑树,链接到全局的链表

static void __insert_vmap_area(struct vmap_area *va)
{
    // 全局红黑树
    struct rb_node **p = &vmap_area_root.rb_node;
    struct rb_node *parent = NULL;
    struct rb_node *tmp;

    while (*p) {
        struct vmap_area *tmp_va;
        // 以vmalloc区域起始地址大小来排序, 找到插入的位置
        parent = *p;
        tmp_va = rb_entry(parent, struct vmap_area, rb_node);
        if (va->va_start < tmp_va->va_end)
            p = &(*p)->rb_left;
        else if (va->va_end > tmp_va->va_start)
            p = &(*p)->rb_right;
        else
            BUG();
    }

    // 设置父结点
    rb_link_node(&va->rb_node, parent, p);
    // 添加到红黑树中
    rb_insert_color(&va->rb_node, &vmap_area_root);

    /* address-sort this list */
    /*
     * vmap_area_list 链表也是按照从小到大链接的
     * rb_prev 用来找到前项结点
     * 如果存在则连接到该节点后,如果不存在说明当前结点是最小的,直接链接到vmap_area_list的开头
     */
    tmp = rb_prev(&va->rb_node);
    if (tmp) {
        struct vmap_area *prev;
        prev = rb_entry(tmp, struct vmap_area, rb_node);
        list_add_rcu(&va->list, &prev->list);
    } else
        list_add_rcu(&va->list, &vmap_area_list);
}
  1. 回到 __get_vm_area_node->setup_vmalloc_vm
    前面通过alloc_vmap_area 已经找到了可用的vmalloc区间,这里只是简单的做赋值

static 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;
    va->flags |= VM_VM_AREA;
    spin_unlock(&vmap_area_lock);
}

3.2.2 申请物理内存并建立映射

  1. __vmalloc_area_node
    首先给vm_struct->pages 申请空间, 然后走伙伴系统申请物理页,并把物理对应的page赋值给pages[]

static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
                 pgprot_t prot, int node)
{
    const int order = 0;
    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;

    // 大小换换为页数
    nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;
    array_size = (nr_pages * sizeof(struct page *));

    area->nr_pages = nr_pages;
    /* Please note that the recursion is strictly bounded. */
    // 第3次调用申请函数
    if (array_size > PAGE_SIZE) {
        // 大于4K还是调用vmalloc
        pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
                PAGE_KERNEL, node, area->caller);
        area->flags |= VM_VPAGES;
    } else {
        // 小于4K走kmem_cache 申请内存
        pages = kmalloc_node(array_size, nested_gfp, node);
    }
    area->pages = pages;
    if (!area->pages) {
        remove_vm_area(area->addr);
        kfree(area);
        return NULL;
    }

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

        /*
         * 走vmalloc调用进来, 参数为NUMA_NO_NODE
         * 因为order=0, 因此每次申请了物理页大小为4K
         *
         */
        // 第4次调用申请函数
        if (node == NUMA_NO_NODE)
            page = alloc_page(alloc_mask);
        else
        page = alloc_pages_node(node, alloc_mask, order);

        if (unlikely(!page)) {
            /* Successfully allocated i pages, free them in __vunmap() */
            area->nr_pages = i;
            goto fail;
        }
        // 分多次走伙伴系统申请4K的物理页, 物理页地址可能不是连续的
        // 就是常说的虚拟地址空间连续, 但是物理地址空间不连续的原因
        area->pages[i] = page;
        if (gfpflags_allow_blocking(gfp_mask))
            cond_resched();
    }

    // 建立映射
    if (map_vm_area(area, prot, pages))
        goto fail;
    return area->addr;

fail:
    warn_alloc_failed(gfp_mask, order,
              "vmalloc: allocation failure, allocated %ld of %ld bytes\n",
              (area->nr_pages*PAGE_SIZE), area->size);
    vfree(area->addr);
    return NULL;
}
  1. __vmalloc_area_node->map_vm_area->vmap_page_range
    把申请到的vmalloc地址和对应的物理页做映射,配置到页表中

int 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;

}

static 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;

  1. vmap_page_range_noflush->vmap_pud_range->vmap_pmd_range->vmap_pte_range
    层层调用, 最后调用到 vmap_pte_range

static int vmap_pte_range(pmd_t *pmd, unsigned long addr,
        unsigned long end, pgprot_t prot, struct page **pages, int *nr)
{
    pte_t *pte;

    /*
     * nr is a running index into the array which helps higher level
     * callers keep track of where we're up to.
     */

    // 申请pte
    pte = pte_alloc_kernel(pmd, addr);
    if (!pte)
        return -ENOMEM;
    do {
        // page就是在__vmalloc_area_node中申请的4K物理页
        struct page *page = pages[*nr];

        if (WARN_ON(!pte_none(*pte)))
            return -EBUSY;
        if (WARN_ON(!page))
            return -ENOMEM;
        // 设置页表
        set_pte_at(&init_mm, addr, pte, mk_pte(page, prot));
        (*nr)++;
    } while (pte++, addr += PAGE_SIZE, addr != end);
    return 0;
}

在这里插入图片描述

3.3 vfree

调用vmalloc申请的内存需要使用vfree来释放,释放分为2部分
部分1 先释放申请的物理页,释放struct vm_struct->pages, 释放 struct vm_struct 在 __vunmap中完成
部分2 struct vmap_area 的释放在 try_purge_vmap_area_lazy 中完成

  1. vfree 主要调用__vunmap

void vfree(const void *addr)
{
    BUG_ON(in_nmi());

    kmemleak_free(addr);

    if (!addr)
        return;
    // 如果在中断中就需要延后释放
    if (unlikely(in_interrupt())) {
        struct vfree_deferred *p = this_cpu_ptr(&vfree_deferred);
        if (llist_add((struct llist_node *)addr, &p->list))
            schedule_work(&p->wq);
    } else
        __vunmap(addr, 1);
}

3.3.1 释放部分1

  1. __vunmap

static void __vunmap(const void *addr, int deallocate_pages)
{
    struct vm_struct *area;

    // ...
    // 通过地址从红黑树中找到对应的vmalloc区域
    // 清除页表
    area = remove_vm_area(addr);
    if (unlikely(!area)) {
        WARN(1, KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
                addr);
        return;
    }

    if (deallocate_pages) {
        int i;

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

            // 释放物理页
            BUG_ON(!page);
            __free_page(page);
        }

        // 释放 vm_struct->pages, 存放page的空间
        if (area->flags & VM_VPAGES)
            vfree(area->pages);
        else
            kfree(area->pages);
    }

    // 释放 vm_struct
    kfree(area);
    return;
}
  1. __vunmap->remove_vm_area

struct vm_struct *remove_vm_area(const void *addr)
{
    struct vmap_area *va;

    // 根据地址找到对于应的 vmap_area
    va = find_vmap_area((unsigned long)addr);
    if (va && va->flags & VM_VM_AREA) {
        struct vm_struct *vm = va->vm;

        spin_lock(&vmap_area_lock);
        va->vm = NULL;
        va->flags &= ~VM_VM_AREA;
        spin_unlock(&vmap_area_lock);

        vmap_debug_free_range(va->va_start, va->va_end);
        kasan_free_shadow(vm);
        // 清空物理地址与虚拟区间的映射
        free_unmap_vmap_area(va);

        return vm;
    }
    return NULL;
}
  1. free_unmap_vmap_area->free_unmap_vmap_area_noflush->free_vmap_area_noflush
    这里把要释放的 vmap_area设置了对应的标志位,但是还没有释放 vmap_area

static void free_vmap_area_noflush(struct vmap_area *va)
{
    // 待清除的区间标志位设置为  VM_LAZY_FREE
    va->flags |= VM_LAZY_FREE;
    atomic_add((va->va_end - va->va_start) >> PAGE_SHIFT, &vmap_lazy_nr);
    // 当数量超过 lazy_max_pages 才会调用 try_purge_vmap_area_lazy
    if (unlikely(atomic_read(&vmap_lazy_nr) > lazy_max_pages()))
        try_purge_vmap_area_lazy();
}

3.3.2 释放部分2

  1. try_purge_vmap_area_lazy
    需要删除的 vmap_area在free_vmap_area_noflush中会把flag配置为VM_LAZY_FREE
    会先把需要删除的 vmap_area链接到valist, 然后遍历链表valist, 调用__free_vmap_area 进行删除

static void __purge_vmap_area_lazy(unsigned long *start, unsigned long *end,
                    int sync, int force_flush)
{
    static DEFINE_SPINLOCK(purge_lock);
    LIST_HEAD(valist);
    struct vmap_area *va;
    struct vmap_area *n_va;
    int nr = 0;

    // ...
    rcu_read_lock();
    list_for_each_entry_rcu(va, &vmap_area_list, list) {
        // 需要释放的标志位才会配置为 VM_LAZY_FREE
        if (va->flags & VM_LAZY_FREE) {
            if (va->va_start < *start)
                *start = va->va_start;
            if (va->va_end > *end)
                *end = va->va_end;
            nr += (va->va_end - va->va_start) >> PAGE_SHIFT;
            // 要删除的vmap_area 临时添加到链表valist上
            list_add_tail(&va->purge_list, &valist);
            va->flags |= VM_LAZY_FREEING;
            va->flags &= ~VM_LAZY_FREE;
        }
    }
    rcu_read_unlock();

    if (nr)
        atomic_sub(nr, &vmap_lazy_nr);

    if (nr || force_flush)
        flush_tlb_kernel_range(*start, *end);

    if (nr) {
        spin_lock(&vmap_area_lock);
        // 遍历valist链表,删除上面的 vmap_area
        list_for_each_entry_safe(va, n_va, &valist, purge_list)
            __free_vmap_area(va);
        spin_unlock(&vmap_area_lock);
    }
    spin_unlock(&purge_lock);
}
  1. try_purge_vmap_area_lazy->__free_vmap_area
    把要删除的vmap_area 从红黑树和链表中移除, 最后释放 vmap_area

static void __free_vmap_area(struct vmap_area *va)
{
    BUG_ON(RB_EMPTY_NODE(&va->rb_node));

    if (free_vmap_cache) {
        if (va->va_end < cached_vstart) {
            free_vmap_cache = NULL;
        } else {
            struct vmap_area *cache;
            cache = rb_entry(free_vmap_cache, struct vmap_area, rb_node);
            if (va->va_start <= cache->va_start) {
                // 更新 free_vmap_cache, 方便下次快速查找区间
                free_vmap_cache = rb_prev(&va->rb_node);
                /*
                 * We don't try to update cached_hole_size or
                 * cached_align, but it won't go very wrong.
                 */
            }
        }
    }
    // 从红黑树中删除结点
    rb_erase(&va->rb_node, &vmap_area_root);
    RB_CLEAR_NODE(&va->rb_node);
    // 从全局链表 vmap_area_list 中删除结点
    list_del_rcu(&va->list);

     */
    if (va->va_end > VMALLOC_START && va->va_end <= VMALLOC_END)
        vmap_area_pcpu_hole = max(vmap_area_pcpu_hole, va->va_end);

    // 最后释放 vmap_area
    kfree_rcu(va, rcu_head);
}
  1. 一次删除的函数调用情况
    在这里插入图片描述

4 总结

  1. 申请内存部分
    1.1)会从vmalloc中找到一段空闲的区间,地址上会是连续的
    1.2)地址信息保存到 struct vmap_area 和 struct vm_struct 中, 把 vmap_area 按照起始地址从小到大添加到红黑树和全局链表中
    1.3)向伙伴系统申请物理页, 这里的物理页都是4K组成,物理页的地址可能不连续
    1.4) 把vmalloc中申请的空间和申请的物理页做映射关系

  2. 释放内存
    2.1)会先把 vm_struct , 物理页,vm_struct->pages全部释放
    2.2)vmap_area的释放会做延后处理,当vmalloc剩余空间不够或需要释放的地址大小超过一定的数值才会调用__purge_vmap_area_lazy进行最后的释放

  3. 因为 vmap_area的延后释放,导致全局链表和红黑树上的信息也存在,当连续调用vmalloc()、vfree() 申请释放内存, 得到的虚拟地址是递增的。

测试demo, 每次申请1Mx5次, 发现每次地址是递增的,当达到一定数量后才会释放

在这里插入图片描述

测试demo源码

#include <linux/device.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>


#include "my_error.h"

/* 代码思路
    测试vmalloc
*/
#define VMALLOC_TEST_NUM (5)
#define VMALLOC_TEST_SIZE (1024 * 1024)
static void *p[VMALLOC_TEST_NUM];

static int vmalloc_test_init(void)
{
    int i;

    PRINT_INFO("size:%d, array_num:%d \n", VMALLOC_TEST_SIZE, VMALLOC_TEST_NUM);

    for (i = 0; i < VMALLOC_TEST_NUM; i++)
    {
        p[i] = vmalloc(VMALLOC_TEST_SIZE);
        if (!p[i])
        {
            PRINT_ERR("i:%d, vmalloc fail \n", i);
            goto OUT;
        }
    }


    PRINT_INFO("%s init \n", __FUNCTION__);

    return 0;
OUT:
    for (; i > 0; i--)
    {
        vfree(p[i]);
    }

    return -1;
}

static void vmalloc_test_exit(void)
{
    int i;

    for (i = 0; i < VMALLOC_TEST_NUM; i++)
    {
        vfree(p[i]);
    }

    PRINT_INFO("%s exit \n", __FUNCTION__);
}

module_init(vmalloc_test_init);
module_exit(vmalloc_test_exit);

MODULE_LICENSE("GPL");

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多