mmap实现分析

本文不是介绍mmap函数的使用方法,而是分析其内核实现,相关使用方法网上已经有很多资料。Mmap的本质其实就是:为当前进程分配(或找到)一个合适的vma,然后为该vma设置对应的缺页处理函数

我们知道mmap按照flag可以分为匿名映射和非匿名映射,又可分为shared映射和private映射。这样从两个维度,我们就得到了四种映射。

(1) 匿名shared映射:fd-1,可用于父子进程通信。

(2) 匿名private映射:例如malloc大块的内存(大于128k)。

(3) 非匿名shared映射:常见的用于进程通信方式。

(4) 非匿名private映射:例如程序在启动时加载so时,就是用的这种方式,相当于“写时拷贝”。

    下面我们就看下内核中几种方式的区别。

内核中mmap主要有函数sys_mmap_pgoff函数负责实现,该函数定义在mm/mmap.c中。

点击(此处)折叠或打开

  1. SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
  2. unsigned long, prot, unsigned long, flags,
  3. unsigned long, fd, unsigned long, pgoff)
  4. {
  5.     struct file *file = NULL;
  6.     unsigned long retval = -EBADF;
  7.     if (!(flags & MAP_ANONYMOUS)) { /*匿名映射*/
  8.         audit_mmap_fd(fd, flags);
  9.         if (unlikely(flags & MAP_HUGETLB))
  10.             return -EINVAL;
  11.         file = fget(fd); /*由fd找到对应的file结构*/
  12.         if (!file)
  13.             goto out;
  14.         if (is_file_hugepages(file))
  15.             len = ALIGN(len, huge_page_size(hstate_file(file)));
  16.     } else if (flags & MAP_HUGETLB) {
  17.         /*......*/
  18.     }
  19.     flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE);
  20.     retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
  21.     if (file)
  22.         fput(file);
  23.     out:
  24.         return retval;
  25. }

该函数主要功能由vm_mmap_pgoff来实现,而vm_mmap_pgoff主要逻辑就是调用了do_mmap_pgoff。下面我们看vm_mmap_pgoff的实现。

do_mmap_pgoff

点击(此处)折叠或打开

  1. unsigned long do_mmap_pgoff(struct file *file, unsigned long addr,
  2. unsigned long len, unsigned long prot,
  3. unsigned long flags, unsigned long pgoff,
  4. unsigned long *populate)
  5. {
  6.     struct mm_struct * mm = current->mm;
  7.     struct inode *inode;
  8.     /*......*/
  9.     /* Obtain the address to map to. we verify (or select) it and ensure
  10.      * that it represents a valid section of the address space.
  11.      */
  12.     addr = get_unmapped_area(file, addr, len, pgoff, flags);
  13.     if (addr & ~PAGE_MASK)
  14.         return addr;
  15.     /*......*/
  16.     addr = mmap_region(file, addr, len, vm_flags, pgoff);
  17.     /*......*/
  18.     return addr;
  19. }

这个函数首先通过 get_unmapped_area创建(或获取)一个合适的vma,然后调用mmap_regionvma进行设置。我们具体看下mmap_region的实现。

mmap_region

点击(此处)折叠或打开

  1. unsigned long mmap_region(struct file *file, unsigned long addr,
  2. unsigned long len, vm_flags_t vm_flags, unsigned long pgoff)
  3. {
  4.     struct mm_struct *mm = current->mm;
  5.     struct vm_area_struct *vma, *prev;
  6.     int correct_wcount = 0;
  7.     int error;
  8.     struct rb_node **rb_link, *rb_parent;
  9.     unsigned long charged = 0;
  10.     struct inode *inode = file ? file_inode(file) : NULL;
  11.     /*......*/
  12.     if (file) { /*如果不是匿名映射*/
  13.         if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
  14.             goto free_vma;
  15.         if (vm_flags & VM_DENYWRITE) {
  16.             error = deny_write_access(file);
  17.             if (error)
  18.                 goto free_vma;
  19.             correct_wcount = 1;
  20.         }
  21.         vma->vm_file = get_file(file);
  22.         error = file->f_op->mmap(file, vma); /*调用对应文件系统的mmap函数*/
  23.         if (error)
  24.             goto unmap_and_free_vma;
  25.         addr = vma->vm_start;
  26.         pgoff = vma->vm_pgoff;
  27.         vm_flags = vma->vm_flags;
  28.     } else if (vm_flags & VM_SHARED) { /*shared 匿名映射*/
  29.         if (unlikely(vm_flags & (VM_GROWSDOWN|VM_GROWSUP)))
  30.             goto free_vma;
  31.         error = shmem_zero_setup(vma);
  32.         if (error)
  33.             goto free_vma;
  34.     } /*private 匿名映射*/
  35.     file = vma->vm_file;
  36.     /*......*/
  37. }

如果传入了fd,则调用对应文件系统的mmap函数。以ext4文件系统为例。其mmap函数为 ext4_file_mmap

ext4_file_mmap

点击(此处)折叠或打开

  1. static int ext4_file_mmap(struct file *file, struct vm_area_struct *vma)
  2. {
  3.     struct address_space *mapping = file->f_mapping;
  4.     if (!mapping->a_ops->readpage)
  5.         return -ENOEXEC;
  6.     file_accessed(file);
  7.     vma->vm_ops = &ext4_file_vm_ops;
  8.     return 0;
  9. }

可以看到这个函数只是设置vma->vm_ops为当前文件系统的处理函数。

点击(此处)折叠或打开

  1. static const struct vm_operations_struct ext4_file_vm_ops = {
  2.     .fault = filemap_fault,
  3.     .page_mkwrite = ext4_page_mkwrite,
  4.     .remap_pages = generic_file_remap_pages,
  5. };

如果是匿名映射(不传入fd),且传入了shared flag。则调用shmem_zero_setup

shmem_zero_setup

点击(此处)折叠或打开

  1. int shmem_zero_setup(struct vm_area_struct *vma)
  2. {
  3.     struct file *file;
  4.     loff_t size = vma->vm_end - vma->vm_start;
  5.     file = shmem_file_setup("dev/zero", size, vma->vm_flags);
  6.     if (IS_ERR(file))
  7.         return PTR_ERR(file);
  8.     if (vma->vm_file)
  9.         fput(vma->vm_file);
  10.     vma->vm_file = file;
  11.     vma->vm_ops = &shmem_vm_ops;
  12.     return 0;
  13. }

可以看到这里将vma->vm_ops设置为tmpfs文件系统的shmem_vm_ops

点击(此处)折叠或打开

  1. static const struct vm_operations_struct shmem_vm_ops = {
  2.     .fault = shmem_fault,
  3. #ifdef CONFIG_NUMA
  4.     .set_policy = shmem_set_policy,
  5.     .get_policy = shmem_get_policy,
  6. #endif
  7.     .remap_pages = generic_file_remap_pages,
  8. };

整个mmap函数的处理过程如下:

mmap实现分析-LMLPHP

我们知道mmap函数只是为进程分配了虚拟内存空间,并没有真的建立虚拟内存和物理内存的映射。这个建立映射的过程是到缺页中断的函数中进行的。

缺页中断的处理过程大体如下:

mmap实现分析-LMLPHP


点击(此处)折叠或打开

  1. int handle_pte_fault(struct mm_struct *mm,
  2.          struct vm_area_struct *vma, unsigned long address,
  3.          pte_t *pte, pmd_t *pmd, unsigned int flags)
  4. {
  5.     pte_t entry;
  6.     spinlock_t *ptl;
  7.    /*......*/
  8.     entry = *pte;
  9.     if (!pte_present(entry)) {
  10.         if (pte_none(entry)) {
  11.             if (vma->vm_ops)
  12.                 return do_linear_fault(mm, vma, address,
  13.                         pte, pmd, flags, entry);
  14.             /*匿名private 映射*/
  15.             return do_anonymous_page(mm, vma, address,
  16.                          pte, pmd, flags);
  17.         }
  18.     }

  19.     return 0;
  20. }


我们看到vma->vm_ops时会调用do_anonymous_page。这里需要注意,有人看到函数名就以为这是匿名映射的逻辑,但是根据前面的代码分析匿名shared的时候也是会设置vma->vm_ops的。只有一种情况不会设置,那就是匿名private映射。

所以综上,有以下结论:

(1)非匿名shared映射:调用文件各自文件系统的缺页函数;

(2)非匿名private映射:调用文件各自文件系统的缺页函数;

(3)匿名shared映射:调用tmpfs文件系统的缺页函数;

(4)匿名private映射:do_anonymous_page处理缺页,也是目前唯一支持THP(透明大页)的方式。

    另外补充:其实我们常用的posixsystemV共享内存底层都是通过tmpfs实现的,详见http://hustcat.github.io/shared-memory-tmpfs/ 。但注意其实内核是有两个tmpfs文件系统的,一个是内核启动自行挂载的用于共享匿名映射和systemV共享内存,而另一个通过mount挂载,其大小默认为系统内存的1/2,用于posix共享内存。

01-17 00:11