zoukankan      html  css  js  c++  java
  • 用户态内存映射

    mmap 的原理

    每一个进程都有一个列表 vm_area_struct,指向虚拟地址空间的不同的内存块,这个变量的名字叫 mmap

    其实内存映射不仅仅是物理内存和虚拟内存之间的映射,还包括将文件中的内容映射到虚拟内存空间。

    这个时候,访问内存空间就能够访问到文件里面的数据。而仅有物理内存和虚拟内存的映射,是一种特殊情况。

    struct mm_struct {
      struct vm_area_struct *mmap;    /* list of VMAs */
    ......
    }
    
    struct vm_area_struct {
      /*
       * For areas with an address space and backing store,
       * linkage into the address_space->i_mmap interval tree.
       */
      struct {
        struct rb_node rb;
        unsigned long rb_subtree_last;
      } shared;
    
      /*
       * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
       * list, after a COW of one of the file pages.  A MAP_SHARED vma
       * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
       * or brk vma (with NULL file) can only be in an anon_vma list.
       */
      struct list_head anon_vma_chain; /* Serialized by mmap_sem &
                * page_table_lock */
      struct anon_vma *anon_vma;  /* Serialized by page_table_lock */
    
      /* Function pointers to deal with this struct. */
      const struct vm_operations_struct *vm_ops;
      /* Information about our backing store: */
      unsigned long vm_pgoff;    /* Offset (within vm_file) in PAGE_SIZE units */
      struct file * vm_file;    /* File we map to (can be NULL). */
      void * vm_private_data;    /* was vm_pte (shared mem) */
    }
    
    SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
                    unsigned long, prot, unsigned long, flags,
                    unsigned long, fd, unsigned long, off)
    {
            ......
            error = sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
            ......
    }
    
    
    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;
      ......
      file = fget(fd);
      ......
      retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
      return retval;
    }
    
    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) {
        if (file->f_op->get_unmapped_area)
          get_area = file->f_op->get_unmapped_area;
      } 
      ......
    }

    如果要映射到文件,fd 会传进来一个文件描述符,并且 mmap_pgoff 里面通过 fget 函数,根据文件描述符获得 struct file。struct file 表示打开的一个文件。接下来的调用链是

    vm_mmap_pgoff->do_mmap_pgoff->do_mmap

    这里面主要干了两件事情:

    调用 get_unmapped_area 找到一个没有映射的区域;

    调用 mmap_region 映射这个区域。

    如果是匿名映射,则调用 mm_struct 里面的 get_unmapped_area 函数。这个函数其实是 arch_get_unmapped_area。它会调用 find_vma_prev,在表示虚拟内存区域的 vm_area_struct 红黑树上找到相应的位置。之所以叫 prev,是说这个时候虚拟内存区域还没有建立,找到前一个 vm_area_struct。

    如果不是匿名映射,而是映射到一个文件,这样在 Linux 里面,每个打开的文件都有一个 struct file 结构,里面有一个 file_operations,用来表示和这个文件相关的操作。如果是我们熟知的 ext4 文件系统,调用的是 thp_get_unmapped_area。如果我们仔细看这个函数,最终还是调用 mm_struct 里面的 get_unmapped_area 函数,殊途同归。

    unsigned long mmap_region(struct file *file, unsigned long addr,
        unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
        struct list_head *uf)
    {
      struct mm_struct *mm = current->mm;
      struct vm_area_struct *vma, *prev;
      struct rb_node **rb_link, *rb_parent;
    
    
      /*
       * Can we just expand an old mapping?
       */
      vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
          NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);
      if (vma)
        goto out;
    
    
      /*
       * 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_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;
      INIT_LIST_HEAD(&vma->anon_vma_chain);
    
    
      if (file) {
        vma->vm_file = get_file(file);
        error = call_mmap(file, vma);
        addr = vma->vm_start;
        vm_flags = vma->vm_flags;
      } 
      ......
      // 将新创建的 vm_area_struct 挂在了 mm_struct 里面的红黑树上
      vma_link(mm, vma, prev, rb_link, rb_parent);
      return addr;
      .....
    }
    
    struct address_space {
      struct inode    *host;    /* owner: inode, block_device */
      ......
      struct rb_root    i_mmap;    /* tree of private and shared mappings */
      ......
      const struct address_space_operations *a_ops;  /* methods */
      ......
    }
    
    static void __vma_link_file(struct vm_area_struct *vma)
    {
      struct file *file;
      file = vma->vm_file;
      if (file) {
        struct address_space *mapping = file->f_mapping;
        vma_interval_tree_insert(vma, &mapping->i_mmap);
      }

    之前找到了虚拟内存区域的前一个 vm_area_struct,首先要看,是否能够基于它进行扩展,也即调用 vma_merge,和前一个 vm_area_struct 合并到一起。如果不能,就需要调用 kmem_cache_zalloc,在 Slub 里面创建一个新的 vm_area_struct 对象,设置起始和结束位置,将它加入队列。

    如果是映射到文件,则设置 vm_file 为目标文件,调用 call_mmap。其实就是调用 file_operations 的 mmap 函数。对于 ext4 文件系统,调用的是 ext4_file_mmap。从这个函数的参数可以看出,这一刻文件和内存开始发生关系了。这里我们将 vm_area_struct 的内存操作设置为文件系统操作,也就是说,读写内存其实就是读写文件系统。

    这个时候,从内存到文件的映射关系,至少要在逻辑层面建立起来。那从文件到内存的映射关系呢?vma_link 还做了另外一件事情,就是 __vma_link_file。这个东西要用于建立这层映射关系。对于打开的文件,会有一个结构 struct file 来表示。它有个成员指向 struct address_space 结构,这里面有棵变量名为 i_mmap 的红黑树,vm_area_struct 就挂在这棵树上。

    用户态缺页异常

    一旦开始访问虚拟内存的某个地址,发现并没有对应的物理页,那就触发缺页中断,调用 do_page_fault。

    dotraplinkage void notrace
    do_page_fault(struct pt_regs *regs, unsigned long error_code)
    {
      unsigned long address = read_cr2(); /* Get the faulting address */
      ......
      __do_page_fault(regs, error_code, address);
      ......
    }
    
    
    /*
     * This routine handles page faults.  It determines the address,
     * and the problem, and then passes it off to one of the appropriate
     * routines.
     */
    static noinline void
    __do_page_fault(struct pt_regs *regs, unsigned long error_code,
        unsigned long address)
    {
      struct vm_area_struct *vma;
      struct task_struct *tsk;
      struct mm_struct *mm;
      tsk = current;
      mm = tsk->mm;
    
    
      if (unlikely(fault_in_kernel_space(address))) {
        if (vmalloc_fault(address) >= 0)
          return;
      }
      ......
      vma = find_vma(mm, address);
      ......
      fault = handle_mm_fault(vma, address, flags);
      ......
    }

    在 __do_page_fault 里面,先要判断缺页中断是否发生在内核。

    如果发生在内核则调用 vmalloc_fault。在内核里面,vmalloc 区域需要内核页表映射到物理页。

    如果发生在用户空间,找到要访问的那个地址所在的区域 vm_area_struct,然后调用 handle_mm_fault 来映射这个区域。

    static int __handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
        unsigned int flags)
    {
      struct vm_fault vmf = {
        .vma = vma,
        .address = address & PAGE_MASK,
        .flags = flags,
        .pgoff = linear_page_index(vma, address),
        .gfp_mask = __get_fault_gfp_mask(vma),
      };
      struct mm_struct *mm = vma->vm_mm;
      pgd_t *pgd;
      p4d_t *p4d;
      int ret;
    
    
      pgd = pgd_offset(mm, address);
      p4d = p4d_alloc(mm, pgd, address);
      ......
      vmf.pud = pud_alloc(mm, p4d, address);
      ......
      vmf.pmd = pmd_alloc(mm, vmf.pud, address);
      ......
      return handle_pte_fault(&vmf);
    }

    到这里,终于看到了我们熟悉的 PGD、P4G、PUD、PMD、PTE,这就是前面讲页表的时候,讲述的四级页表的概念,因为暂且不考虑五级页表,我们暂时忽略 P4G。

    每个进程都有独立的地址空间,为了这个进程独立完成映射,每个进程都有独立的进程页表,这个页表的最顶级的 pgd 存放在 task_struct 中的 mm_struct 的 pgd 变量里面。

    在一个进程新创建的时候,会调用 fork,对于内存的部分会调用 copy_mm,里面调用 dup_mm。

    /*
     * Allocate a new mm structure and copy contents from the
     * mm structure of the passed in task structure.
     */
    static struct mm_struct *dup_mm(struct task_struct *tsk)
    {
      struct mm_struct *mm, *oldmm = current->mm;
      mm = allocate_mm();
      memcpy(mm, oldmm, sizeof(*mm));
      if (!mm_init(mm, tsk, mm->user_ns))
        goto fail_nomem;
      err = dup_mmap(mm, oldmm);
      return mm;
    }
    
    // mm_init => mm_alloc_pgd
    static inline int mm_alloc_pgd(struct mm_struct *mm)
    {
      mm->pgd = pgd_alloc(mm); // 分配全局页目录项
      return 0;
    }
    
    // pgd_alloc => pgd_ctor
    static void pgd_ctor(struct mm_struct *mm, pgd_t *pgd)
    {
      /* If the pgd points to a shared pagetable level (either the
         ptes in non-PAE, or shared PMD in PAE), then just copy the
         references from swapper_pg_dir. 内核页表的最顶级的全局页目录*/
      if (CONFIG_PGTABLE_LEVELS == 2 ||
          (CONFIG_PGTABLE_LEVELS == 3 && SHARED_KERNEL_PMD) ||
          CONFIG_PGTABLE_LEVELS >= 4) {
        clone_pgd_range(pgd + KERNEL_PGD_BOUNDARY,
            swapper_pg_dir + KERNEL_PGD_BOUNDARY,
            KERNEL_PGD_PTRS);
      }
      ......
    }

    pgd_alloc 里面除了分配 PGD 之外,还做了很重要的一个事情,就是调用 pgd_ctor。

    pgd_ctor 拷贝了对于 swapper_pg_dir 的引用。swapper_pg_dir 是内核页表的最顶级的全局页目录。

    至此,一个进程 fork 完毕之后,有了内核页表,有了自己顶级的 pgd,但是对于用户地址空间来讲,还完全没有映射过。这需要等到这个进程在某个 CPU 上运行,并且对内存访问的那一刻了。

    当这个进程被调度到某个 CPU 上运行的时候,要调用 context_switch 进行上下文切换。对于内存方面的切换会调用 switch_mm_irqs_off,这里面会调用 load_new_mm_cr3。

    cr3 是 CPU 的一个寄存器,它会指向当前进程的顶级 pgd。如果 CPU 的指令要访问进程的虚拟内存,它就会自动从 cr3 里面得到 pgd 在物理内存的地址,然后根据里面的页表解析虚拟内存的地址为物理内存,从而访问真正的物理内存上的数据。

    这里需要注意两点。

    第一点,cr3 里面存放当前进程的顶级 pgd,这个是硬件的要求。cr3 里面需要存放 pgd 在物理内存的地址,不能是虚拟地址。因而 load_new_mm_cr3 里面会使用 __pa,将 mm_struct 里面的成员变量 pgd(mm_struct 里面存的都是虚拟地址)变为物理地址,才能加载到 cr3 里面去。

    第二点,用户进程在运行的过程中,访问虚拟内存中的数据,会被 cr3 里面指向的页表转换为物理地址后,才在物理内存中访问数据,这个过程都是在用户态运行的,地址转换的过程无需进入内核态。

    只有访问虚拟内存的时候,发现没有映射到物理内存,页表也没有创建过,才触发缺页异常。进入内核调用 do_page_fault,然后__handle_mm_fault 调用 pud_alloc 和 pmd_alloc,来创建相应的页目录项,最后调用 handle_pte_fault 来创建页表项。

    static int handle_pte_fault(struct vm_fault *vmf)
    {
      pte_t entry;
      ......
      vmf->pte = pte_offset_map(vmf->pmd, vmf->address);
      vmf->orig_pte = *vmf->pte;
      ......
      // 如果页表项 PTE,从来没有出现过,那就是新映射的页
      if (!vmf->pte) {
      // 如果是匿名页,应该映射到一个物理内存页
        if (vma_is_anonymous(vmf->vma))
          return do_anonymous_page(vmf);
        else // 如果是映射到文件
          return do_fault(vmf);
      }
    
      // 如果 PTE 原来出现过,说明原来页面在物理内存中,后来换出到硬盘了,现在应该换回来
      if (!pte_present(vmf->orig_pte))
        return do_swap_page(vmf);
      ......
    }
    static int do_anonymous_page(struct vm_fault *vmf)
    {
      struct vm_area_struct *vma = vmf->vma;
      struct mem_cgroup *memcg;
      struct page *page;
      int ret = 0;
      pte_t entry;
      ......
      // 分配一个页表项
      if (pte_alloc(vma->vm_mm, vmf->pmd, vmf->address))
        return VM_FAULT_OOM;
      ......
      // 分配一个页
      page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
      ......
      // 将页表项指向新分配的物理页
      entry = mk_pte(page, vma->vm_page_prot);
      if (vma->vm_flags & VM_WRITE)
        entry = pte_mkwrite(pte_mkdirty(entry));
    
    
      vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
          &vmf->ptl);
      ......
      // 将页表项塞到页表里面
      set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
      ......
    }
    static int __do_fault(struct vm_fault *vmf)
    {
      struct vm_area_struct *vma = vmf->vma;
      int ret;
      ......
      ret = vma->vm_ops->fault(vmf);
      ......
      return ret;
    }
    
    
    static const struct vm_operations_struct ext4_file_vm_ops = {
      .fault    = ext4_filemap_fault,
      .map_pages  = filemap_map_pages,
      .page_mkwrite   = ext4_page_mkwrite,
    };
    
    
    int ext4_filemap_fault(struct vm_fault *vmf)
    {
      // vm_file 就是当时 mmap 的时候映射的那个文件
      struct inode *inode = file_inode(vmf->vma->vm_file);
      ......
      err = filemap_fault(vmf);
      ......
      return err;
    }
    int filemap_fault(struct vm_fault *vmf)
    {
      int error;
      struct file *file = vmf->vma->vm_file;
      struct address_space *mapping = file->f_mapping;
      struct inode *inode = mapping->host;
      pgoff_t offset = vmf->pgoff;
      struct page *page;
      int ret = 0;
      ......
      //对于文件映射来说,一般这个文件会在物理内存里面有页面作为它的缓存,find_get_page 就是找那个页
      page = find_get_page(mapping, offset);
      if (likely(page) && !(vmf->flags & FAULT_FLAG_TRIED)) {
        // 如果找到了,就预读一些数据到内存里面
        do_async_mmap_readahead(vmf->vma, ra, file, page, offset);
      } else if (!page) {
        goto no_cached_page;
      }
      ......
      vmf->page = page;
      return ret | VM_FAULT_LOCKED;
    no_cached_page:
      // 如果没有物理内存中的缓存页
      error = page_cache_read(file, offset, vmf->gfp_mask);
      ......
    }
    
    
    static int page_cache_read(struct file *file, pgoff_t offset, gfp_t gfp_mask)
    {
      struct address_space *mapping = file->f_mapping;
      struct page *page;
      ......
      // 显示分配一个缓存页
      page = __page_cache_alloc(gfp_mask|__GFP_COLD);
      ......
      // 将这一页加到 lru 表里面
      ret = add_to_page_cache_lru(page, mapping, offset, gfp_mask & GFP_KERNEL);
      ......
      // 将文件内容读到内存中
      ret = mapping->a_ops->readpage(file, page);
      ......
    }
    static const struct address_space_operations ext4_aops = {
      .readpage    = ext4_readpage,
      .readpages    = ext4_readpages,
     ......
    };
    
    
    static int ext4_read_inline_page(struct inode *inode, struct page *page)
    {
      void *kaddr;
      ......
      // 临时内核映射,将物理内存映射到内核的虚拟地址空间,得到内核中的地址 kaddr
      kaddr = kmap_atomic(page);
      // 读取文件到这个虚拟地址
      ret = ext4_read_inline_data(inode, kaddr, len, &iloc);
      flush_dcache_page(page);
      // 读取完毕后,取消这个临时映射
      kunmap_atomic(kaddr);
      ......
    }

    kmap_atomic是用来做临时内核映射的。本来把物理内存映射到用户虚拟地址空间,不需要在内核里面映射一把。但是,现在因为要从文件里面读取数据并写入这个物理页面,又不能使用物理地址,我们只能使用虚拟地址,这就需要在内核里面临时映射一把。

    int do_swap_page(struct vm_fault *vmf)
    {
      struct vm_area_struct *vma = vmf->vma;
      struct page *page, *swapcache;
      struct mem_cgroup *memcg;
      swp_entry_t entry;
      pte_t pte;
      ......
      entry = pte_to_swp_entry(vmf->orig_pte);
      ......
      // 先查找 swap 文件有没有缓存页
      page = lookup_swap_cache(entry);
      if (!page) {
        // 将 swap 文件读到内存中来,形成内存页
        page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE, vma,
              vmf->address);
        ......
      } 
      ......
      swapcache = page;
      ......
      // 生成页表项
      pte = mk_pte(page, vma->vm_page_prot);
      ......
      // 将页表项插入页表
      set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);
      vmf->orig_pte = pte;
      ......
      // 将 swap 文件清理(因为重新加载回内存了,不再需要 swap 文件)
      swap_free(entry);
      ......
    }
    
    // swapin_readahead 会最终调用 swap_readpage
    int swap_readpage(struct page *page, bool do_poll)
    {
      struct bio *bio;
      int ret = 0;
      struct swap_info_struct *sis = page_swap_info(page);
      blk_qc_t qc;
      struct block_device *bdev;
      ......
      if (sis->flags & SWP_FILE) {
        struct file *swap_file = sis->swap_file;
    struct address_space *mapping = swap_file->f_mapping;
    // 读取普通文件和读取 swap 文件,过程是一样的,同样需要用 kmap_atomic 做临时映射
        ret = mapping->a_ops->readpage(swap_file, page);
        return ret;
      }
      ......
    }

    通过上面复杂的过程,用户态缺页异常处理完毕了。物理内存中有了页面,页表也建立好了映射。接下来,用户程序在虚拟内存空间里面,可以通过虚拟地址顺利经过页表映射的访问物理页面上的数据了。

  • 相关阅读:
    Java基础(二十三)——IO流
    Java基础(二十二) 递归
    Java基础(二十二) Lambda表达式和File类
    Java基础(二十一)——多线程和Lambda表达式
    Java基础(二十)——多线程
    Java基础(十八)——Comparator比较器、Comparable接口和Map集合
    根据 key值查找数组对象中所有的符合的对象 (递归)
    超级好用的 支付宝小程序 网络请求封装 async/await
    关于Vue Loading chunk {n} failed的问题
    支付宝小程序iconfont兼容性问题
  • 原文地址:https://www.cnblogs.com/sunnycindy/p/14916900.html
Copyright © 2011-2022 走看看