高端内存映射之vmalloc分配内存中不连续的页--Linux内存管理(十九)

1 内存中不连续的页的分配

根据上文的讲述, 咱们知道物理上连续的映射对内核是最好的, 但并不总能成功地使用. 在分配一大块内存时, 可能不遗余力也没法找到连续的内存块.前端

在用户空间中这不是问题,由于普通进程设计为使用处理器的分页机制, 固然这会下降速度并占用TLB.node

在内核中也可使用一样的技术. 内核分配了其内核虚拟地址空间的一部分, 用于创建连续映射.linux

在IA-32系统中, 前16M划分给DMA区域, 后面一直到第896M做为NORMAL直接映射区, 紧随直接映射的前896MB物理内存,在插入的8MB安全隙以后, 是一个用于管理不连续内存的区域. 这一段具备线性地址空间的全部性质. 分配到其中的页可能位于物理内存中的任何地方. 经过修改负责该区域的内核页表, 便可作到这一点.c#

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地址空间内部划分状况app

2 用vmalloc分配内存

vmalloc是一个接口函数, 内核代码使用它来分配在虚拟内存中连续但在物理内存中不必定连续的内存electron

//  http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L70
void *vmalloc(unsigned long size);

该函数只须要一个参数, 用于指定所需内存区的长度, 与此前讨论的函数不一样, 其长度单位不是页而是字节, 这在用户空间程序设计中是很广泛的.

使用vmalloc的最著名的实例是内核对模块的实现. 由于模块可能在任什么时候候加载, 若是模块数据比较多, 那么没法保证有足够的连续内存可用, 特别是在系统已经运行了比较长时间的状况下.

若是可以用小块内存拼接出足够的内存, 那么使用vmalloc能够规避该问题

内核中还有大约400处地方调用了vmalloc, 特别是在设备和声音驱动程序中.

由于用于vmalloc的内存页老是必须映射在内核地址空间中, 所以使用ZONE_HIGHMEM内存域的页要优于其余内存域. 这使得内核能够节省更宝贵的较低端内存域, 而又不会带来额外的坏处. 所以, vmalloc等映射函数是内核出于自身的目的(并不是由于用户空间应用程序)使用高端内存页的少数情形之一.

全部有关vmalloc的数据结构和API结构声明在include/linux/vmalloc.h

声明头文件 NON-MMU实现 MMU实现
include/linux/vmalloc.h mm/nommu.c mm/vmalloc.c

2.1 数据结构

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

// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L32
struct vm_struct {
    struct vm_struct    *next;
    void            *addr;
    unsigned long       size;
    unsigned long       flags;
    struct page         **pages;
    unsigned int        nr_pages;
    phys_addr_t         phys_addr;
    const void          *caller;
};

注意, 内核使用了一个重要的数据结构称之为vm_area_struct, 以管理用户空间进程的虚拟地址空间内容. 尽管名称和目的都是相似的, 虽然两者都是作虚拟地址空间映射的, 但不能混淆这两个结构。

  1. 前者是内核虚拟地址空间映射,然后者则是应用进程虚拟地址空间映射。
  2. 前者不会产生page fault,然后者通常不会提早分配页面,只有当访问的时候,产生page fault来分配页面。

对于每一个用vmalloc分配的子区域, 都对应于内核内存中的一个该结构实例. 该结构各个成员的语义以下

字段 描述
next 使得内核能够将vmalloc区域中的全部子区域保存在一个单链表上
addr 定义了分配的子区域在虚拟地址空间中的起始地址。size表示该子区域的长度. 能够根据该信息来勾画出vmalloc区域的完整分配方案
flags 存储了与该内存区关联的标志集合, 这几乎是不可避免的. 它只用于指定内存区类型
pages 是一个指针,指向page指针的数组。每一个数组成员都表示一个映射到虚拟地址空间中的物理内存页的page实例
nr_pages 指定pages中数组项的数目,即涉及的内存页数目
phys_addr 仅当用ioremap映射了由物理地址描述的物理内存区域时才须要。该信息保存在phys_addr中
caller

其中flags只用于指定内存区类型, 全部可能的flag标识以宏的形式定义在include/linux/vmalloc.h?v=4.7, line 14

//  http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L14
/* 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_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 */
flag标识 描述
VM_IOREMAP 表示将几乎随机的物理内存区域映射到vmalloc区域中. 这是一个特定于体系结构的操做
VM_ALLOC 指定由vmalloc产生的子区域

VM_MAP 用于表示将现存pages集合映射到连续的虚拟地址空间中
VM_USERMAP |
VM_UNINITIALIZED|
VM_NO_GUARD |
VM_KASAN|

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

2.2 建立vm_area

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

2.2.1 vmlist全局链表

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

// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1170
static struct vm_struct *vmlist __initdata;

2.2.2 分配函数

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

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?v=4.7, line 1354

因为各个vmalloc子区域之间须要插入1页(警惕页)做为安全隙, 内核首先适当提升须要分配的内存长度.

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

start和end参数分别由调用者设置, 好比get_vm_area函数和get_vm_area_caller函数传入VMALLOC_START和VMALLOC_END. 接下来循环遍历vmlist的全部表元素,直至找到一个适当的项

2.2.3 释放函数

remove_vm_area函数将一个现存的子区域从vmalloc地址空间删除.

函数声明以下, include/linux/vmalloc.h?v=4.7, line 121

//  http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L121
struct vm_struct *remove_vm_area(void *addr);

函数定义在mm/vmalloc.c?v=4.7, line 1454

//  http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1446
/**
 *      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;
}

2.3 vmalloc分配内存区

vmalloc发起对不连续的内存区的分配操做. 该函数只是一个前端, 为__vmalloc提供适当的参数, 后者直接调用__vmalloc_node.

vmalloc只是__vmalloc_node_flags的前端接口, 复杂向__vmalloc_node_flags传递数据, 而__vmalloc_node_flags又是__vmalloc_node的前端接口, 然后者又将实际的工做交给__vmalloc_node_range函数来完成

vmalloc函数定义在mm/vmalloc.c?v=4.7, line 1754, 将实际的工做交给__vmalloc_node_flags函数来完成.

//  http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1754
/**
 *      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函数定义在mm/vmalloc.c?v=4.7, line 1747, 经过__vmalloc_node来完成实际的工做.

//  http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1747
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函数定义在mm/vmalloc.c?v=4.7, line 1719, 经过__vmalloc_node_range来完成实际的工做.

//  http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1719
/**
 *      __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最终完成了内存区的分配工做

//  http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1658
/**
 *      __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;

    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;

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

实现分为3部分

  • 首先, get_vm_areavmalloc地址空间中找到一个适当的区域.
  • 接下来从物理内存分配各个页
  • 最后将这些页连续地映射到vmalloc区域中, 分配虚拟内存的工做就完成了.

若是显式指定了分配页帧的结点, 则内核调用alloc_pages_node, 不然,使用alloc_page从当前结点分配页帧.

分配的页从相关结点的伙伴系统移除. 在调用时, vmalloc将gfp_mask设置为GFP_KERNEL | __GFP_HIGHMEM,内核经过该参数指示内存管理子系统尽量从ZONE_HIGHMEM内存域分配页帧. 理由已经在上文给出:低端内存域的页帧更为宝贵,所以不该该浪费到vmalloc的分配中,在此使用高

3 备选映射方法

除了vmalloc以外,还有其余方法能够建立虚拟连续映射。这些都基于上文讨论的__vmalloc函数或使用很是相似的机制

  • vmalloc_32的工做方式与vmalloc相同,但会确保所使用的物理内存老是能够用普通32位指针寻址。若是某种体系结构的寻址能力超出基于字长计算的范围, 那么这种保证就很重要。例如,在启用了PAEIA-32系统上,就是如此.
  • vmap使用一个page数组做为起点,来建立虚拟连续内存区。与vmalloc相比,该函数所用的物理内存位置不是隐式分配的,而须要先行分配好,做为参数传递。此类映射可经过vm_map实例中的VM_MAP标志辨别。
  • 不一样于上述的全部映射方法, ioremap是一个特定于处理器的函数, 必须在全部体系结构上实现. 它能够将取自物理地址空间、由系统总线用于I/O操做的一个内存块,映射到内核的地址空间中.

该函数在设备驱动程序中使用不少, 可将用于与外设通讯的地址区域暴露给内核的其余部分使用(固然也包括其自己).

4 释放内存

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

void __vunmap(void *addr, int deallocate_pages)

addr表示要释放的区域的起始地址, deallocate_pages指定了是否将与该区域相关的物理内存页返回给伙伴系统. vfree将后一个参数设置为1, 而vunmap设置为0, 由于在这种状况下只删除映射, 而不将相关的物理内存页返回给伙伴系统. 图3-40给出了__vunmap的代码流程图

没必要明确给出须要释放的区域长度, 长度能够从vmlist中的信息导出. 所以__vunmap的第一个任务是在__remove_vm_area(由remove_vm_area在完成锁定以后调用)中扫描该链表, 以找到 相关项。

unmap_vm_area使用找到的vm_area实例,从页表删除再也不须要的项。与分配内存时相似,该函 数须要操做各级页表,但这一次须要删除涉及的项。它还会更新CPU高速缓存。

若是__vunmap的参数deallocate_pages设置为1(在vfree中),内核会遍历area->pages的所 有元素,即指向所涉及的物理内存页的page实例的指针。而后对每一项调用__free_page,将页释放 到伙伴系统。

最后,必须释放用于管理该内存区的内核数据结构。

相关文章
相关标签/搜索