mmap内核源码分析

对于mmap函数,我之前的理解太单一了。这几天好好复习了一下以前学过的知识,重新对该函数有了新的认识。

之前我的认识是,mmap是用来映射内存的,它映射的内存来自磁盘上文件。所以我以为malloc函数底层也映射文件内存。后来一直想不通。

实际上,mmap函数再malloc底层实现中采用了匿名映射(就是这个匿名映射,我之前一直概念不清)。

先说下malloc调用mmap一般的形式:

//原型
//mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);

addr = mmap(NULL, 4096, PROT_READ|PORT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

对于malloc映射匿名内存来说,必须是以页为单位的,比如上面的4096。用户进程向内核空间分配内存都是直接向伙伴系统要的。在此基础上glibc将内存细化为可以按照字节分配的方式。

匿名内存的显著特征,MAP_ANONYMOUS,以及文件描述符fd传递-1。

下面开始剖析源码,看看匿名内存与文件映射有什么不一样。

mmap的系统调用时sys_mmap2,实际上就是一个简单转调用:

asmlinkage long sys_mmap2(unsigned long addr, unsigned long len,
    unsigned long prot, unsigned long flags,
    unsigned long fd, unsigned long pgoff)
{
    return do_mmap2(addr, len, prot, flags, fd, pgoff);
}

do_mmap2,代码如下:‘

static inline long do_mmap2(
    unsigned long addr, unsigned long len,
    unsigned long prot, unsigned long flags,
    unsigned long fd, unsigned long pgoff)
{
    int error = -EBADF;
    struct file * file = NULL;  //呵呵,注意这里file指针初始为NULL

    flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE);
    if (!(flags & MAP_ANONYMOUS)) {//MAP_ANONYMOUS设成1,表示没有文件,实际上只是用来"圈地"
        file = fget(fd);//获取file结构
        if (!file)
            goto out;
    }

//所以上面的步骤,如果我们设置了MAP_ANONYMOUS,那么不会用fd获取实际的文件,以后file指针仍然为NULL

    down(t->mm->mmap_sem);
    error = do_mmap_pgoff(file, addr, len, prot, flags, pgoff);  //传入file=NULL
    up(¤t->mm->mmap_sem);

    if (file)
        fput(file);
out:
    return error;
}

inline函数do_mmap(),是供内核自己用的,它也是将已打开文件映射到当前进程空间。代码为:

static inline unsigned long do_mmap(struct file *file, unsigned long addr,
    unsigned long len, unsigned long prot,
    unsigned long flag, unsigned long offset)
{
    unsigned long ret = -EINVAL;
    if ((offset + PAGE_ALIGN(len)) < offset)
        goto out;
    if (!(offset & ~PAGE_MASK))
        ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);  //没得说,file还是NULL
out:
    return ret;
}

两者都调用,do_mmap_pgoff,代码如下:

unsigned long do_mmap_pgoff(struct file * file, unsigned long addr, unsigned long len,
    unsigned long prot, unsigned long flags, unsigned long pgoff)
{
    struct mm_struct * mm = current->mm;
    struct vm_area_struct * vma;
    int correct_wcount = 0;
    int error;

    .....//各种判断,先忽略
    if (flags & MAP_FIXED) {
        if (addr & ~PAGE_MASK)
            return -EINVAL;
    } else {//MAP_FIXED为0,就表示指定的映射地址只是一个参考值,不能满足时可以由内核给分配一个
        addr = get_unmapped_area(addr, len);//当前进程的用户空间中分配一个起始地址
        if (!addr)
            return -ENOMEM;
    }

    /* Determine the object being mapped and call the appropriate
     * specific mapper. the address has already been validated, but
     * not unmapped, but the maps are removed from the list.
     */
    vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);//映射到一个特定的文件也是一种属性,属性不同的区段不能共存于同一逻辑区间,所以总要为之单独建立一个逻辑区间
    if (!vma)
        return -ENOMEM;

    vma->vm_mm = mm;
    vma->vm_start = addr;//起始地址
    vma->vm_end = addr + len;//结束地址
    vma->vm_flags = vm_flags(prot,flags) | mm->def_flags;

    if (file) {//设置vma->flags
        VM_ClearReadHint(vma);
        vma->vm_raend = 0;

        if (file->f_mode & FMODE_READ)
            vma->vm_flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
        if (flags & MAP_SHARED) {
            vma->vm_flags |= VM_SHARED | VM_MAYSHARE;

            /* This looks strange, but when we don't have the file open
             * for writing, we can demote the shared mapping to a simpler
             * private mapping. That also takes care of a security hole
             * with ptrace() writing to a shared mapping without write
             * permissions.
             *
             * We leave the VM_MAYSHARE bit on, just to get correct output
             * from /proc/xxx/maps..
             */
            if (!(file->f_mode & FMODE_WRITE))
                vma->vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
        }
    } else {
        vma->vm_flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
        if (flags & MAP_SHARED)
            vma->vm_flags |= VM_SHARED | VM_MAYSHARE;
    }
    vma->vm_page_prot = protection_map[vma->vm_flags & 0x0f];
    vma->vm_ops = NULL;
    vma->vm_pgoff = pgoff;//所映射内容在文件中的起点,有了这个起点,发生缺页异常时,就可以根据虚拟地址计算出相应页面在文件中的位置
    vma->vm_file = NULL;
    vma->vm_private_data = NULL;

    /* Clear old maps */
    error = -ENOMEM;
    if (do_munmap(mm, addr, len))//检查目标地址在当前进程的虚拟空间是否已经在使用,如果已经在使用就要将老的映射撤销,要是这个操作失败,则goto free_vma。因为flags的标志位为MAP_FIXED为1时,并未对此检查。
        goto free_vma;

    /* Check against address space limit. */
    if ((mm->total_vm << PAGE_SHIFT) + len //虚拟空间的使用是否超出了为其设置的下限
        > current->rlim[RLIMIT_AS].rlim_cur)
        goto free_vma;

    /* Private writable mapping? Check memory availability.. */
    if ((vma->vm_flags & (VM_SHARED | VM_WRITE)) == VM_WRITE &&//物理页面数是否够
        !(flags & MAP_NORESERVE)                 &&
        !vm_enough_memory(len >> PAGE_SHIFT))
        goto free_vma;

    if (file) {
        if (vma->vm_flags & VM_DENYWRITE) {
            error = deny_write_access(file);//排斥常规文件操作,如read write 
            if (error)
                goto free_vma;
            correct_wcount = 1;
        }
        vma->vm_file = file;//重点哦
        get_file(file);
        error = file->f_op->mmap(file, vma);//指向了generic_file_mmap
        if (error)
            goto unmap_and_free_vma;
    } else if (flags & MAP_SHARED) {
        error = shmem_zero_setup(vma);
        if (error)
            goto free_vma;
    }

    /* Can addr have changed??
     *
     * Answer: Yes, several device drivers can do it in their
     *         f_op->mmap method. -DaveM
     */
    flags = vma->vm_flags;
    addr = vma->vm_start;

    insert_vm_struct(mm, vma);//插入到对应的队列中
    if (correct_wcount)
        atomic_inc(&file->f_dentry->d_inode->i_writecount);

    mm->total_vm += len >> PAGE_SHIFT;
    if (flags & VM_LOCKED) {//仅在加锁时才调用make_pages_present
        mm->locked_vm += len >> PAGE_SHIFT;
        make_pages_present(addr, addr + len);
    }
    return addr;//最后返回的起始虚拟地址,一般是后12位为0

unmap_and_free_vma:
    if (correct_wcount)
        atomic_inc(&file->f_dentry->d_inode->i_writecount);
    vma->vm_file = NULL;
    fput(file);
    /* Undo any partial mapping done by a device driver. */
    flush_cache_range(mm, vma->vm_start, vma->vm_end);
    zap_page_range(mm, vma->vm_start, vma->vm_end - vma->vm_start);
    flush_tlb_range(mm, vma->vm_start, vma->vm_end);
free_vma:
    kmem_cache_free(vm_area_cachep, vma);
    return error;
}

哈哈,我关注的重点来了,这个函数get_unmapped_area,是用来给进程找到一块VMA的,来看看它干了什么:

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

    get_area = current->mm->get_unmapped_area;  //默认使用当前进程虚存管理对应的get_unmapped_ared函数,这里只是从进程地址空间获得VMA
    if (file && file->f_op && file->f_op->get_unmapped_area)   //匿名映射文件指针依旧为空
        get_area = file->f_op->get_unmapped_area;  //如果文件指针不为NULL,使用文件对应的get_unmapped_area函数,这里就是从文件获得VMA
    addr = get_area(file, addr, len, pgoff, flags);
    if (IS_ERR_VALUE(addr))
        return addr;

    if (addr > TASK_SIZE - len)
        return -ENOMEM;
    if (addr & ~PAGE_MASK)  //PAGE_MASK低12位都是0,这里是用来检测是否是整页面,如果不是则出错
        return -EINVAL;

    return arch_rebalance_pgtables(addr, len);
}

唉,真相大白。匿名映射就是file=NULL。

get_unmmaped_ared函数解开了我的疑惑,该函数实现的内核对于匿名文件映射和文件映射的选择。使用函数指针,要么赋值mm->get_unmapped_area从进程地址空间获得VMA,要么从file->f_op->get_unmapped_ared获得VMA。

并没有做实际的内存分配,只是简单的获取了一片VMA。获取VMA是调用find_vma()函数在vma_ared_struct的双链表中查找,并且匿名映射还有可能和紧挨着的VMA合并。

当CPU第一个引用mmap区域的页面时,会引发缺页中断,内核会在物理存储器中找到一个合适的牺牲页面,如果该页面被修改过,就将这个页面换出来,用二进制零覆盖牺牲页面并更新页表,将这个页面标记为是驻留在存储器中的。注意在磁盘和存储器之间并没有实际的数据传送。因为这个原因,映射到匿名文件的区域的页面有时也叫做请求二进制零的页(demand zero page)。

另外,由于使用mmap分配内存,内核需要清零页面,并且mmap分配的内存都是页对齐的,所以使用mmap有一定的消耗。所以glibc才设定大于128k使用mmap,一般使用sbrk()函数分配内存。

参考:

  1. Linux内核源代码情景分析-系统调用mmap()
  2. Linux内核分析之进程地址空间
  3. linux mmap函数详解
  4. CSAPP, Randal E.Byant David R.O.Hallaron著,龚奕利,雷迎春译。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值