关闭

系统调用mmap--使用详解

标签: Linux编程
1125人阅读 评论(0) 收藏 举报
分类:

本文原始资料来源于http://blog.chinaunix.net/uid-24517893-id-164217.html,本人只插了两张图,仅供学习参考。

一、mmap的用户层应用

void* mmap ( void *start , size_t len , int prot , int flags ,int fd , off_t offset )
内存映射函数mmap, 负责把文件内容映射到进程的虚拟内存空间, 通过对这段内存的读取和修改,来实现对文件的读取和修改,而不需要再调用read,write等操作。



具体参数含义:

start指定映射的起始地址, 通常设为NULL,代表让系统自动选定地址,映射成功后返回该地址。
length:映射到内存的文件长度。
prot:映射区的保护方式, 可以为以下几种方式的组合:
                    PROT_EXEC 映射区域可被执行
                    PROT_READ 映射区域可被读取
                    PROT_WRITE 映射区域可被写入
                    PROT_NONE 映射区域不能存取

flags :影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
        MAP_SHARED 写入映射区的数据会复制回文件, 且允许其他映射该文件的进程共享。
        MAP_PRIVATE对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件。
        MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。

MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。

        MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
        AP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。
fd   :由open返回的文件描述符, 代表要映射的文件。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,然后对该文件进行映射,可以同样达到匿名内存映         射的效果。
offset:以文件开始处的偏移量, 必须是分页大小的整数倍, 通常为0, 表示从文件头开始映射。

返回值:若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。

错误代码:
            EBADF  参数fd 不是有效的文件描述词
            EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能    写入。
            EINVAL 参数start、length 或offset有一个不合法。
            EAGAIN 文件被锁住,或是有太多内存被锁住。
            ENOMEM 内存不足。
用户层的调用很简单,其具体功能就是直接将物理内存直接映射到用户虚拟内存,使用户空间可以直接对物理空间操作。但是对于内核层而言,其具体实现比较复杂。

解除映射

int munmap(void *start,size_t length)
功能:
取消参数start所指向的映射内存,参数length表示欲取消的内存大小。start即mmap的返回值
返回值:
解除成功返回0,否则返回-1,错误原因存于errno中。

代码示例:



二、mmap的内核实现
对于mmap的内核有了解的都会知道用户层的mmap到内核层的mmap其中多了一个参数vma_struct这个结构体,在开始时对于这个参数很疑惑就是这个参数的值是哪儿来的,
在这里我们会一一来讲述。
还是从do_mmap开始吧。
2.1 do_mmap
参数说明:
file  :就是用户层想要映射的file
addr  :欲映射的起始地址,即用户层的start
prot  :用户层传入的port
flag  :同上
offset:同上
从这里可以知道,这里面的参数几乎均是用户层传入的参数。
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)  --页对齐len,检测传入参数是否有误。
        goto out;
    if (!(offset & ~PAGE_MASK))           --检测offset是否页对齐。映射时只能映射页对齐的长度。
        ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);  
out:
    return ret;
}

3.2 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;      --当前用户进程的mm
    struct inode *inode; 
    unsigned int vm_flags;
    int error;
    int accountable = 1;
    unsigned long reqprot = prot;

    if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))   --是否隐藏了可执行属性。
        if (!(file && (file->f_path.mnt->mnt_flags & MNT_NOEXEC)))
            prot |= PROT_EXEC;

    if (!len)         
        return -EINVAL;

    if (!(flags & MAP_FIXED))              -  
        addr = round_hint_to_min(addr);    --判断输入的欲映射的起始地址是否小于最小映射地址,如果小于,将addr修改为最小地址,不过前提是MAP_FIXED旗标没有设置。

    error = arch_mmap_check(addr, len, flags);   --不同平台对于mmap参数的不同检测。这里之间返回0
    if (error)
        return error;

    len = PAGE_ALIGN(len);        --检测len是否越界,len的范围在0~TASK_SIZE之间。
    if (!len || len > TASK_SIZE)
        return -ENOMEM;             --错误值为nomem

    if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)  --再次检测是否越界。我们这里不得不小心哪个晕头了传入一个莫名其妙的值
    return -EOVERFLOW;

    if (mm->map_count > sysctl_max_map_count)   --在一个进程中对于mmap个数是有限制的。超出了还是nomem的错误。
        return -ENOMEM;

    addr = get_unmapped_area(file, addr, len, pgoff, flags);  --获取没有映射的地址,这个是查询mm中空闲的内存地址,这个在下面理解。
    if (addr & ~PAGE_MASK)
        return addr;

    vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) | mm->def_flags |
               VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;      --设置vm_flags,根据传入的port和flags以及mm本身自有的旗标来设置。

    if (flags & MAP_LOCKED) {       
        if (!can_do_mlock())         
            return -EPERM;
        vm_flags |= VM_LOCKED;
    }

    if (vm_flags & VM_LOCKED) {
        unsigned long locked, lock_limit;
        locked = len >> PAGE_SHIFT;
        locked += mm->locked_vm;
        lock_limit = current->signal->rlim[RLIMIT_MEMLOCK].rlim_cur;
        lock_limit >>= PAGE_SHIFT;
        if (locked > lock_limit && !capable(CAP_IPC_LOCK))
            return -EAGAIN;
    }
  --关于锁定的内存区在以后学习中再看,这里就不细看。
    inode = file ? file->f_path.dentry->d_inode : NULL;  --判断是否匿名映射,如果不是则赋值inode

    if (file) {
        switch (flags & MAP_TYPE) {   --MAP_TYPE = 0x0F type的掩码
        case MAP_SHARED:
            if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE))   --file应该被打开并允许写入。
                return -EACCES;
            if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))  --不能写入一个只允许写追加的文件
                return -EACCES;
            if (locks_verify_locked(inode))      --确保文件没有被强制锁定。
                return -EAGAIN;

            vm_flags |= VM_SHARED | VM_MAYSHARE;  --尝试允许其他进程共享。
            if (!(file->f_mode & FMODE_WRITE))    --如果file不允许写就算了,共享也没有用啊,因为file就一直固定死了,共享也没有意义。
                vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
        case MAP_PRIVATE:
            if (!(file->f_mode & FMODE_READ))
                return -EACCES;
            if (file->f_path.mnt->mnt_flags & MNT_NOEXEC) {
                if (vm_flags & VM_EXEC)
                    return -EPERM;
                vm_flags &= ~VM_MAYEXEC;
            }
            if (is_file_hugepages(file))
                accountable = 0;

            if (!file->f_op || !file->f_op->mmap)
                return -ENODEV;
            break;

        default:
            return -EINVAL;
        }
    } else {
        switch (flags & MAP_TYPE) {
        case MAP_SHARED:
            pgoff = 0;
            vm_flags |= VM_SHARED | VM_MAYSHARE;
            break;
        case MAP_PRIVATE:
            pgoff = addr >> PAGE_SHIFT;
            break;
        default:
            return -EINVAL;
        }
    }
  --上面就是对一些旗标进行检测,防止出现旗标冲突,比如我欲映射的文件不允许写,而我映射的旗标却设定是可写并可以共享的,这个就冲突了。
    error = security_file_mmap(file, reqprot, prot, flags, addr, 0);   --这个函数就忽略了。
    if (error)
        return error;

    return mmap_region(file, addr, len, flags, vm_flags, pgoff,accountable);  --最后一个参数为是否为大页,如果是的就为0.其余的参数都好理解。
}

3.3 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);
    get_area = current->mm->get_unmapped_area;
    if (file && file->f_op && file->f_op->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 (addr & ~PAGE_MASK)
        return -EINVAL;

    return arch_rebalance_pgtables(addr, len);
}
对于get_area函数我们以arch_get_unmapped_area为例来看如何查找一个空闲的mmap area
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;
    unsigned long start_addr;

    if (len > TASK_SIZE)
        return -ENOMEM;

    if (flags & MAP_FIXED)    --还记否这个MAP_FIXED是什么含义不?
        return addr;

    if (addr) {
        addr = PAGE_ALIGN(addr);
        vma = find_vma(mm, addr); --vma为NULL即addr的地址不在任一个VMA(vma->vm_start~vma->vm_end) addr的地址没有被映射,
                                    而且空洞足够我们这次的映射,那么返回addr以准备这次的映射
        if (TASK_SIZE - len >= addr &&(!vma || addr + len <= vma->vm_start))
            return addr;
    }
    if (len > mm->cached_hole_size) { --如果所需的长度大于当前vma之间的空洞长度
            start_addr = addr = mm->free_area_cache;
    } else {
            start_addr = addr = TASK_UNMAPPED_BASE;  --需要的长度小于当前空洞,为了不至于时间浪费,那么从0开始搜寻,
                                                       这里的搜寻基地址TASK_UNMAPPED_BASE很重要,用户mmap的地址的基地址必须在TASK_UNMAPPED_BASE之上,
                                                       但是一定这样严格 吗?看上面的if (addr)判断,如果用户给了一个地址在TASK_UNMAPPED_BASE之下,
                                                       映射实际上还是会发生的。
            mm->cached_hole_size = 0;
    }

full_search:
    for (vma = find_vma(mm, addr); ; vma = vma->vm_next) {
        if (TASK_SIZE - len < addr) {
            if (start_addr != TASK_UNMAPPED_BASE) {
                addr = TASK_UNMAPPED_BASE;
              start_addr = addr;
                mm->cached_hole_size = 0;
                goto full_search;
            }
            return -ENOMEM;
        }
    
        if (!vma || addr + len <= vma->vm_start) {        --如果第一次find_vma返回值即为NULL ,vma没有被映射并且空洞足够映射
                                                        !vma的条件只有可能在循环的第一次满足,在其后不可能满足,在其后的判断条件即为
                                                         vma->vma_end~vma->vma_next->vma_start之间的空洞大小大于所需要映射的长度即可,
                                                         下面判断条件中的addr为vma->vma_end,而vma->vm_start为vma->vma_next->vma_start
            mm->free_area_cache = addr + len;
            return addr;
        }
        if (addr + mm->cached_hole_size < vma->vm_start)  --在循环的第一次如果vma不为NULL,不会满足下面的条件,在以后循环中mm->cached_hole_size 
                                                            则为该次vma->vm_start 与上一次的vma->vm_end之间的差值

                mm->cached_hole_size = vma->vm_start - addr;
        addr = vma->vm_end;
    }
}
还记否以前看的红黑树,这里就现实的用了红黑树的算法。关于这个我们就不看了。
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
{
    struct vm_area_struct *vma = NULL;

    if (mm) {
        vma = mm->mmap_cache;
        if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
            struct rb_node * rb_node;
            rb_node = mm->mm_rb.rb_node;
            vma = NULL;
            while (rb_node) {
                struct vm_area_struct * vma_tmp;

                vma_tmp = rb_entry(rb_node,struct vm_area_struct, vm_rb);
                if (vma_tmp->vm_end > addr) {
                    vma = vma_tmp;
                    if (vma_tmp->vm_start <= addr)
                        break;
                    rb_node = rb_node->rb_left;
                } else
                    rb_node = rb_node->rb_right;
            }
            if (vma)
                mm->mmap_cache = vma;
        }
    }
    return vma;
}

3.4 mmap_region
unsigned long mmap_region(struct file *file, unsigned long addr,unsigned long len, unsigned long flags,
                                unsigned int vm_flags, unsigned long pgoff,int accountable)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma, *prev;
    struct vm_area_struct *merged_vma;
    int correct_wcount = 0;
    int error;
    struct rb_node **rb_link, *rb_parent;
    unsigned long charged = 0;
    struct inode *inode =  file ? file->f_path.dentry->d_inode : NULL;

    /* Clear old maps */
    error = -ENOMEM;
munmap_back:
    vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent); --函数find_vma_prepare()与find_vma()基本相同,它扫描当前进程地址空间的vm_area_struct
                                                                     结构所形成的红黑树,试图找到结束地址高于addr的第一个区间;如果找到了一个虚拟区,
                                                                     说明addr所在的虚拟区已经在使用,也就是已经有映射存在,因此要调用do_munmap()
                                                                     把这个老的虚拟区从进程地址空间中撤销,如果撤销不成功,就返回一个负数;
                                                                     如果撤销成功,就继续查找,直到在红黑树中找不到addr所在的虚拟区
    if (vma && vma->vm_start < addr + len) {
        if (do_munmap(mm, addr, len))
            return -ENOMEM;
        goto munmap_back;
    }
    if (!may_expand_vm(mm, len >> PAGE_SHIFT))                   -- 页数和超过限定值返回 0 ,不超过返回1
        return -ENOMEM;

    if (flags & MAP_NORESERVE)                             -- 如果flags参数中没有设置MAP_NORESERVE标志,新的虚拟区含有私有的可写页,空闲页面数小于要映射的虚拟区
                                                              的大小;则函数终止并返回一个负数;其中函数security_vm_enough_memory()用来检查一个
                                                              进程的地址空间中是否有足够的内存来进行一个新的映射
        vm_flags |= VM_NORESERVE;

    if (accountable && (!(flags & MAP_NORESERVE) ||
                sysctl_overcommit_memory == OVERCOMMIT_NEVER)) {
        if (vm_flags & VM_SHARED) {
            /* Check memory availability in shmem_file_setup? */
            vm_flags |= VM_ACCOUNT;
        } else if (vm_flags & VM_WRITE) {
            charged = len >> PAGE_SHIFT;
            if (security_vm_enough_memory(charged))
                return -ENOMEM;
            vm_flags |= VM_ACCOUNT;
        }
    }
    if (!file && !(vm_flags & VM_SHARED)) { --如果是匿名映射(file为空),并且这个虚拟区是非共享的,则可以把这个虚拟区和与它紧挨的前一个虚拟区进行合并;
                                              虚拟区的合并是由vma_merge()函数实现的。如果合并成功,则转out处,请看后面out处的代码。
        vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
                    NULL, NULL, pgoff, NULL);
        if (vma)
            goto out;
    }
    vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
    if (!vma) {
        error = -ENOMEM;
        goto unacct_error;
    }

    vma->vm_mm = mm;
    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;

    if (file) {
        error = -EINVAL;
        if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
            goto free_vma;
        if (vm_flags & VM_DENYWRITE) {
            error = deny_write_access(file);
            if (error)
                goto free_vma;
            correct_wcount = 1;
        }
        vma->vm_file = file;
        get_file(file);
        error = file->f_op->mmap(file, vma);    -- (⊙o⊙)哦 ,终于可以调用设备文件中真正的mmap
        if (error)
            goto unmap_and_free_vma;
        if (vm_flags & VM_EXECUTABLE)
            added_exe_file_vma(mm);
    } else if (vm_flags & VM_SHARED) {
        error = shmem_zero_setup(vma);
        if (error)
            goto free_vma;
    }

如果建立的是从文件到虚存区间的映射,则:
1.当参数flags中的VM_GROWSDOWN或VM_GROWSUP标志位为1时,说明这个区间可以向低地址或高地址扩展,但从文件映射的区间不能进行扩展,因此转到free_vma,释放给vm_area_struct分配的Slab,并返回一个错误;
2.当flags中的VM_DENYWRITE标志位为1时,就表示不允许通过常规的文件操作访问该文件,所以要调用deny_write_access()排斥常规的文件操作(参见第八章)。
3.get_file()函数的主要作用是递增file结构中的共享计数;
4.每个文件系统都有个fiel_operation数据结构,其中的函数指针mmap提供了用来建立从该类文件到虚存区间进行映射的操作,这是最具有实质意义的函数;对于大部分文件系统,这个函数为generic_file_mmap( )函数实现的,该函数执行以下操作:
        (1)初始化vm_area_struct结构中的vm_ops域。如果VM_SHARED标志为1,就把该域设置成file_shared_mmap,否则就把该域设置成file_private_mmap。从某种意义上说,这个步骤所做的事情类似于打开一个文件并初始化文件对象的方法。
        (2)从索引节点的i_mode域(参见第八章)检查要映射的文件是否是一个常规文件。如果是其他类型的文件(例如目录或套接字),就返回一个错误代码。
        (3)从索引节点的i_op域中检查是否定义了readpage( )的索引节点操作。如果没有定义,就返回一个错误代码。
        (4)调用update_atime( )函数把当前时间存放在该文件索引节点的i_atime域中,并将这个索引节点标记成脏。
5.如果flags参数中的MAP_SHARED标志位为1,则调用shmem_zero_setup()进行共享内存的映射。

    if ((vm_flags & (VM_SHARED|VM_ACCOUNT)) == (VM_SHARED|VM_ACCOUNT))
        vma->vm_flags &= ~VM_ACCOUNT;

    addr = vma->vm_start;
    pgoff = vma->vm_pgoff;
    vm_flags = vma->vm_flags;

    if (vma_wants_writenotify(vma))
        vma->vm_page_prot = vm_get_page_prot(vm_flags & ~VM_SHARED);

    merged_vma = NULL;
    if (file)
        merged_vma = vma_merge(mm, prev, addr, vma->vm_end,
            vma->vm_flags, NULL, file, pgoff, vma_policy(vma));
    if (merged_vma) {
        mpol_put(vma_policy(vma));
        kmem_cache_free(vm_area_cachep, vma);
        fput(file);
        if (vm_flags & VM_EXECUTABLE)
            removed_exe_file_vma(mm);
        vma = merged_vma;
    } else {
        vma_link(mm, vma, prev, rb_link, rb_parent);
        file = vma->vm_file;
    }
此时,把新建的虚拟区插入到进程的地址空间,这是由函数vma_link()完成的,该函数具有三方面的功能:
(1)把vma 插入到虚拟区链表中
(2)把vma插入到虚拟区形成的红黑树中
(3)把vam插入到索引节点(inode)共享链表中
函数atomic_inc(x)给*x加1,这是一个原子操作。在内核代码中,有很多地方调用了以atomic为前缀的函数。原子操作,在操作过程中不会被中断。

    if (correct_wcount)
        atomic_inc(&inode->i_writecount);
out:
    mm->total_vm += len >> PAGE_SHIFT;
    vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
    if (vm_flags & VM_LOCKED) {
        long nr_pages = mlock_vma_pages_range(vma, addr, addr + len);
        if (nr_pages < 0)
            return nr_pages;    /* vma gone! */
        mm->locked_vm += (len >> PAGE_SHIFT) - nr_pages;
    } else if ((flags & MAP_POPULATE) && !(flags & MAP_NONBLOCK))
        make_pages_present(addr, addr + len);
    return addr;

unmap_and_free_vma:
    if (correct_wcount)
        atomic_inc(&inode->i_writecount);
    vma->vm_file = NULL;
    fput(file);

    unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
    charged = 0;
free_vma:
    kmem_cache_free(vm_area_cachep, vma);
unacct_error:
    if (charged)
        vm_unacct_memory(charged);
    return error;
}

ok!到此mmap的内核核心就可以了,关于具体的mmap的实现,以后再看。


0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:154215次
    • 积分:2076
    • 等级:
    • 排名:千里之外
    • 原创:12篇
    • 转载:214篇
    • 译文:0篇
    • 评论:22条
    最新评论