Linux內存管理 (6)vmalloc【轉】 Linux內存管理專題

轉自:https://www.cnblogs.com/arnoldlu/p/8251333.html

專題: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.\n", 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\n",
              (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);
}
複製代碼

 

聯繫方式:[email protected]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章