內存管理(三)虛擬內存映射(讀奔跑吧linux內核總結)

一:vmalloc

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

vmalloc創建內核空間的連續的虛擬地址的內存塊。(主要是在vmalloc區域找到合適的hole,然後逐頁分配內存從屋裏上填充hole)特點:可能連續,虛擬地址連續,物理地址不連續,size頁對齊(不適合小內存分配)。

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類函數的返回地址
};

VMALLOC_START和VMALLOC_END是vmalloc中重要的宏,在arch/arm/include/pgtable.h頭文件中它是在High_memory制定的高端內存開始地址加上8mb的安全區域內,vmalloc的內存範圍是在0xf0000000~0xff000000大小爲240M的區域內,

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

執行函數vmalloc->_vmalloc_node_range->_get_vm_area_node(找到符合要求的空閒vmalloc區域的hole,分配頁面,並且創建頁表映射關係)。

__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頁表映射關係

map_vm_area對分配的頁面進行了映射,map_vm_area-->vmap_page_range-->vmap_page_range_noflush。

二:vma操作

用戶空間擁有3gb的空間,我們如何管理這些虛擬地址空間,用戶進程多次調用malloc,mmap接口文件文件來進行讀寫操作,,這些操作要求在虛擬地址空間中分配內存塊,內存在物理上是離散的,

進程地址空間使用struct vm_area_struct的數據結構來描述,簡稱VMA,被稱爲進程地址空間或者進程線性區域。

struct vm_area_struct {
    unsigned long vm_start;        /* Our start address within vm_mm. */--------VMA在進程地址空間的起始結束地址
    unsigned long vm_end;        /* The first byte after our end address
                       within vm_mm. */
    /* linked list of VM areas per task, sorted by address */
    struct vm_area_struct *vm_next, *vm_prev;----------------------------------VMA鏈表的前後成員

    struct rb_node vm_rb;------------------------------------------------------VMA作爲一個節點加入到紅黑樹中,每個進程的mm_struct中都有一個紅黑樹mm->mm_rb。
    unsigned long rb_subtree_gap;
    /* Second cache line starts here. */
    struct mm_struct *vm_mm;    /* The address space we belong to. */--------指向VMA所屬進程的struct mm_struct結構。
    pgprot_t vm_page_prot;        /* Access permissions of this VMA. */------VMA訪問權限
    unsigned long vm_flags;        /* Flags, see mm.h. */--------------------VMA標誌位
    struct {
        struct rb_node rb;
        unsigned long rb_subtree_last;
    } shared;

    struct list_head anon_vma_chain; /* Serialized by mmap_sem &-----------用於管理RMAP反向映射。
                      * page_table_lock */
    struct anon_vma *anon_vma;    /* Serialized by page_table_lock */------用於管理RMAP反向映射。

    /* Function pointers to deal with this struct. */
    const struct vm_operations_struct *vm_ops;-----------------------------VMA操作函數合集,常用於文件映射。

    /* Information about our backing store: */
    unsigned long vm_pgoff;        /* Offset (within vm_file) in PAGE_SIZE-指定文件映射的偏移量,單位是頁面。
                       units, *not* PAGE_CACHE_SIZE */
    struct file * vm_file;        /* File we map to (can be NULL). */------描述一個被映射的文件。
    void * vm_private_data;        /* was vm_pte (shared mem) */

#ifndef CONFIG_MMU
    struct vm_region *vm_region;    /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
    struct mempolicy *vm_policy;    /* NUMA policy for the VMA */
#endif
}
struct mm_struct {
    struct vm_area_struct *mmap;        /* list of VMAs */-----單鏈表,按起始地址遞增的方式插入,所有的VMA都連接到此鏈表中。鏈表頭是mm_struct->mmap。
    struct rb_root mm_rb;--------------------------------------所有的VMA按照地址插入mm_struct->mm_rb紅黑樹中,mm_struct->mm_rb是根節點,每個進程都有一個紅黑樹。
...
}

2.1:查找vma

find_vma()通過虛擬地址查找vma

調用:vma = vmacache_find(mm, addr);內核中查找vma的優化方法,裏面存放一個最近訪問過的vma數組vmaacache[VMACACHE_SIZE]可以存放四個最近使用的vma,還沒找到,就遍歷用戶進程的mm_rb紅黑樹,該紅黑樹存放進程所有的VMA

insert_vm_struct()插入vma的核心函數。想vma鏈表的紅黑樹插入一個新的vma,

vma_merge()合併vma

三:malloc

先說malloc的調用流程:

malloc->GLibC(用戶空間)->brk(內核空間,新邊界爲brk)->find_vma_intersection(查找是否存在vma)->get_umapped_area(判斷是否有足夠的空間)->vma_merge(判斷是否可以合併附近的vma)->分配一個新的vma->吧新的vma插入mm系統中(mm_populate->_M,ock_vma_pages_rangs->_get_user_pages->find_extend_vma->follow_page_mask(page頁面分配映射))->返回新的brk邊界。

沒有初始化的內存分配:當使用內存時,CPU去查詢頁表,發現頁表爲空,cpu觸發缺頁中斷,然後在缺頁中斷中一頁一頁的分配,然後虛擬地址空間建立映射關係。

分出初始化的內存,需要的虛擬內存都已近分配了物理內存並且建立了頁表映射。

系統調用接口brk()(mm/mmap.c)。

內核空間爲用戶空間劃分3GB的虛擬空間,用戶空間有可執行的代碼段和數據段組成,用戶空間是從3gb虛擬空間的頂部開始,由頂部向下延伸,二brk分配的空間是由end_data到用戶棧的底部,所以動態分配空間是從end_data開始,沒分配一次空間,就把邊界往上推,內核和進程都會記錄當前的位置。

struct mm_struct {
...
    unsigned long start_code, end_code, start_data, end_data;-----代碼段從start_code到end_code;數據段從start_code到end_code。
    unsigned long start_brk, brk, start_stack;--------------------堆從start_brk開始,brk表示堆的結束地址;棧從start_stack開始。
    unsigned long arg_start, arg_end, env_start, env_end;---------表示參數列表和環境變量的起始和結束地址,這兩個區域都位於棧的最高區域。
...
}

 malloc是libc實現的接口,主要通過sys_brk這個系統調用分配內存。 調用SYSCALL_DEFINE1->do_brk(判斷虛擬地址是否足夠,然後查找VMA的插入點,並判斷是否能夠進行VMA合併,如果找不到VMA插入點,就創建一個VMA,並且更新到mm->mmap中去)。

static unsigned long do_brk(unsigned long addr, unsigned long len)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma, *prev;
    unsigned long flags;
    struct rb_node **rb_link, *rb_parent;
    pgoff_t pgoff = addr >> PAGE_SHIFT;
    int error;
    len = PAGE_ALIGN(len);
    if (!len)
        return addr;
    flags = VM_DATA_DEFAULT_FLAGS | VM_ACCOUNT | mm->def_flags;

    error = get_unmapped_area(NULL, addr, len, 0, MAP_FIXED);---------------------------判斷虛擬地址空間是否有足夠的空間,這部分代碼是跟體系結構緊耦合的。
    if (error & ~PAGE_MASK)
        return error;
    error = mlock_future_check(mm, mm->def_flags, len);
    if (error)
        return error;

    /*
     * mm->mmap_sem is required to protect against another thread
     * changing the mappings in case we sleep.
     */
    verify_mm_writelocked(mm);
 munmap_back:
    if (find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent)) {----------循環遍歷用戶進程紅黑樹中的VMA,然後根據addr來查找合適的插入點
        if (do_munmap(mm, addr, len))
            return -ENOMEM;
        goto munmap_back;
    }
    /* Check against address space limits *after* clearing old maps... */
    if (!may_expand_vm(mm, len >> PAGE_SHIFT))
        return -ENOMEM;

    if (mm->map_count > sysctl_max_map_count)
        return -ENOMEM;

    if (security_vm_enough_memory_mm(mm, len >> PAGE_SHIFT))
        return -ENOMEM;

    /* Can we just expand an old private anonymous mapping? */
    vma = vma_merge(mm, prev, addr, addr + len, flags,------------------------------去找有沒有可能合併addr附近的VMA。
                    NULL, NULL, pgoff, NULL);
    if (vma)
        goto out;

    /*
     * create a vma struct for an anonymous mapping
     */
    vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);----------------------------如果沒辦法合併,只能新創建一個VMA,VMA地址空間是[addr, addr+len]。
    if (!vma) {
        vm_unacct_memory(len >> PAGE_SHIFT);
        return -ENOMEM;
    }

    INIT_LIST_HEAD(&vma->anon_vma_chain);
    vma->vm_mm = mm;
    vma->vm_start = addr;
    vma->vm_end = addr + len;
    vma->vm_pgoff = pgoff;
    vma->vm_flags = flags;
    vma->vm_page_prot = vm_get_page_prot(flags);
    vma_link(mm, vma, prev, rb_link, rb_parent);------------------------------------將新創建的VMA加入到mm->mmap鏈表和紅黑樹中。
out:
    perf_event_mmap(vma);
    mm->total_vm += len >> PAGE_SHIFT;
    if (flags & VM_LOCKED)
        mm->locked_vm += (len >> PAGE_SHIFT);
    vma->vm_flags |= VM_SOFTDIRTY;
    return addr;
}

 從arch_pick_mmap_layout中可知,current->mm->get_ummapped_area對應的是arch_get_unmapped_area_topdown

 所以get_unmapped_area指向arch_get_unmapped_area_topdown(用來判斷虛擬地址是否有足夠的空間,返回一塊沒有映射 過的空間的起始地址)。

3.1:VM_LOCK的情況

表示馬上爲這塊進程虛擬地址空間分配物理頁面並建立映射關係。

mm_populate調用__mm_populate來分配頁面,同時ignore_erros。

int __mm_populate(unsigned long start, unsigned long len, int ignore_errors)
{
    struct mm_struct *mm = current->mm;
    unsigned long end, nstart, nend;
    struct vm_area_struct *vma = NULL;
    int locked = 0;
    long ret = 0;

    VM_BUG_ON(start & ~PAGE_MASK);
    VM_BUG_ON(len != PAGE_ALIGN(len));
    end = start + len;

    for (nstart = start; nstart < end; nstart = nend) {----------------------------以start爲起始地址,先通過find_vma()查找VMA。
        /*
         * We want to fault in pages for [nstart; end) address range.
         * Find first corresponding VMA.
         */
        if (!locked) {
            locked = 1;
            down_read(&mm->mmap_sem);
            vma = find_vma(mm, nstart);
        } else if (nstart >= vma->vm_end)
            vma = vma->vm_next;
        if (!vma || vma->vm_start >= end)
            break;
        /*
         * Set [nstart; nend) to intersection of desired address
         * range with the first VMA. Also, skip undesirable VMA types.
         */
        nend = min(end, vma->vm_end);
        if (vma->vm_flags & (VM_IO | VM_PFNMAP))
            continue;
        if (nstart < vma->vm_start)
            nstart = vma->vm_start;
        /*
         * Now fault in a range of pages. __mlock_vma_pages_range()
         * double checks the vma flags, so that it won't mlock pages
         * if the vma was already munlocked.
         */
        ret = __mlock_vma_pages_range(vma, nstart, nend, &locked);------------------爲vma分配物理內存
        if (ret < 0) {
            if (ignore_errors) {
                ret = 0;
                continue;    /* continue at next VMA */
            }
            ret = __mlock_posix_error_return(ret);
            break;
        }
        nend = nstart + ret * PAGE_SIZE;
        ret = 0;
    }
    if (locked)
        up_read(&mm->mmap_sem);
    return ret;    /* 0 or negative error code */
}

 

__mlock_vma_pages_range爲vma指定虛擬地址空間的物理頁面:

long __mlock_vma_pages_range(struct vm_area_struct *vma,
        unsigned long start, unsigned long end, int *nonblocking)
{
    struct mm_struct *mm = vma->vm_mm;
    unsigned long nr_pages = (end - start) / PAGE_SIZE;
    int gup_flags;

    VM_BUG_ON(start & ~PAGE_MASK);
    VM_BUG_ON(end   & ~PAGE_MASK);
    VM_BUG_ON_VMA(start < vma->vm_start, vma);
    VM_BUG_ON_VMA(end   > vma->vm_end, vma);
    VM_BUG_ON_MM(!rwsem_is_locked(&mm->mmap_sem), mm);------------------------一些錯誤判斷

    gup_flags = FOLL_TOUCH | FOLL_MLOCK;
    /*
     * We want to touch writable mappings with a write fault in order
     * to break COW, except for shared mappings because these don't COW
     * and we would not want to dirty them for nothing.
     */
    if ((vma->vm_flags & (VM_WRITE | VM_SHARED)) == VM_WRITE)
        gup_flags |= FOLL_WRITE;

    /*
     * We want mlock to succeed for regions that have any permissions
     * other than PROT_NONE.
     */
    if (vma->vm_flags & (VM_READ | VM_WRITE | VM_EXEC))
        gup_flags |= FOLL_FORCE;

    /*
     * We made sure addr is within a VMA, so the following will
     * not result in a stack expansion that recurses back here.
     */
    return __get_user_pages(current, mm, start, nr_pages, gup_flags,----------爲進程地址空間分配物理內存並且建立映射關係。
                NULL, NULL, nonblocking);
}

__get_user_pages是很重要的分配物理內存的接口函數,很多驅動使用這個API用於爲用戶空間分配物理內存

long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
        unsigned long start, unsigned long nr_pages,
        unsigned int gup_flags, struct page **pages,
        struct vm_area_struct **vmas, int *nonblocking)
{
    long i = 0;
    unsigned int page_mask;
    struct vm_area_struct *vma = NULL;

    if (!nr_pages)
        return 0;

    VM_BUG_ON(!!pages != !!(gup_flags & FOLL_GET));

    /*
     * If FOLL_FORCE is set then do not force a full fault as the hinting
     * fault information is unrelated to the reference behaviour of a task
     * using the address space
     */
    if (!(gup_flags & FOLL_FORCE))
        gup_flags |= FOLL_NUMA;

    do {
        struct page *page;
        unsigned int foll_flags = gup_flags;
        unsigned int page_increm;

        /* first iteration or cross vma bound */
        if (!vma || start >= vma->vm_end) {
            vma = find_extend_vma(mm, start);------------------------------查找VMA,如果vma->vm_start大於查找地址start,那麼它會嘗試去擴增vma,吧vma->vm_start邊界擴大到start中。
            if (!vma && in_gate_area(mm, start)) {
                int ret;
                ret = get_gate_page(mm, start & PAGE_MASK
                        gup_flags, &vma,
                        pages ? &pages[i] : NULL);
                if (ret)
                    return i ? : ret;
                page_mask = 0;
                goto next_page;
            }

            if (!vma || check_vma_flags(vma, gup_flags))
                return i ? : -EFAULT;
            if (is_vm_hugetlb_page(vma)) {
                i = follow_hugetlb_page(mm, vma, pages, vmas,
                        &start, &nr_pages, i,
                        gup_flags);
                continue;
            }
        }
retry:
        /*
         * If we have a pending SIGKILL, don't keep faulting pages and
         * potentially allocating memory.
         */
        if (unlikely(fatal_signal_pending(current)))-----------------------如果收到一個SIGKILL信號,不需要繼續內存分配,直接退出。
            return i ? i : -ERESTARTSYS;
        cond_resched();----------------------------------------------------判斷當前進程是否需要被調度。
        page = follow_page_mask(vma, start, foll_flags, &page_mask);-------查看vma中的虛擬地址是否已經分配了物理內存。
        if (!page) {
            int ret;
            ret = faultin_page(tsk, vma, start, &foll_flags,
                    nonblocking);
            switch (ret) {
            case 0:
                goto retry;
            case -EFAULT:
            case -ENOMEM:
            case -EHWPOISON:
                return i ? i : ret;
            case -EBUSY:
                return i;
            case -ENOENT:
                goto next_page;
            }
            BUG();
        }
        if (IS_ERR(page))
            return i ? i : PTR_ERR(page);
        if (pages) {-------------------------------------------------------flush頁面對應的cache
            pages[i] = page;
            flush_anon_page(vma, page, start);
            flush_dcache_page(page);
            page_mask = 0;
        }
next_page:
        if (vmas) {
            vmas[i] = vma;
            page_mask = 0;
        }
        page_increm = 1 + (~(start >> PAGE_SHIFT) & page_mask);
        if (page_increm > nr_pages)
            page_increm = nr_pages;
        i += page_increm;
        start += page_increm * PAGE_SIZE;
        nr_pages -= page_increm;
    } while (nr_pages);
    return i;
}
follow_page_mask函數:由mm和地址address找到當前進程頁表對應的PGD頁面目錄項,用戶進程內存管理mm_struct的pgd成員指向用戶進程的頁表的基地址。如果pgd表項的頁表爲空,則返回報錯。
vm_normal_page:根據pte來返回normal mapping頁面的struct page數據結構體。

一些特殊映射的頁面是不會返回struct page結構的,這些頁面不希望被參與到內存管理的一些活動中,如頁面回收、頁遷移和KSM等。

內核嘗試用pte_mkspecial()宏來設置PTE_SPECIAL軟件定義的比特位,主要用途有

  • 內核的零頁面zero page
  • 大量的驅動程序使用remap_pfn_range()函數來實現映射內核頁面到用戶空間。這些用戶程序使用的VMA通常設置了(VM_IO|VM_PFNMAP|VM_DONTEXPAND|VM_DONTDUMP)
  • vm_insert_page()/vm_insert_pfn()映射內核頁面到用戶空間

eg:malloc(30k)函數調用brk,將_edata指針往高推30k,完成虛擬內存分配,(這塊內存現在還沒有物理頁與之對應),等到進程第一次讀寫這塊內存的時候,發生缺頁中斷,內核才分配內存的物理頁面。

 

四:mmap映射

mmap作用:常用的一個系統接口調用,用戶程序分配內存,讀寫大文件,連接動態庫文件,多進程間共享內存。

可以分爲私有內存和共享內存。

私有內存:常見作用爲glibc分配大塊內存

共享內存:讓相關進程共享一塊內存區域。,通常用於父子進程的通信。

mmap的兩個小問題:

1:兩次對相同地址執行mmap是否成功?

複製代碼

#include <stdio.h>
#include <sys/mman.h>

void main(void)
{
    char *pmap1, *pmap2;

    pmap1 = (char *)mmap(0x20000000, 10240, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0);
    if(MAP_FAILED == pmap1)
        printf("pmap1 failed\n");

    pmap2 = (char *)mmap(0x20000000, 1024, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0);
    if(MAP_FAILED == pmap2)
        printf("pmap1 failed\n");
}

第二次沒有返回錯誤:原因find_vma_links()會遍歷進程中所有的vmas當檢查到當前映射區域和已有映射區域有重疊時返回錯誤,然後在mmap_reion函數中調用do_munmap函數吧這段將要映射的區域先銷燬然後重新映射。

 

問題2:在一個播放系統中同時打開幾十個不同高清視頻文件,發現播放有些卡頓,打開文件使用的是mmap,分析原因並解決。

mmap建立文件映射時,只建立了VMA,而沒有分配對應的頁面和建立映射關係。當播放器真正讀取文件時才產恆缺頁中斷讀取文件內容到pagecache中去。每次讀取文件時,會頻繁的產生缺頁中斷。

播放時會不同發生缺頁異常去讀取文件內容,導致性能較差。

解決方法:1.對mmap映射後的地址用madvise(addr, len, MADV_SEQUENTIAL)。MADV_SEQUENTIAL會立刻啓動io進行預讀,增大內核默認的預讀窗口。

                  2.通過"blockdev --setra"來增大內核默認預讀窗口,默認是128KB

 

 

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