• Linux内存管理 (6)vmalloc


    专题:Linux内存管理专题

    关键词:vmalloc、页对齐、虚拟地址连续、物理不连续

    至此,已经介绍了集中内核中内存分配函数,在开始简单做个对比总结Linux中常用内存分配函数的异同点,然后重点介绍了vmalloc相关的hole查找,页面分配等等。

    vmalloc的核心是在vmalloc区域中找到合适的hole,hole是虚拟地址连续的;然后逐页分配内存来从物理上填充hole。

    vmalloc的gfp_maks和逐页分配就决定了它的属性:可能睡眠、虚拟地址连续、物理地址不连续、size对齐到页;所以不适合小内存分配,开销较大。

    1. Linux中常用内存分配函数的异同点

    用户/内核 API名称 物理连续? 大小限制 单位 场景
    用户空间 malloc/calloc/realloc/free  不保证  堆申请  字节 calloc初始化为0;realloc改变内存大小。
    alloca    栈申请  字节 向栈申请内存
    mmap/munmap       将文件利用虚拟内存技术映射到内存中去。
    brk、sbrk        虚拟内存到内存的映射。sbrk(0)返回program break地址,sbrk调整对的大小。

    间    

      vmalloc/vfree

    虚拟连续

    物理不定

     vmalloc区大小限制

     页

    VMALLOC区域

    可能睡眠,不能从中断上下文中调用,或其他不允许阻塞情况下调用。

    VMALLOC区域vmalloc_start~vmalloc_end之间,vmalloc比kmalloc慢,适用于分配大内存。

      slab kmalloc/kcalloc/krealloc/kfree 物理连续

    64B-4MB

    (随slab而变)

     2^order字节

    Normal区域

    大小有限,不如vmalloc/malloc大。

    最大/小值由KMALLOC_MIN_SIZE/KMALLOC_SHIFT_MAX,对应64B/4MB。

    从/proc/slabinfo中的kmalloc-xxxx中分配,建立在kmem_cache_create基础之上。

    kmem_cache_create 物理连续 64B-4MB

    字节大小,需对齐

    Normal区域

    便于固定大小数据的频繁分配和释放,分配时从缓存池中获取地址,释放时也不一定真正释放内存。通过slab进行管理。

    伙伴系统  __get_free_page/__get_free_pages 物理连续  4MB(1024页)

    Normal区域

     __get_free_pages基于alloc_pages,但是限定不能使用HIGHMEM。
     alloc_page/alloc_pages/free_pages 物理连续 4MB 

    Normal/Vmalloc都可 

     CONFIG_FORCE_MAX_ZONEORDER定义了最大页面数2^11,一次能分配到的最大页面数是1024。

    2.1 vmalloc

    2.1 重要数据结构

    在进行vmalloc代码走读之前,先简单看一下两个重要的数据结构:struct vm_struct(vmalloc描述符)和struct vmap_area(记录在vmap_area_root中的vmalooc分配情况和vmap_area_list列表中)。

    struct vm_struct {
        struct vm_struct    *next;----------下一个vm。
        void            *addr;--------------指向第一个内存单元虚拟地址
        unsigned long        size;----------该内存区对应的大小
        unsigned long        flags;---------vm标志位,如下。
        struct page        **pages;---------指向页面没描述符的指针数组
        unsigned int        nr_pages;-------vmalloc映射的page数目
        phys_addr_t        phys_addr;-------用来映射硬件设备的IO共享内存,其他情况下为0
        const void        *caller;----------调用vmalloc类函数的返回地址
    };

    其中VM_NO_GUARD表示不需要多分配一页来做安全垫。

    /* 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 */

     vmap_area表示内核空间的vmalloc区域的一个vmalloc,由rb_node和list进行串联。

    struct vmap_area {
        unsigned long va_start;--------------malloc区的起始地址
        unsigned long va_end;----------------malloc区的结束地址
        unsigned long flags;-----------------类型标识
        struct rb_node rb_node;         /* address sorted rbtree */----按地址的红黑树
        struct list_head list;          /* address sorted list */------按地址的列表
        struct list_head purge_list;    /* "lazy purge" list */
        struct vm_struct *vm;------------------------------------------指向配对的vm_struct
        struct rcu_head rcu_head;
    };

    2.2 函数走查

    vmalloc用于分配虚拟地址连续的内存空间,vzmalloc相对于vmalloc多了个0初始化。

    同时vmalloc/vzmalloc分配的虚拟地址范围在VMALLOC_START/VMALLOC_END之间。

    void *vmalloc(unsigned long size)
    {
        return __vmalloc_node_flags(size, NUMA_NO_NODE,
                        GFP_KERNEL | __GFP_HIGHMEM);
    }
    
    void *vzalloc(unsigned long size)
    {
        return __vmalloc_node_flags(size, NUMA_NO_NODE,
                    GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);
    }
    
    static 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的主要工作是找到符合大小要求的空闲vmalloc区域的hole;分配页面,并创建页表映射关系。

    下面是__vmalloc_node_range的主要子函数,反映了其主要工作内容。

    __vmalloc_node_range----------------vmalloc的核心函数
        __get_vm_area_node--------------找到符合大小的空闲vmalloc区域
            alloc_vmap_area-------------从vmap_area_root中找到合适的hole,填充vmap_area结构体,并插入到vmap_area_root红黑树中
            setup_vmalloc_vm------------将vmap_area的参数填入vm_struct
        __vmalloc_area_node-------------计算需要的页面数,分配页面,并创建页表映射关系
            alloc_page------------------分配页面
            map_vm_area-----------------建立PGD/PTE页表映射关系

     __vmalloc_node_range是vmalloc的核心函数: 

    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);----------------------------------------对地址进行了页对齐,哪怕分配10B大小也分配一页的空间。所以适合大内存分配。
        if (!size || (size >> PAGE_SHIFT) > totalram_pages)-------------对size大小进行判断,大于0小于总page数
            goto fail;
    
        area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED |
                    vm_flags, start, end, node, gfp_mask, caller);------申请并填充vm_struct结构体。
        if (!area)
            goto fail;
    
        addr = __vmalloc_area_node(area, gfp_mask, prot, node);---------分配内存,建立页面映射关系。
        if (!addr)
            return NULL;
    ...
        /*
         * A ref_count = 2 is needed because vm_struct allocated in
         * __get_vm_area_node() contains a reference to the virtual address of
         * the vmalloc'ed block.
         */
        kmemleak_alloc(addr, real_size, 2, gfp_mask);-------------------kmemleak记录分配信息
    
        return addr;----------------------------------------------------最后返回vmalloc分配区域的首地址
    ...
    }

     __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());------------------------------------------------------vmalloc不能中在中断中被调用
        if (flags & VM_IOREMAP)
            align = 1ul << clamp(fls(size), PAGE_SHIFT, IOREMAP_MAX_ORDER);
    
        size = PAGE_ALIGN(size);-----------------------------------------------------页对齐操作
        if (unlikely(!size))
            return NULL;
    
        area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);-------分配一个struct vm_struct来描述vmalloc区域
        if (unlikely(!area))
            return NULL;
    
        if (!(flags & VM_NO_GUARD))
            size += PAGE_SIZE;-------------------------------------------------------加一页作为安全区间
    
        va = alloc_vmap_area(size, align, start, end, node, gfp_mask);---------------申请一个vmap_area并将其插入vmap_area_root中。
        if (IS_ERR(va)) {
            kfree(area);
            return NULL;
        }
    
        setup_vmalloc_vm(area, va, flags, caller);-----------------------------------填充vmalloc描述符vm_struct area。
    
        return area;
    }

     alloc_vmap_area在整个vmalloc空间中查找一块大小合适并且每人使用的空间,即hole。空间范围是VMALLOC_START~VMALLOC_END。

    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;
    
        BUG_ON(!size);
        BUG_ON(size & ~PAGE_MASK);
        BUG_ON(!is_power_of_2(align));
    
        va = kmalloc_node(sizeof(struct vmap_area),--------------------分配一个vmap_area结构体
                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:
        spin_lock(&vmap_area_lock);
        /*
         * Invalidate cache if we have more permissive parameters.
         * cached_hole_size notes the largest hole noticed _below_
         * the vmap_area cached in free_vmap_cache: if size fits
         * into that hole, we want to scan from vstart to reuse
         * the hole instead of allocating above free_vmap_cache.
         * Note that __free_vmap_area may update free_vmap_cache
         * without updating cached_hole_size or cached_align.
         */
        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 */
        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;--------------------------------------------vmap_area_root存放系统中正在使用的vmalloc块,为vmap_area结构。
            first = NULL;
    
            while (n) {------------------------------------------------------------遍历vmap_area_root左子叶节点找区间地址最小的区块。
                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)
                        break;-----------------------------------------------------此时tmp->va_start<=addr<=tmp->va_end,找到起始地址最小的vmalloc区块。
                    n = n->rb_left;
                } else
                    n = n->rb_right;
            }
    
            if (!first)
                goto found;
        }
    
        /* from the starting point, walk areas until a suitable hole is found */
        while (addr + size > first->va_start && addr + size <= vend) {-------------判断申请空间addr+size的合法性。
            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,------------------------------------检查下一个hole是否满足
                    struct vmap_area, list);
        }
    
    found:
        if (addr + size > vend)
            goto overflow;
    
        va->va_start = addr;
        va->va_end = addr + size;
        va->flags = 0;
        __insert_vmap_area(va);----------------------------------------------------将找到的新区块插入到vmap_area_root中
        free_vmap_cache = &va->rb_node;
        spin_unlock(&vmap_area_lock);
    
        BUG_ON(va->va_start & (align-1));
        BUG_ON(va->va_start < vstart);
        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 (printk_ratelimit())
            pr_warn("vmap allocation for size %lu failed: "
                "use vmalloc=<size> to increase size.
    ", size);
        kfree(va);
        return ERR_PTR(-EBUSY);
    }

     setup_vmalloc_vm主要用来设置vm_struct,同时将vm_struct和vmap_area关联。

    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);
    }

    至此,已经在vmalloc找到合适大小的hole,并且将其插入到vmap_area_root中,更行了vmalloc描述符vm_struct。

    __vmalloc_area_node则进行实际的页面分配,并建立页表映射,更新页表cache。

    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;---------------------------------计算vmalloc描述符vm_struct->size需要多少页
        array_size = (nr_pages * sizeof(struct page *));
    
        area->nr_pages = nr_pages;
        /* Please note that the recursion is strictly bounded. */
        if (array_size > PAGE_SIZE) {----------------------------------------------------分配也表指针数组需要的空间
            pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
                    PAGE_KERNEL, node, area->caller);
            area->flags |= VM_VPAGES;
        } else {
            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++) {------------------------------------------逐页分配页框,这里也可以看出对vmalloc是无法保证屋里连续的,页不是一起分配,而是一页一页分配的。
            struct page *page;
    
            if (node == NUMA_NO_NODE)
                page = alloc_page(alloc_mask);-----------------------------------------alloc_mask为GFP_KERNEL|__GFP_HIGHMEM|__GFP_NOWARN,所以优先在vmalloc区域,允许睡眠。
            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;
            }
            area->pages[i] = page;
            if (gfp_mask & __GFP_WAIT)
                cond_resched();
        }
    
        if (map_vm_area(area, prot, pages))----------------------------------------------建议vmalloc区域的页面映射关系
            goto fail;
        return area->addr;
    
    fail:
        warn_alloc_failed(gfp_mask, order,
                  "vmalloc: allocation failure, allocated %ld of %ld bytes
    ",
                  (area->nr_pages*PAGE_SIZE), area->size);
        vfree(area->addr);
        return NULL;
    }

     map_vm_area对分配的页面进行了映射,map_vm_area-->vmap_page_range-->vmap_page_range_noflush。

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

     vmap_page_range_noflush建立了映射关系,但是没有刷新缓存。

    static 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);--------------------------------得到地址区域对应的PGD地址
        do {-----------------------------------------------------遍历地址空间中的所有对应PGD;如果end和start在同一PGD区域,则只需要一次。
            next = pgd_addr_end(addr, end);----------------------addr和end在同一PGD的话,next即为end;否则为addr下一个PGD对应起始地址。
            err = vmap_pud_range(pgd, addr, next, prot, pages, &nr);
            if (err)
                return err;
        } while (pgd++, addr = next, addr != end);
    
        return nr;
    }
    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_alloc_kernel(pmd, addr);-------------------------------定位于addr对应的页表项
        if (!pte)
            return -ENOMEM;
        do {
            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;
    }

    flush_cache_vmap则进行了相关操作:

    static inline void flush_cache_vmap(unsigned long start, unsigned long end)
    {
        if (!cache_is_vipt_nonaliasing())
            flush_cache_all();
        else
            /*
             * set_pte_at() called from vmap_pte_range() does not
             * have a DSB after cleaning the cache line.
             */
            dsb(ishst);
    }
  • 相关阅读:
    如何获得浏览器localStorage的剩余容量
    按Enter键后Form表单自动提交的问题
    IE10 11的css hack
    text-transform设置单词首字母大写
    Jade模板引擎(一)之Attributes
    sql server之ROW_NUMBER() OVER()取每组的第N行数据
    CSS3之让背景图片全部显示
    摆脱npm的网络问题: 淘宝npm镜像
    MaskedTextBox的聚焦和光标位置
    将博客搬至CSDN
  • 原文地址:https://www.cnblogs.com/arnoldlu/p/8251333.html
Copyright © 2020-2023  润新知