zoukankan      html  css  js  c++  java
  • Linux内存管理 (9)mmap(补充)

    之前写过一篇简单的介绍mmap()/munmap()的文章《Linux内存管理 (9)mmap》,比较单薄,这里详细的梳理一下。

    从常用的使用者角度介绍两个函数的使用;然后重点是分析内核的实现流程;最后对mmap()/munmap()进行一些验证测试。

    mmap系统调用并不完全是为了共享内存而设计的,它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件操作。

    mmap系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read()/write()等操作。

    mmap并不分配空间,只是将文件映射到调用进程的地址空间里(占用虚拟地址空间),然后就可以使用memcpy()等操作,内存中内容并不立即更行到文件中,而是有一段时间的延迟,可以使用msync()显式同步。

    取消内存映射通过munmap()。

    下面这张图示意了mmap的内存映射,起始地址是返回的addr,off和len分别对应参数offset和length。

    1. mmap API解释

     对mmap()/munmap()的使用比较简单,有两个参数组合导致了多样性,分别是protflags

    #include <sys/mman.h>
    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
    int munmap(void *addr, size_t length);

    下面对这些参数做一个简单的介绍:

    • addr:如果不为NULL,内核会在此地址创建映射;否则,内核会选择一个合适的虚拟地址。大部分情况不指定虚拟地址,意义不大,而是让内核选择返回一个地址给用户空间使用。
    • length:表示映射到进程地址空间的大小。
    • prot:内存区域的读/写/执行属性。
    • flags:内存映射的属性,共享、私有、匿名、文件等。
    • fd:表示这是一个文件映射,fd是打开文件的句柄。如果是文件映射,需要指定fd;匿名映射就指定一个特殊的-1。
    • offset:在文件映射时,表示相对文件头的偏移量;返回的地址是偏移量对应的虚拟地址。

    1.1 mmap优点

    1.1.1 提升效率

    一般读写文件需要open、read、write,需要先将磁盘文件读取到内核cache缓冲区,然后再拷贝到用户空间内存区,设计两次读写操作。

    mmap通过将磁盘文件映射到用户空间,当进程读文件时,发生缺页中断,给虚拟内存分配对应的物理内存,在通过磁盘调页操作将磁盘数据读到物理内存上,实现了用户空间数据的读取,整个过程只有一次内存拷贝。

    1.1.2 用于进程间大数据量通信

    两个进程映射同一个文件,在两个进程中,同一个文件区域映射的虚拟地址空间不同。一个进程操作文件时,先通过缺页获取物理内存,进而通过磁盘文件调页操作将文件数据读入内存。

    另一个进程访问文件的时候,发现没有物理页面映射到虚拟内存,通过fs的缺页处理查找cache区是否有读入磁盘文件,有的话建立映射关系,这样两个进程通过共享内存就可以进行通信。

    1.1.3 文件关闭,内存可以继续使用

    因为在内核中已经通过fd找到对应的磁盘文件,从而将文件跟vma关联。

    1.2 mmap缺点

    映射时文件长度已经确定,没法通过mmap访问操作len的区间。

    1.3 私有/共享、文件/匿名映射组合

    共有四种组合,下面逐一介绍。

    1.3.1 私有文件映射

    多个进程使用同样的物理页面进行初始化,但是各个进程对内存文件的修改不会共享,也不会反映到物理文件中。

    比如对linux .so动态库文件就采用这种方式映射到各个进程虚拟地址空间中。

    1.3.2 私有匿名映射

    mmap会创建一个新的映射,各个进程不共享,主要用于分配内存(malloc分配大内存会调用mmap)。

    1.3.3 共享文件映射

    多个进程通过虚拟内存技术共享同样物理内存,对内存文件的修改会反应到实际物理内存中,也是进程间通信的一种。

    1.3.4 共享匿名映射

    这种机制在进行fork时不会采用写时复制,父子进程完全共享同样的物理内存页,也就是父子进程通信。

    2. mmap内核实现

    系统调用的入口是entry_SYSCALL_64_fastpath,然后根据系统调用号在sys_call_table中找到对应的函数。

    mmap()和munmap()对应的系统调用分别是SyS_mmap()和SyS_munmap()下面就来分析一下实现。

    2.0 mmap/munmap调用路径

    在分析具体内核实现之前,通过脚本来看看mmap/munmap调用路径。

    通过增加set_ftrace_filter的函数,修改current_tracer发现函数的调用者,逐步丰富调用路径。

    #!/bin/bash 
     
    DPATH="/sys/kernel/debug/tracing"
    PID=$$ 
    ## Quick basic checks 
    [ `id -u` -ne 0 ] && { echo "needs to be root" ; exit 1; } # check for root permissions 
    [ -z $1 ] && { echo "needs process name as argument" ; exit 1; } # check for args to this function 
    mount | grep -i debugfs &> /dev/null 
    [ $? -ne 0 ] && { echo "debugfs not mounted, mount it first"; exit 1; } #checks for debugfs mount 
     
    # flush existing trace data 
    echo > $DPATH/trace
    echo nop > $DPATH/current_tracer
    
    echo > $DPATH/set_ftrace_filter
    echo "SyS_mmap SyS_mmap_pgoff SyS_munmap SyS_open SyS_read SyS_write SyS_close SyS_brk SyS_msync" >> $DPATH/set_ftrace_filter
    echo "do_brk elf_map load_elf_binary" >> $DPATH/set_ftrace_filter
    echo "do_mmap do_munmap get_unmapped_area mmap_region vm_mmap vm_munmap vm_mmap_pgoff" >> $DPATH/set_ftrace_filter
    echo "__split_vma* unmap_region" >> $DPATH/set_ftrace_filter
    
    # set function tracer
    echo function_graph > $DPATH/current_tracer
    
    
    
    # write current process id to set_ftrace_pid file
    echo $PID > $DPATH/set_ftrace_pid
    
    
    #echo "common_pid==$PID" > /sys/kernel/debug/tracing/events/syscalls/sys_enter_mmap/filter
    #echo 1 > /sys/kernel/debug/tracing/events/syscalls/sys_enter_mmap/enable
    #echo "common_pid==$PID" > /sys/kernel/debug/tracing/events/syscalls/sys_enter_munmap/filter
    #echo 1 > /sys/kernel/debug/tracing/events/syscalls/sys_enter_munmap/enable
    
    # start the tracing
    echo 1 > $DPATH/tracing_on
    # execute the process
    exec $*
    
    
    #sudo cat $DPATH/trace > /home/al/v4l2/trace.txt

    最后使用function_graph跟踪器查看调用关系如下:

     1)               |  SyS_mmap() {
     1)               |    SyS_mmap_pgoff() {
     1)               |      vm_mmap_pgoff() {
     1)               |        do_mmap() {
     1)   0.548 us    |          get_unmapped_area();
     1)   3.388 us    |          mmap_region();
     1)   4.598 us    |        }
     1)   5.286 us    |      }
     1)   5.756 us    |    }
     1)   6.058 us    |  }
     1)               |  SyS_munmap() {
     1)               |    vm_munmap() {
     1)               |      do_munmap() {
     1) + 99.985 us   |        unmap_region();
     1) ! 101.439 us  |      }
     1) ! 101.838 us  |    }
     1) ! 102.410 us  |  }

    下面就围绕这条路径展开分析。

    2.1 mmap()

    mmap()系统调用的核心是do_mmap(),可以分为三部分。

    第一部分通过get_unmapped_area()函数,找到一段虚拟地址,范围是[addr, addr+len]。

    用户进程一般不会指定addr,也就是由内核指定这个虚拟空间的首地址addr在哪里。

    在函数do_mmap_pgoff()调用get_unmapped_area()之前会预指定addr,通过round_hint_to_min()实现,然后用这个预指定addr为参数调用get_unmapped_area()。

    第二部分确定vma线性区的flags,针对文件、匿名,私有、共享有所不同。

    第三部分是实际创建vma先行区,通过函数mmap_region()实现。

    asmlinkage unsigned long
    sys_mmap (unsigned long addr, unsigned long len, int prot, int flags, int fd, long off)
    {
        if (offset_in_page(off) != 0)
            return -EINVAL;
    
        addr = sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
        if (!IS_ERR((void *) addr))
            force_successful_syscall_return();
        return addr;
    }
    
    SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
            unsigned long, prot, unsigned long, flags,
            unsigned long, fd, unsigned long, pgoff)
    {
        struct file *file = NULL;
        unsigned long retval;
    
        if (!(flags & MAP_ANONYMOUS)) {------------------------------------------对非匿名文件映射的检查,必须能根据文件句柄找到struct file。
            audit_mmap_fd(fd, flags);
            file = fget(fd);
            if (!file)
                return -EBADF;
            if (is_file_hugepages(file))
                len = ALIGN(len, huge_page_size(hstate_file(file)));-------------根据file->f_op来判断是否是hugepage,然后进行hugepage页面对齐。
            retval = -EINVAL;
            if (unlikely(flags & MAP_HUGETLB && !is_file_hugepages(file)))
                goto out_fput;
        } else if (flags & MAP_HUGETLB) {
            struct user_struct *user = NULL;
            struct hstate *hs;
    
            hs = hstate_sizelog((flags >> MAP_HUGE_SHIFT) & SHM_HUGE_MASK);
            if (!hs)
                return -EINVAL;
    
            len = ALIGN(len, huge_page_size(hs));
            /*
             * VM_NORESERVE is used because the reservations will be
             * taken when vm_ops->mmap() is called
             * A dummy user value is used because we are not locking
             * memory so no accounting is necessary
             */
            file = hugetlb_file_setup(HUGETLB_ANON_FILE, len,
                    VM_NORESERVE,
                    &user, HUGETLB_ANONHUGE_INODE,
                    (flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);
            if (IS_ERR(file))
                return PTR_ERR(file);
        }
    
        flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE);
    
        retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
    out_fput:
        if (file)
            fput(file);
        return retval;
    }
    
    unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr,
        unsigned long len, unsigned long prot,
        unsigned long flag, unsigned long pgoff)
    {
        unsigned long ret;
        struct mm_struct *mm = current->mm;
        unsigned long populate;
    
        ret = security_mmap_file(file, prot, flag);
        if (!ret) {
            down_write(&mm->mmap_sem);
            ret = do_mmap_pgoff(file, addr, len, prot, flag, pgoff,
                        &populate);
            up_write(&mm->mmap_sem);
            if (populate)
                mm_populate(ret, populate);
        }
        return ret;
    }
    
    unsigned long do_mmap(struct file *file, unsigned long addr,
                unsigned long len, unsigned long prot,
                unsigned long flags, vm_flags_t vm_flags,
                unsigned long pgoff, unsigned long *populate)
    {
        struct mm_struct *mm = current->mm;
    
        *populate = 0;
    
        if (!len)
            return -EINVAL;
    
        if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
            if (!(file && path_noexec(&file->f_path)))
                prot |= PROT_EXEC;
    
        if (!(flags & MAP_FIXED))-------------------------------------------------对于非MAP_FIXED,addr不能小于mmap_min_addr大小,如果小于则使用mmap_min_addr页对齐后的地址。
            addr = round_hint_to_min(addr);
    
        /* Careful about overflows.. */
        len = PAGE_ALIGN(len);
        if (!len)-----------------------------------------------------------------这里不是判断len是否为0,而是检查len是否溢出。
            return -ENOMEM;
    
        /* offset overflow? */
        if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)--------------------------------检查offset是否溢出
            return -EOVERFLOW;
    
        /* Too many mappings? */
        if (mm->map_count > sysctl_max_map_count)---------------------------------进程中mmap个数限制,超出返回ENOMEM错误。
            return -ENOMEM;
        addr = get_unmapped_area(file, addr, len, pgoff, flags);------------------在创建新的ma区域之前首先寻找一块足够大小的空闲区域,本函数就是用于查找未映射的区域,返回值addr就是这段空间的首地址。
        if (offset_in_page(addr))
            return addr;
    
        vm_flags |= calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
                mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;------------根据prot/flags以及mm->flags来得到vm_flags。
    
        if (flags & MAP_LOCKED)
            if (!can_do_mlock())
                return -EPERM;
    
        if (mlock_future_check(mm, vm_flags, len))
            return -EAGAIN;
    
        if (file) {---------------------------------------------------------------文件映射情况处理,主要更新vm_flags。
            struct inode *inode = file_inode(file);
    
            if (!file_mmap_ok(file, inode, pgoff, len))
                return -EOVERFLOW;
    
            switch (flags & MAP_TYPE) {
            case MAP_SHARED:------------------------------------------------------共享文件映射
                if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE))
                    return -EACCES;
                if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
                    return -EACCES;
                if (locks_verify_locked(file))
                    return -EAGAIN;
                vm_flags |= VM_SHARED | VM_MAYSHARE;
                if (!(file->f_mode & FMODE_WRITE))
                    vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
            case MAP_PRIVATE:-----------------------------------------------------私有文件映射
                if (!(file->f_mode & FMODE_READ))
                    return -EACCES;
                if (path_noexec(&file->f_path)) {
                    if (vm_flags & VM_EXEC)
                        return -EPERM;
                    vm_flags &= ~VM_MAYEXEC;
                }
                if (!file->f_op->mmap)
                    return -ENODEV;
                if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
                    return -EINVAL;
                break;
            default:
                return -EINVAL;
            }
        } else {------------------------------------------------------------------匿名映射情况处理
            switch (flags & MAP_TYPE) {
            case MAP_SHARED:------------------------------------------------------共享匿名映射
                if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
                    return -EINVAL;
                pgoff = 0;--------------------------------------------------------为什么为0?
                vm_flags |= VM_SHARED | VM_MAYSHARE;
                break;
            case MAP_PRIVATE:-----------------------------------------------------私有匿名映射
                pgoff = addr >> PAGE_SHIFT;
                break;
            default:
                return -EINVAL;
            }
        }
        if (flags & MAP_NORESERVE) {
            /* We honor MAP_NORESERVE if allowed to overcommit */
            if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
                vm_flags |= VM_NORESERVE;
    
            /* hugetlb applies strict overcommit unless MAP_NORESERVE */
            if (file && is_file_hugepages(file))
                vm_flags |= VM_NORESERVE;
        }
    
        addr = mmap_region(file, addr, len, vm_flags, pgoff);--------------------实际创建vma
        if (!IS_ERR_VALUE(addr) &&
            ((vm_flags & VM_LOCKED) ||
             (flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
            *populate = len;
        return addr;
    }

    get_unmapped_area()根据输入的addr,以及其它参数通过get_area()来找到一个满足条件的虚拟空间,返回这个虚拟空间的首地址。

    get_area()是一个函数指针,有两种可能使用mm->get_unmapped_area()或者file->f_op->get_unmapped_area()。

    unsigned long
    get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
            unsigned long pgoff, unsigned long flags)
    {
        unsigned long (*get_area)(struct file *, unsigned long,
                      unsigned long, unsigned long, unsigned long);
    
        unsigned long error = arch_mmap_check(addr, len, flags);
        if (error)
            return error;
    
        /* Careful about overflows.. */
        if (len > TASK_SIZE)
            return -ENOMEM;
    
        get_area = current->mm->get_unmapped_area;------------使用mm_struct->get_unmapped_area()方法,即arch_get_unmapped_area()。
        if (file && file->f_op->get_unmapped_area)------------如果是文件映射,并且该文件的file_operations定义了get_unmapped_area方法,那么使用它实现定位虚拟区间。
            get_area = file->f_op->get_unmapped_area;
        addr = get_area(file, addr, len, pgoff, flags);
        if (IS_ERR_VALUE(addr))
            return addr;
    
        if (addr > TASK_SIZE - len)
            return -ENOMEM;
        if (offset_in_page(addr))
            return -EINVAL;
    
        addr = arch_rebalance_pgtables(addr, len);
        error = security_mmap_addr(addr);
        return error ? error : addr;
    }

    看arch_get_unmapped_area()名字就知道,可能有各架构自己的实现函数。这里以平台无关的函数进行分析。

    arch_get_unmapped_area()完成从低地址向高地址创建新的映射,而arch_get_unmapped_area_topdown()完成从高地址向低地址创建新的映射。

    unsigned long
    arch_get_unmapped_area(struct file *filp, unsigned long addr,
            unsigned long len, unsigned long pgoff, unsigned long flags)
    {
        struct mm_struct *mm = current->mm;
        struct vm_area_struct *vma;
        int do_align = 0;
        int aliasing = cache_is_vipt_aliasing();
        struct vm_unmapped_area_info info;
    
        if (aliasing)
            do_align = filp || (flags & MAP_SHARED);
    
        if (flags & MAP_FIXED) {------------------这里可以看出MAP_FIXED不参与选址,固定地址创建。
            if (aliasing && flags & MAP_SHARED &&
                (addr - (pgoff << PAGE_SHIFT)) & (SHMLBA - 1))
                return -EINVAL;
            return addr;
        }
    
        if (len > TASK_SIZE)
            return -ENOMEM;
    
        if (addr) {--------------------------------当addr非0,表示制定了一个特定的优先选用地址,内核会检查该区域是否与现存区域重叠,有find_vma()完成查找功能。
            if (do_align)
                addr = COLOUR_ALIGN(addr, pgoff);
            else
                addr = PAGE_ALIGN(addr);
    
            vma = find_vma(mm, addr);
            if (TASK_SIZE - len >= addr &&
                (!vma || addr + len <= vm_start_gap(vma)))
                return addr;
        }
    
        info.flags = 0;
        info.length = len;
        info.low_limit = mm->mmap_base;
        info.high_limit = TASK_SIZE;
        info.align_mask = do_align ? (PAGE_MASK & (SHMLBA - 1)) : 0;
        info.align_offset = pgoff << PAGE_SHIFT;
        return vm_unmapped_area(&info);-----------当addr为空或者指定的优选地址不满足分配条件时,内核必须遍历进程中可用的区域,设法找到一个大小适当的空闲区域,vm_unmapped_area()完成实际的工作。
    }
    
    static inline unsigned long
    vm_unmapped_area(struct vm_unmapped_area_info *info)
    {
        if (info->flags & VM_UNMAPPED_AREA_TOPDOWN)
            return unmapped_area_topdown(info);--从高地址到低地址穿点映射。
        else
            return unmapped_area(info);----------从低地址到高地址创建映射。
    }
    
    unsigned long unmapped_area(struct vm_unmapped_area_info *info)
    {
        /*
         * We implement the search by looking for an rbtree node that
         * immediately follows a suitable gap. That is,
         * - gap_start = vma->vm_prev->vm_end <= info->high_limit - length;
         * - gap_end   = vma->vm_start        >= info->low_limit  + length;
         * - gap_end - gap_start >= length
         */
    
        struct mm_struct *mm = current->mm;
        struct vm_area_struct *vma;
        unsigned long length, low_limit, high_limit, gap_start, gap_end;
    
        /* Adjust search length to account for worst case alignment overhead */
        length = info->length + info->align_mask;
        if (length < info->length)
            return -ENOMEM;
    
        /* Adjust search limits by the desired length */
        if (info->high_limit < length)
            return -ENOMEM;
        high_limit = info->high_limit - length;
    
        if (info->low_limit > high_limit)
            return -ENOMEM;
        low_limit = info->low_limit + length;
    
        /* Check if rbtree root looks promising */
        if (RB_EMPTY_ROOT(&mm->mm_rb))
            goto check_highest;
        vma = rb_entry(mm->mm_rb.rb_node, struct vm_area_struct, vm_rb);
        if (vma->rb_subtree_gap < length)
            goto check_highest;
    
        while (true) {
            /* Visit left subtree if it looks promising */
            gap_end = vm_start_gap(vma);----------------------------------先从低地址开始查询。
            if (gap_end >= low_limit && vma->vm_rb.rb_left) {
                struct vm_area_struct *left =
                    rb_entry(vma->vm_rb.rb_left,
                         struct vm_area_struct, vm_rb);
                if (left->rb_subtree_gap >= length) {
                    vma = left;
                    continue;
                }
            }
    
            gap_start = vma->vm_prev ? vm_end_gap(vma->vm_prev) : 0;------当前结点rb_subtree_gap已经是最后一个可能满足这次分配。
    check_current:
            /* Check if current node has a suitable gap */
            if (gap_start > high_limit)
                return -ENOMEM;
            if (gap_end >= low_limit &&
                gap_end > gap_start && gap_end - gap_start >= length)
                goto found;
    
            /* Visit right subtree if it looks promising */
            if (vma->vm_rb.rb_right) {
                struct vm_area_struct *right =
                    rb_entry(vma->vm_rb.rb_right,
                         struct vm_area_struct, vm_rb);
                if (right->rb_subtree_gap >= length) {
                    vma = right;
                    continue;
                }
            }
    
            /* Go back up the rbtree to find next candidate node */
            while (true) {
                struct rb_node *prev = &vma->vm_rb;
                if (!rb_parent(prev))
                    goto check_highest;
                vma = rb_entry(rb_parent(prev),
                           struct vm_area_struct, vm_rb);
                if (prev == vma->vm_rb.rb_left) {
                    gap_start = vm_end_gap(vma->vm_prev);
                    gap_end = vm_start_gap(vma);
                    goto check_current;
                }
            }
        }
    
    check_highest:
        /* Check highest gap, which does not precede any rbtree node */
        gap_start = mm->highest_vm_end;
        gap_end = ULONG_MAX;  /* Only for VM_BUG_ON below */
        if (gap_start > high_limit)
            return -ENOMEM;
    
    found:
        /* We found a suitable gap. Clip it with the original low_limit. */
        if (gap_start < info->low_limit)
            gap_start = info->low_limit;
    
        /* Adjust gap address to the desired alignment */
        gap_start += (info->align_offset - gap_start) & info->align_mask;
    
        VM_BUG_ON(gap_start + info->length > info->high_limit);
        VM_BUG_ON(gap_start + info->length > gap_end);
        return gap_start;
    }

    mmap_region()首先调用find_vma_links()查找是否已有vma线性区包含addr,如果有调用do_munmap()把这个vma干掉。

    Linux不希望vma和vma之间存在空洞,只要新创建vma的flags属性和前面或者后面vma仙童,就尝试合并成一个新的vma,减少slab缓存消耗量,同时也减少了空洞浪费。

    如果无法合并,那么只好新创建vma并对vma结构体初始化先关成员;根据vma是否有页锁定标志(VM_LOCKED),决定是否立即分配物理页。

    最后将新建的vma插入进程空间vma红黑树中,并返回addr。

    unsigned long mmap_region(struct file *file, unsigned long addr,
            unsigned long len, vm_flags_t vm_flags, unsigned long pgoff)
    {
        struct mm_struct *mm = current->mm;
        struct vm_area_struct *vma, *prev;
        int error;
        struct rb_node **rb_link, *rb_parent;
        unsigned long charged = 0;
    
        /* Check against address space limit. */
        if (!may_expand_vm(mm, len >> PAGE_SHIFT)) {--------------------检查当前total_vm+len是否查过RLIMIT_AS,确保虚拟映射可以进行。
            unsigned long nr_pages;
    
            if (!(vm_flags & MAP_FIXED))
                return -ENOMEM;
    
            nr_pages = count_vma_pages_range(mm, addr, addr + len);
    
            if (!may_expand_vm(mm, (len >> PAGE_SHIFT) - nr_pages))
                return -ENOMEM;
        }
    
        while (find_vma_links(mm, addr, addr + len, &prev, &rb_link,
                      &rb_parent)) {-----------------------------------遍历该进程已有的vma红黑树,如果找到vma覆盖[addr, end]区域,那么返回0,表示找到。如果覆盖已有的vma区域,返回ENOMEM。
            if (do_munmap(mm, addr, len))------------------------------存在覆盖已有区域的情况,那么尝试取munmap这块区域。如果munmap成功返回0,不成功则mmap_region()失败。
                return -ENOMEM;
        }
    
        if (accountable_mapping(file, vm_flags)) {
            charged = len >> PAGE_SHIFT;
            if (security_vm_enough_memory_mm(mm, charged))
                return -ENOMEM;
            vm_flags |= VM_ACCOUNT;
        }
    
        vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
                NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);-----------------------至此表示已经可以找到合适的vma区域,原有映射是否可以被新的映射复用,减少因为vma导致的slab消耗和虚拟内存的空洞。
        if (vma)
            goto out;
    
        vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);----------------------在没有找到的情况下,新建一个vma。
        if (!vma) {
            error = -ENOMEM;
            goto unacct_error;
        }
    
        vma->vm_mm = mm;---------------------------------------------------------初始化vma数据
        vma->vm_start = addr;
        vma->vm_end = addr + len;
        vma->vm_flags = vm_flags;
        vma->vm_page_prot = vm_get_page_prot(vm_flags);
        vma->vm_pgoff = pgoff;
        INIT_LIST_HEAD(&vma->anon_vma_chain);
    
        if (file) {--------------------------------------------------------------如果是文件映射
            if (vm_flags & VM_DENYWRITE) {
                error = deny_write_access(file);
                if (error)
                    goto free_vma;
            }
            if (vm_flags & VM_SHARED) {
                error = mapping_map_writable(file->f_mapping);
                if (error)
                    goto allow_write_and_free_vma;
            }
    
            vma->vm_file = get_file(file);
            error = file->f_op->mmap(file, vma);---------------------------------调用文件操作函数集的mmap成员。
            if (error)
                goto unmap_and_free_vma;
    
            WARN_ON_ONCE(addr != vma->vm_start);
    
            addr = vma->vm_start;
            vm_flags = vma->vm_flags;
        } else if (vm_flags & VM_SHARED) {--------------------------------------共享匿名区
            error = shmem_zero_setup(vma);
            if (error)
                goto free_vma;
        }
    
        vma_link(mm, vma, prev, rb_link, rb_parent);----------------------------将新建的vma插入到进程地址空间的vma红黑树中,已经做一些计数更新等。
        /* Once vma denies write, undo our temporary denial count */
        if (file) {
            if (vm_flags & VM_SHARED)
                mapping_unmap_writable(file->f_mapping);
            if (vm_flags & VM_DENYWRITE)
                allow_write_access(file);
        }
        file = vma->vm_file;
    out:
        perf_event_mmap(vma);
    
        vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
        if (vm_flags & VM_LOCKED) {
            if (!((vm_flags & VM_SPECIAL) || is_vm_hugetlb_page(vma) ||
                        vma == get_gate_vma(current->mm)))
                mm->locked_vm += (len >> PAGE_SHIFT);
            else
                vma->vm_flags &= VM_LOCKED_CLEAR_MASK;
        }
    
        if (file)
            uprobe_mmap(vma);
    
        vma->vm_flags |= VM_SOFTDIRTY;
    
        vma_set_page_prot(vma);
    
        return addr;
    
    unmap_and_free_vma:
        vma->vm_file = NULL;
        fput(file);
    
        /* Undo any partial mapping done by a device driver. */
        unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
        charged = 0;
        if (vm_flags & VM_SHARED)
            mapping_unmap_writable(file->f_mapping);
    allow_write_and_free_vma:
        if (vm_flags & VM_DENYWRITE)
            allow_write_access(file);
    free_vma:
        kmem_cache_free(vm_area_cachep, vma);
    unacct_error:
        if (charged)
            vm_unacct_memory(charged);
        return error;
    }

    参考文档:《linux进程地址空间(3) 内存映射(1)mmap与do_mmap》、《进程地址空间 get_unmmapped_area()

    2.2 munmap

    检查目标地址在当前进程的虚拟空间是否已经在使用,如果已经在使用就要将老的映射撤销,要是这个操作失败,则goto free_vma。因为flags的标志位为MAP_FIXED为1时,并未对此检查。

    munmap()用于解除内存映射,其核心函数式do_munmap()。

    SYSCALL_DEFINE2(munmap, unsigned long, addr, size_t, len)
    {
        profile_munmap(addr);
        return vm_munmap(addr, len);
    }
    
    
    int vm_munmap(unsigned long start, size_t len)
    {
        int ret;
        struct mm_struct *mm = current->mm;
    
        down_write(&mm->mmap_sem);
        ret = do_munmap(mm, start, len);
        up_write(&mm->mmap_sem);
        return ret;
    }
    
    int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
    {
        unsigned long end;
        struct vm_area_struct *vma, *prev, *last;
    
        if ((offset_in_page(start)) || start > TASK_SIZE || len > TASK_SIZE-start)
            return -EINVAL;
    
        len = PAGE_ALIGN(len);
        if (len == 0)
            return -EINVAL;
    
        /* Find the first overlapping VMA */
        vma = find_vma(mm, start);-----------------找到起始地址落在哪个vma内。
        if (!vma)----------------------------------如果没有找到的话,直接返回0。
            return 0;
        prev = vma->vm_prev;
    
        end = start + len;
        if (vma->vm_start >= end)------------------如果要释放空间的结束地址都小于vma起始地址,说明这两者没有重叠,直接退出。
            return 0;
    
        if (start > vma->vm_start) {
            int error;
    
            if (end < vma->vm_end && mm->map_count >= sysctl_max_map_count)
                return -ENOMEM;
    
            error = __split_vma(mm, vma, start, 0);----由于start>vma->vm_start,说明要释放空间和vm_start有一段空隙。这里就是分离这段gap。
            if (error)
                return error;
            prev = vma;
        }
    
        last = find_vma(mm, end);----------------------找到要释放空间结束地址的vma。
        if (last && end > last->vm_start) {
            int error = __split_vma(mm, last, end, 1);-如果if成立,说明要释放空间end和vm_start之间有gap,就需要分离这段gap。
            if (error)
                return error;
        }
        vma = prev ? prev->vm_next : mm->mmap;
    
        if (mm->locked_vm) {
            struct vm_area_struct *tmp = vma;
            while (tmp && tmp->vm_start < end) {
                if (tmp->vm_flags & VM_LOCKED) {
                    mm->locked_vm -= vma_pages(tmp);
                    munlock_vma_pages_all(tmp);-------如果这段空间是VM_LOCKED,就需要unlock。
                }
                tmp = tmp->vm_next;
            }
        }
    
        detach_vmas_to_be_unmapped(mm, vma, prev, end);
        unmap_region(mm, vma, prev, start, end);------释放实际占用的页面。
    
        arch_unmap(mm, vma, start, end);
    
        /* Fix up all other VM information */
        remove_vma_list(mm, vma);---------------------删除mm_struct结构中的vma信息。
    
        return 0;
    }
    
    static void unmap_region(struct mm_struct *mm,
            struct vm_area_struct *vma, struct vm_area_struct *prev,
            unsigned long start, unsigned long end)
    {
        struct vm_area_struct *next = prev ? prev->vm_next : mm->mmap;
        struct mmu_gather tlb;
    
        lru_add_drain();
        tlb_gather_mmu(&tlb, mm, start, end);
        update_hiwater_rss(mm);
        unmap_vmas(&tlb, vma, start, end);---------扫描线性地址空间的所有页表项
        free_pgtables(&tlb, vma, prev ? prev->vm_end : FIRST_USER_ADDRESS,
                     next ? next->vm_start : USER_PGTABLES_CEILING);---回收上一步已经清空的进程页表。
        tlb_finish_mmu(&tlb, start, end);----------刷新TLB,在多处理器系统中,调用freepages_and_swap_cache()释放页框。
    }
    
    void unmap_vmas(struct mmu_gather *tlb,
            struct vm_area_struct *vma, unsigned long start_addr,
            unsigned long end_addr)
    {
        struct mm_struct *mm = vma->vm_mm;
    
        mmu_notifier_invalidate_range_start(mm, start_addr, end_addr);
        for ( ; vma && vma->vm_start < end_addr; vma = vma->vm_next)
            unmap_single_vma(tlb, vma, start_addr, end_addr, NULL);
        mmu_notifier_invalidate_range_end(mm, start_addr, end_addr);
    }

    参考文档:《内存管理API之do_munmap》《释放线性地址区间》。

    2.3 msync()

    进程对映射的内存空间内容改变并不直接回写到磁盘中,往往在调用munmap()后才执行操作。

    msync()函数将映射内存空间内容同步到磁盘文件中。

    SYSCALL_DEFINE3(msync, unsigned long, start, size_t, len, int, flags)
    {
        unsigned long end;
        struct mm_struct *mm = current->mm;
        struct vm_area_struct *vma;
        int unmapped_error = 0;
        int error = -EINVAL;
    
        if (flags & ~(MS_ASYNC | MS_INVALIDATE | MS_SYNC))
            goto out;
        if (offset_in_page(start))
            goto out;
        if ((flags & MS_ASYNC) && (flags & MS_SYNC))
            goto out;
        error = -ENOMEM;
        len = (len + ~PAGE_MASK) & PAGE_MASK;
        end = start + len;
        if (end < start)
            goto out;
        error = 0;
        if (end == start)
            goto out;
        /*
         * If the interval [start,end) covers some unmapped address ranges,
         * just ignore them, but return -ENOMEM at the end.
         */
        down_read(&mm->mmap_sem);
        vma = find_vma(mm, start);
        for (;;) {
            struct file *file;
            loff_t fstart, fend;
    
            /* Still start < end. */
            error = -ENOMEM;
            if (!vma)
                goto out_unlock;
            /* Here start < vma->vm_end. */
            if (start < vma->vm_start) {
                start = vma->vm_start;
                if (start >= end)
                    goto out_unlock;
                unmapped_error = -ENOMEM;
            }
            /* Here vma->vm_start <= start < vma->vm_end. */
            if ((flags & MS_INVALIDATE) &&
                    (vma->vm_flags & VM_LOCKED)) {
                error = -EBUSY;
                goto out_unlock;
            }
            file = vma->vm_file;
            fstart = (start - vma->vm_start) +
                 ((loff_t)vma->vm_pgoff << PAGE_SHIFT);
            fend = fstart + (min(end, vma->vm_end) - start) - 1;
            start = vma->vm_end;
            if ((flags & MS_SYNC) && file &&
                    (vma->vm_flags & VM_SHARED)) {
                get_file(file);
                up_read(&mm->mmap_sem);
                error = vfs_fsync_range(file, fstart, fend, 1);
                fput(file);
                if (error || start >= end)
                    goto out;
                down_read(&mm->mmap_sem);
                vma = find_vma(mm, start);
            } else {
                if (start >= end) {
                    error = 0;
                    goto out_unlock;
                }
                vma = vma->vm_next;
            }
        }
    out_unlock:
        up_read(&mm->mmap_sem);
    out:
        return error ? : unmapped_error;
    }
    
    int vfs_fsync_range(struct file *file, loff_t start, loff_t end, int datasync)
    {
        struct inode *inode = file->f_mapping->host;
    
        if (!file->f_op->fsync)
            return -EINVAL;
        if (!datasync && (inode->i_state & I_DIRTY_TIME)) {
            spin_lock(&inode->i_lock);
            inode->i_state &= ~I_DIRTY_TIME;
            spin_unlock(&inode->i_lock);
            mark_inode_dirty_sync(inode);
        }
        return file->f_op->fsync(file, start, end, datasync);
    }

    2.4 malloc和brk()/mmap()关系

    通过getconf PAGESIZE查看当前系统页面大小,可知当前系统页面大小为4096。

    malloc()分配内存,并不一定都通过brk()进行;如果分配的内存达到128K,就要通过mmap进行。

    #include<unistd.h>
    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<sys/mman.h>
    
    #define MAX (4096*31+4072)
    
    int main()
    {
        int i=0;
        char *array = (char *)malloc(MAX);
    
        for( i=0; i<MAX; ++i )
            ++array[ i ];
    
        free(array);
    
        return 0;
    }

    下面就来看看MAX不同大小,对malloc的影响。

    当MAX为(4096*31+4072)时,跟踪系统调用如下:

    ...
    brk(0x244c000) = 0x244c000
    brk(0x242c000) = 0x242c000
    exit_group(0) = ?
    +++ exited with 0 +++

    当MAX为(4096*31+4073)时,跟踪系统调用如下:

    ...
    mmap(NULL, 135168, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f12b88c9000
    munmap(0x7f12b88c9000, 135168) = 0
    exit_group(0) = ?
    +++ exited with 0 +++

    可以看出当分配的内存接近128KB是,malloc()会对齐到128KB,并且附加了1页作为gap。实际分配的虚拟地址空间达到了132kB。

    3. mmap测试

    3.1 mmap()/munmap()相对于read()/write()优势

    上面有提到mmap()后对内存的操作相对于普通的read()/write()速度更快,这里进行一个简单测试。

    #include<unistd.h>
    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<sys/time.h>
    #include<fcntl.h>
    #include<sys/mman.h>
    
    #define MAX 1024*128
    
    int main()
    {
        int i=0;
        int count=0, fd=0;
        struct timeval tv1, tv2;
        char *array = (char *)malloc(MAX);
    
        /*read*/
        gettimeofday( &tv1, NULL );
        fd = open( "./mmap_test", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
        if(fd<0)
            printf("Open file failed
    ");
        if(MAX != read( fd, (char*)array, MAX ))
        {
            printf("Reading data failed...
    ");
            return -1;
        }
    
        memset(array, 'a', MAX);
    
        lseek(fd,0,SEEK_SET);
        if(MAX != write(fd, (void *)array, MAX))
        {
            printf( "Writing data failed...
    " );
            return -1;
        }
        close( fd );
        gettimeofday( &tv2, NULL );
        free( array );
    
        printf( "Time of read/write: %ldus
    ", (tv2.tv_usec - tv1.tv_usec));
    
        /*mmap*/
        gettimeofday( &tv1, NULL );
        fd = open( "./mmap_test2", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
        array = mmap( NULL, MAX, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    
        memset(array, 'b', MAX);
    
        munmap( array, MAX );
        msync( array, MAX, MS_SYNC );
        close( fd );
        gettimeofday( &tv2, NULL );
    
        printf( "Time of mmap/munmap/msync: %ldus
    ", (tv2.tv_usec - tv1.tv_usec));
    
        return 0;
    }

     首先创建两个128KB的空文件。

    dd bs=1024 count=128 if=/dev/zero of=./mmap_test

    dd bs=1024 count=128 if=/dev/zero of=./mmap_test2

    两个文件内容分别变成了'A'和'B',可以看出mmap领先不少:

    Time of read/write: 134us
    Time of mmap/munmap/msync: 91us

    3.2 mmap和/proc/xxx/maps解析

    #include<stdio.h>
    #include<unistd.h>
    
    void main()
    {
        sleep(1000);
    }

    通过strace执行如上应用,得到如下的系统调用过程。

    execve("./sleep", ["./sleep"], [/* 77 vars */]) = 0
    brk(NULL)                               = 0x1286000
    access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
    access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
    open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
    fstat(3, {st_mode=S_IFREG|0644, st_size=145720, ...}) = 0
    mmap(NULL, 145720, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa2e0dec000--------------------------------------------------------1,只读私有文件映射,在a处释放。
    close(3)                                = 0
    access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
    open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
    read(3, "177ELF21133>1P	2"..., 832) = 832
    fstat(3, {st_mode=S_IFREG|0755, st_size=1868984, ...}) = 0
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa2e0deb000--------------------------------2,匿名映射一页,范围0x7fa2e0deb000-0x7fa2e0dec000,可读写
    mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fa2e0821000-------------------------------3,创建可读可执行,私有文件映射,范围0x7fa2e0821000-0x7fa2e0beb000
    mprotect(0x7fa2e09e1000, 2097152, PROT_NONE) = 0-------------------------------------------------------------------------4,修改0x7fa2e09e1000-0x7fa2e0be1000属性,不可读写执行
    mmap(0x7fa2e0be1000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7fa2e0be1000-----5,私有文件固定地址映射,可读写,0x7fa2e0be1000-0x7fa2e0be7000
    mmap(0x7fa2e0be7000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fa2e0be7000-----------6,私有匿名固定地址映射,可读写,0x7fa2e0be7000-0x7fa2e0beb000
    close(3)                                = 0
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa2e0dea000--------------------------------7,匿名映射一页,范围0x7fa2e0dea000-0x7fa2e0deb000,可读写
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa2e0de9000--------------------------------8,匿名映射一页,范围0x7fa2e0de9000-0x7fa2e0dea000,可读写
    arch_prctl(ARCH_SET_FS, 0x7fa2e0dea700) = 0
    mprotect(0x7fa2e0be1000, 16384, PROT_READ) = 0---------------------------------------------------------------------------9,将5创建的内存映射的0x7fa2e0be1000-0x7fa2e0be5000变成只读
    mprotect(0x600000, 4096, PROT_READ)     = 0
    mprotect(0x7fa2e0e10000, 4096, PROT_READ) = 0
    munmap(0x7fa2e0dec000, 145720)          = 0------------------------------------------------------------------------------a,释放1创建的内存映射
    nanosleep({1000, 0}, 0x7ffef87e2c10)    = 0------------------------------------------------------------------------------此时cat /proc/xxx/maps,1创建的内存映射已经被释放。
    exit_group(0)                           = ?
    +++ exited with 0 +++

     下面逐一分析mmap()/munmap()对进程映射空间的影响。

    00400000-00401000 r-xp 00000000 08:08 3415949                            /home/al/mmap/sleep
    00600000-00601000 r--p 00000000 08:08 3415949                            /home/al/mmap/sleep
    00601000-00602000 rw-p 00001000 08:08 3415949                            /home/al/mmap/sleep
    7fa2e0821000-7fa2e09e1000 r-xp 00000000 08:08 3185985                    /lib/x86_64-linux-gnu/libc-2.23.so--------------3创建私有文件映射,可读可执行。
    7fa2e09e1000-7fa2e0be1000 ---p 001c0000 08:08 3185985                    /lib/x86_64-linux-gnu/libc-2.23.so--------------3创建私有文件映射,4修改属性从可读可执行变成不可读写不可执行。
    7fa2e0be1000-7fa2e0be5000 r--p 001c0000 08:08 3185985                    /lib/x86_64-linux-gnu/libc-2.23.so--------------3创建私有文件映射,5修改属性从可读可执行变成可读写,9修改属性为只读。
    7fa2e0be5000-7fa2e0be7000 rw-p 001c4000 08:08 3185985                    /lib/x86_64-linux-gnu/libc-2.23.so--------------3创建私有文件映射,5修改属性从可读可执行变成可读写。
    7fa2e0be7000-7fa2e0beb000 rw-p 00000000 00:00 0 -------------------------------------------------------------------------3创建私有文件映射,6覆盖创建的私有匿名固定地址映射,可读写。
    7fa2e0beb000-7fa2e0c11000 r-xp 00000000 08:08 3185983                    /lib/x86_64-linux-gnu/ld-2.23.so
    7fa2e0de9000-7fa2e0dec000 rw-p 00000000 00:00 0 -------------------------------------------------------------------------2,7,8三个匿名映射因为属性都是私有匿名映射,可读写,所以vma区域合并。
    7fa2e0e10000-7fa2e0e11000 r--p 00025000 08:08 3185983                    /lib/x86_64-linux-gnu/ld-2.23.so
    7fa2e0e11000-7fa2e0e12000 rw-p 00026000 08:08 3185983                    /lib/x86_64-linux-gnu/ld-2.23.so
    7fa2e0e12000-7fa2e0e13000 rw-p 00000000 00:00 0 
    7ffef87c3000-7ffef87e4000 rw-p 00000000 00:00 0                          [stack]
    7ffef87e4000-7ffef87e7000 r--p 00000000 00:00 0                          [vvar]
    7ffef87e7000-7ffef87e9000 r-xp 00000000 00:00 0                          [vdso]
    ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

     对于解释可以参考UNIX系统编程手册如下描述。

    4. 参考文档

    LINUX 中的mmap浅析

    《linux内存映射mmap原理分析

    Linux内存管理之mmap详解

    Linux的mmap内存映射机制解析

    Linux中的mmap的使用

    Linux内核源代码情景分析-系统调用mmap()

  • 相关阅读:
    北京,第七天
    北京!北京!第五天
    关于模板中的动态取值 ---反射与javascript脚本编译
    InstallShield Limited Edition for Visual Studio 2013
    使用WebFrom来模拟一些MVC的MODEL与View的数据交互功能
    关于.net 对excel操作的方法
    关于Aspose对于Word操作的一些扩展及思考
    各种注释与取消注释的快捷键
    安装Vivado时遇到的问题及解决
    apache pdfbox简单读取内容
  • 原文地址:https://www.cnblogs.com/arnoldlu/p/9367253.html
Copyright © 2011-2022 走看看