内存管理(三)虚拟内存映射(读奔跑吧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 
#include 

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

 

 

你可能感兴趣的:(个人随笔)