linux mmap函数详解

linux mmap 详解谨以此文纪念过往的岁月一.前言mmap的具体实现以前在学习内核时学习过,但是对于其中的很多函数是一知半解的,有些只能根据其函数名来猜测其具体的功能,在本文中,一起来重新深入理解其具体的实现。二.mmap的用户层应用void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);具体参数含义start : 指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。length: 代表将文件中多大的部分映射到内存。prot : 映射区域的保护方式。可以为以下几种方式的组合: PROT_EXEC 映射区域可被执行 PROT_READ 映射区域可被读取 PROT_WRITE 映射区域可被写入 PROT_NONE 映射区域不能存取flags : 影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。 MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。 MAP_SHARED 对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。 MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。 MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。 MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。 MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。fd : 要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件, 然后对该文件进行映射,可以同样达到匿名内存映射的效果。offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是PAGE_SIZE的整数倍。返回值: 若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。错误代码: EBADF 参数fd 不是有效的文件描述词 EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能写入。 EINVAL 参数start、length 或offset有一个不合法。 EAGAIN 文件被锁住,或是有太多内存被锁住。 ENOMEM 内存不足。用户层的调用很简单,其具体功能就是直接将物理内存映射到用户虚拟内存,使用户空间可以直接对物理空间操作。但是对于内核层而言,其具体实现比较复杂。三.mmap的内核实现对于mmap的内核有了解的都会知道用户层的mmap到内核层的mmap其中多了一个参数vma_struct这个结构体,在开始时对于这个参数很疑惑就是这个参数的值是哪儿来的,在这里我们会一一来讲述。还是从do_mmap开始吧。3.1 do_mmap参数说明:file :就是用户层想要映射的fileaddr :欲映射的起始地址,即用户层的startprot :用户层传入的portflag :同上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 areaunsigned 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_regionunsigned 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的实现,以后再看。四.总结mmap的实质是什么,其实就是从每一个进程中的用户空间分配一段空间用于映射。这里面的机关重重,需要好好理解,不过谨记一点,进程的vma_struct是采用了红黑树来管理的。对于每一段的内存区都会有一个vma_struct来描述,比如数据区,code区等等,以及mmap所需要的一段内存区。

http://blog.chinaunix.net/uid-24517893-id-164217.html

让你的心情地落到极点,一直学习生活等各个方面都做不好,最终害的还是自己。

linux mmap函数详解

相关文章:

你感兴趣的文章:

标签云: