kernel内存分配中的vmalloc

在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法.

Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述.

伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者.

  • 内核如何记住哪些内存块是空闲的
  • 分配空闲页面的方法
  • 影响分配器行为的众多标识位
  • 内存碎片的问题和分配器如何处理碎片

    当buddy系统还有大量的连续物理内存时,我们可以通过__pages_alloc成功分配很大的一块连续物理内存空间,随着系统运行时间加长,buddy系统内很难中找到一块大的连续物理内存空间,因此__pages_alloc可能会失败,即便通过kswapd进行页面的回收和交换,buddy仍然不可避免的碎片化

首先我们要明确的是,连续物理内存的分配并不是必要的。对于大部分DMA操作,我们的确需要连续的物理内存;但是对于某些分配内存情况:比如,模块加载,设备和声音驱动程序中,可以在内核源码中关键字vmalloc查找,对vmalloc的使用有个感性认识。

vmalloc把buddy系统内的不连续物理内存,映射到内核中一段连续的地址空间内,因此对于那些无法直接映射的高端物理内存Highmem来说,vmalloc是主要用途之一。因此vmalloc理应优先使用廉价的Highmem内存,而把宝贵的低端内存,留给其他的内核操作。事实上也是如此,vmalloc实现函数的分配标志,指明了从Highmem分配
vmalloc是一个接口函数, 内核代码使用它来分配在虚拟内存中连续但在物理内存中不一定连续的内存


/**
 *  vmalloc  -  allocate virtually contiguous memory
 *  @size:      allocation size
 *  Allocate enough pages to cover @size from the page level
 *  allocator and map them into contiguous kernel virtual space.
 *
 *  For tight control over page level allocator and protection flags
 *  use __vmalloc() instead.
 */
void *vmalloc(unsigned long size)
{
    return __vmalloc_node_flags(size, NUMA_NO_NODE,
                    GFP_KERNEL | __GFP_HIGHMEM);
}

对于vmalloc来说是需要预留一定的地址空间的。而DMA和Normal内存zone 又需要占用数百M的地址空间,参见下面这个经典的kernel地址空间划分图
kernel内存分配中的vmalloc_第1张图片

Persistent mappings和Fixmaps地址空间都比较小,这里我们忽略它们,这样只剩下直接地址映射和VMALLOC区,这个划分应该是平衡两个需求的结果

  1. 尽量增加DMA和Normal区大小,也就是直接映射地址空间大小,当前主流平台的内存,基本上都超过了512MB,很多都是标配1GB内存,因此注定有一部分内存无法进行线性映射。

  2. 保留一定数量的VMALLOC大小,这个值是应用平台特定的,如果应用平台某个驱动需要用vmalloc分配很大的地址空间,那么最好通过在kernel参数中指定vmalloc大小的方法,预留较多的vmalloc地址空间。

  3. 并不是Highmem没有或者越少越好,这个是我的个人理解,理由如下:高端内存就像个垃圾桶和缓冲区,防止来自用户空间或者vmalloc的映射破坏Normal zone和DMA zone的连续性,使得它们碎片化。当这个垃圾桶较大时,那么污染Normal 和DMA的机会自然就小了。

下面的图是VMALLOC地址空间内部划分情况
kernel内存分配中的vmalloc_第2张图片

在直接地址映射和VMALLOC区之间有一个8MiB的隔离带,隔离带是做什么的呢? 隔离带是用来针对内核故障的保护措施,当访问虚拟地址越界时,则会产生一个page fault异常,也就是说这个内核地址空间没有对应相应的物理地址,这在内核地址空间是不允许的。如果不存在隔离带,那么越界访问不知不觉的跨越直接映射和VMALLOC区,内核却没注意到这个错误。
在VMALLOC内部,会划分为多个vmalloc_area,每个vmalloc_area直间有一个4KB的地址空隙,通过这个小的隔离,可以防止不同映射区直接的越界访问。

数据结构

内核在管理虚拟内存中的vmalloc区域时, 内核必须跟踪哪些子区域被使用、哪些是空闲的. 为此定义了一个数据结构vm_struct, 将所有使用的部分保存在一个链表中. 该结构提的定义在include/linux/vmalloc.h


struct vm_struct {
    struct vm_struct    *next;//所有的vm_struct通过next 组成一个单链表,表头为全局变量vmlist
    void            *addr;//定义了这个虚拟地址空间子区域的起始地址
    unsigned long       size; //定义了这个虚拟地址空间子区域的大小
    unsigned long       flags; //存储了与该内存区关联的标志
    struct page     **pages; //是一个指针,指向page指针的数组,每个数组成员都表示一个映射到这个地址空间的物理页面的实例
    unsigned int        nr_pages; //page指针数据的长度
    phys_addr_t     phys_addr;  //仅当用ioremap映射了由物理地址描述的物理内存区域才有效。
    const void      *caller;
};

其中flags只用于指定内存区类型, 所有可能的flag标识以宏的形式定义在include/linux/vmalloc.h

/* bits in flags of vmalloc's vm_struct below */
#define VM_IOREMAP      0x00000001  /* ioremap() and friends *///表示将几乎随机的物理内存区域映射到vmalloc区域中. 这是一个特定于体系结构的操作
#define VM_ALLOC        0x00000002  /* vmalloc() *///指定由vmalloc产生的子区域
#define VM_MAP          0x00000004  /* vmap()ed pages *///用于表示将现存pages集合映射到连续的虚拟地址空间中
#define VM_USERMAP      0x00000008  /* suitable for remap_vmalloc_range */
#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 */

下图给出了该结构使用方式的一个实例. 其中依次映射了3个(假想的)物理内存页, 在物理内存中的位置分别是1 023、725和7 311. 在虚拟的vmalloc区域中, 内核将其看作起始于VMALLOC_START + 100的一个连续内存区, 大小为3*PAGE_SIZE的内核地址空间,被映射到物理页面725, 1023和7311
kernel内存分配中的vmalloc_第3张图片

因为大部分体系结构都支持mmu,这里我们只考虑有mmu的情况。实际上没有mmu支持时,vmalloc就无法实现非连续物理地址到连续内核地址空间的映射,vmalloc退化为kmalloc实现

在创建一个新的虚拟内存区之前, 必须找到一个适当的位置. vm_area实例组成的一个链表, 管理着vmalloc区域中已经建立的各个子区域. 定义在mm/vmalloc的全局变量vmlist是表头. 定义在mm/vmalloc.c


static struct vm_struct *vmlist __initdata;

内核在mm/vmalloc中提供了辅助函数get_vm_area__get_vm_area, 它们负责参数准备工作, 而实际的分配工作交给底层函数__get_vm_area_node来完成, 这些函数定义在mm/vmalloc.c


struct vm_struct *__get_vm_area(unsigned long size, unsigned long flags,
                unsigned long start, unsigned long end)
{
    return __get_vm_area_node(size, 1, flags, start, end, NUMA_NO_NODE,
                  GFP_KERNEL, __builtin_return_address(0));
} 
EXPORT_SYMBOL_GPL(__get_vm_area);

struct vm_struct *__get_vm_area_caller(unsigned long size, unsigned long flags,
                       unsigned long start, unsigned long end,
                       const void *caller)
{
    return __get_vm_area_node(size, 1, flags, start, end, NUMA_NO_NODE,
                  GFP_KERNEL, caller);
} 

/**
 *  get_vm_area  -  reserve a contiguous kernel virtual area
 *  @size:      size of the area
 *  @flags:     %VM_IOREMAP for I/O mappings or VM_ALLOC
 *
 *  Search an area of @size in the kernel virtual mapping area,
 *  and reserved it for out purposes.  Returns the area descriptor
 *  on success or %NULL on failure.
 */
struct vm_struct *get_vm_area(unsigned long size, unsigned long flags)
{
    return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END,
                  NUMA_NO_NODE, GFP_KERNEL,
                  __builtin_return_address(0));
}

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

这些函数是负责实际工作的__get_vm_area_node函数的前端. 根据子区域的长度信息, __get_vm_area_node函数试图在虚拟的vmalloc空间中找到一个适当的位置. 该函数定义在mm/vmalloc.c


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;

    area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);
    if (unlikely(!area))
        return NULL;

    if (!(flags & VM_NO_GUARD))
        size += PAGE_SIZE;

    va = alloc_vmap_area(size, align, start, end, node, gfp_mask);
    if (IS_ERR(va)) {
        kfree(area);
        return NULL;
    }

    setup_vmalloc_vm(area, va, flags, caller);

    return area;
}

由于各个vmalloc子区域之间需要插入1页(警戒页)作为安全隙, 内核首先适当提高需要分配的内存长度。start和end参数分别由调用者设置, 比如get_vm_area函数和get_vm_area_caller函数传入VMALLOC_STARTVMALLOC_END. 接下来循环遍历vmlist的所有表元素,直至找到一个适当的项

remove_vm_area函数将一个现存的子区域从vmalloc地址空间删除。函数定义在mm/vmalloc.c


/**
 *  remove_vm_area  -  find and remove a continuous kernel virtual area
 *  @addr:      base address
 *
 *  Search for the kernel VM area starting at @addr, and remove it.
 *  This function returns the found VM area, but using it is NOT safe
 *  on SMP machines, except for its size or flags.
 */
struct vm_struct *remove_vm_area(const void *addr)
{
    struct vmap_area *va;

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

vmalloc发起对不连续的内存区的分配操作。该函数只是一个前端,为__vmalloc提供适当的参数,后者直接调用__vmalloc_node.
vmalloc只是__vmalloc_node_flags的前端接口,复杂向__vmalloc_node_flags传递数据, 而__vmalloc_node_flags又是__vmalloc_node的前端接口,而后者又将实际的工作交给__vmalloc_node_range函数来完成
vmalloc函数定义在mm/vmalloc.c, 将实际的工作交给__vmalloc_node_flags函数来完成.


/**
 *  vmalloc  -  allocate virtually contiguous memory
 *  @size:      allocation size
 *  Allocate enough pages to cover @size from the page level
 *  allocator and map them into contiguous kernel virtual space.
 *
 *  For tight control over page level allocator and protection flags
 *  use __vmalloc() instead.
 */
void *vmalloc(unsigned long size)
{
    return __vmalloc_node_flags(size, NUMA_NO_NODE,
                    GFP_KERNEL | __GFP_HIGHMEM);
}
EXPORT_SYMBOL(vmalloc);

__vmalloc_node_flags 调用了__vmalloc_node 函数:


static 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函数继续调用__vmalloc_node_range函数:


/**
 *  __vmalloc_node  -  allocate virtually contiguous memory
 *  @size:      allocation size
 *  @align:     desired alignment
 *  @gfp_mask:  flags for the page level allocator
 *  @prot:      protection mask for the allocated pages
 *  @node:      node to use for allocation or NUMA_NO_NODE
 *  @caller:    caller's return address
 *
 *  Allocate enough pages to cover @size from the page level
 *  allocator with @gfp_mask flags.  Map them into contiguous
 *  kernel virtual space, using a pagetable protection of @prot.
 */
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_node_range  -  allocate virtually contiguous memory
 *  @size:      allocation size
 *  @align:     desired alignment
 *  @start:     vm area range start
 *  @end:       vm area range end
 *  @gfp_mask:  flags for the page level allocator
 *  @prot:      protection mask for the allocated pages
 *  @vm_flags:  additional vm area flags (e.g. %VM_NO_GUARD)
 *  @node:      node to use for allocation or NUMA_NO_NODE
 *  @caller:    caller's return address
 *
 *  Allocate enough pages to cover @size from the page level
 *  allocator with @gfp_mask flags.  Map them into contiguous
 *  kernel virtual space, using a pagetable protection of @prot.
 */
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;

    //从vmalloc区中的子区域链表vmlist(全局变量)中获取空闲的子区域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;

    //分配物理内存,并通过修改内核页表建立物理地址和虚拟地址间的映射,返回值为vmalloc区的虚拟地址
    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);

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

    return addr;

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

其中调用了__vmalloc_area_node:

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. */
    if (array_size > PAGE_SIZE) { 
     /*如果区域大小大于1页,则进行迭代,注意此处传入的内存分配标记中有__GFP_HIGHMEM,表示优先从高端内存中分配,这也是vmalloc的用处所在。*/
        pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
                PAGE_KERNEL, node, area->caller);
    } else {
        // 迭代直到分配的区域大小小于1页,则使用kmalloc分配
        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;

        if (node == NUMA_NO_NODE) 
            // 分配实际的物理页
            page = alloc_kmem_pages(alloc_mask, order);
        else
            page = alloc_kmem_pages_node(node, alloc_mask, order);

        if (unlikely(!page)) {
            /* Successfully allocated i pages, free them in __vunmap() */
            area->nr_pages = i;
            goto fail;
        }
        //将分配的page填入vm_struct结构中
        area->pages[i] = page;
        if (gfpflags_allow_blocking(gfp_mask))
            cond_resched();
    }

    //将新分配的区域进行映射,即修改内核页表(进程页表在page fault中更新),建立虚拟地址到物理地址间的映射。
    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;
}

有两个函数用于向内核释放内存, vfree用于释放vmallocvmalloc_32分配的区域,而vunmap用于释放由vmap或ioremap创建的映射。这两个函数都会归结到__vunmap

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

    area = remove_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(addr, get_vm_area_size(area));
    debug_check_no_obj_freed(addr, get_vm_area_size(area));

    if (deallocate_pages) {
        int i;

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

            BUG_ON(!page);
            __free_kmem_pages(page, 0);
        }

        kvfree(area->pages);
    }

    kfree(area);
    return;
}

其中参数addr表示要释放的区域的起始地址, deallocate_pages指定了是否将与该区域相关的物理内存页返回给伙伴系统。 vfree将后一个参数设置为1, 而vunmap设置为0, 因为在这种情况下只删除映射, 而不将相关的物理内存页返回给伙伴系统.
不必明确给出需要释放的区域长度, 长度可以从vmlist中的信息导出. 因此__vunmap的第一个任务是在__remove_vm_area(由remove_vm_area在完成锁定之后调用)中扫描该链表, 以找到 相关项。
unmap_vm_area使用找到的vm_area实例,从页表删除不再需要的项。与分配内存时类似,该函
数需要操作各级页表,但这一次需要删除涉及的项。它还会更新CPU高速缓存。
如果__vunmap的参数deallocate_pages设置为1(在vfree中),内核会遍历area->pages的所
有元素,即指向所涉及的物理内存页的page实例的指针。然后对每一项调用__free_page,将页释放
到伙伴系统。
最后,必须释放用于管理该内存区的内核数据结构。

你可能感兴趣的:(kernel)