zoukankan      html  css  js  c++  java
  • 趣谈Linux操作系统学习笔记-内存管理(25讲)--内存映射上

    mmap 的原理

    每一个进程都有一个列表 vm_area_struct

     1 struct mm_struct {
     2   struct vm_area_struct *mmap;    /* list of VMAs */
     3 ......
     4 }
     5 
     6 
     7 struct vm_area_struct {
     8   /*
     9    * For areas with an address space and backing store,
    10    * linkage into the address_space->i_mmap interval tree.
    11    */
    12   struct {
    13     struct rb_node rb;
    14     unsigned long rb_subtree_last;
    15   } shared;
    16 
    17  /*
    18    * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
    19    * list, after a COW of one of the file pages.  A MAP_SHARED vma
    20    * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
    21    * or brk vma (with NULL file) can only be in an anon_vma list.
    22    */
    23   struct list_head anon_vma_chain; /* Serialized by mmap_sem &
    24             * page_table_lock */
    25   struct anon_vma *anon_vma;  /* Serialized by page_table_lock */
    26 
    27 
    28 
    29 
    30   /* Function pointers to deal with this struct. */
    31   const struct vm_operations_struct *vm_ops;
    32   /* Information about our backing store: */
    33   unsigned long vm_pgoff;    /* Offset (within vm_file) in PAGE_SIZE
    34              units */
    35   struct file * vm_file;    /* File we map to (can be NULL). */
    36   void * vm_private_data;    /* was vm_pte (shared mem) */

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

    这个时候,访问内存空间就能够访问到文件里面的数据。

    而仅有物理内存和虚拟内存的映射,是一种特殊情况

    申请小块内存 : brk

    brk是将数据段(.data)的最高地址指针_edata往高地址推;

    使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系), 如下图:

    1、进程启动的时候,其(虚拟)内存空间的初始布局如图1所示。
          其中,mmap内存映射文件是在堆和栈的中间(例如libc-2.2.93.so,其它数据文件等),为了简单起见,省略了内存映射文件。
          _edata指针(glibc里面定义)指向数据段的最高地址。 
    2、
    进程调用A=malloc(30K)以后,内存空间如图2:
          malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配。
          你可能会问:只要把_edata+30K就完成内存分配了?
          事实是这样的,_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。 
    3、
    进程调用B=malloc(40K)以后,内存空间如图3。

    申请一大块内存: mmap

    对于堆的申请来讲,mmap 是映射内存空间到物理内存

    mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。另外,如果一个进程想映射一个文件到自己的虚拟内存空间,也要通过mmap系统调用这个时候mmap是映射内存空间到物理内存再到文件。可见mmap这个系统调用时核心

     1 SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
     2                 unsigned long, prot, unsigned long, flags,
     3                 unsigned long, fd, unsigned long, off)
     4 {
     5 ......
     6         error = sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
     7 ......
     8 }
     9 
    10 
    11 SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
    12     unsigned long, prot, unsigned long, flags,
    13     unsigned long, fd, unsigned long, pgoff)
    14 {
    15   struct file *file = NULL;
    16 ......
    17   file = fget(fd);
    18 ......
    19   retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
    20   return retval;
    21 }

    如果映射到文件,fd会传进来一个文件描述符,并且mmap_pgoff里面通过fget函数,根据文件描述符获得struct file、struct file表示打开一个文件

    接下来的调用链是: vm_mmap_pgoff->do_mmap_pgoff->do_mmap

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

    2 ) 调用 mmap_region 映射这个区域。

     (图引用:https://www.cnblogs.com/luoahong/p/10916458.html)

     1 const struct file_operations ext4_file_operations = {
     2 ......
     3         .mmap           = ext4_file_mmap
     4         .get_unmapped_area = thp_get_unmapped_area,
     5 };
     6  
     7  
     8 unsigned long __thp_get_unmapped_area(struct file *filp, unsigned long len,
     9                 loff_t off, unsigned long flags, unsigned long size)
    10 {
    11         unsigned long addr;
    12         loff_t off_end = off + len;
    13         loff_t off_align = round_up(off, size);
    14         unsigned long len_pad;
    15         len_pad = len + size;
    16 ......
    17         addr = current->mm->get_unmapped_area(filp, 0, len_pad,
    18                                               off >> PAGE_SHIFT, flags);
    19         addr += (off - addr) & (size - 1);
    20         return addr;
    21 }

    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     struct list_head *uf)
     4 {
     5   struct mm_struct *mm = current->mm;
     6   struct vm_area_struct *vma, *prev;
     7   struct rb_node **rb_link, *rb_parent;
     8 
     9 
    10   /*
    11    * Can we just expand an old mapping?
    12    */
    13   vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
    14       NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);
    15   if (vma)
    16     goto out;
    17 
    18 
    19   /*
    20    * Determine the object being mapped and call the appropriate
    21    * specific mapper. the address has already been validated, but
    22    * not unmapped, but the maps are removed from the list.
    23    */
    24   vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
    25   if (!vma) {
    26     error = -ENOMEM;
    27     goto unacct_error;
    28   }
    29 
    30 
    31   vma->vm_mm = mm;
    32   vma->vm_start = addr;
    33   vma->vm_end = addr + len;
    34   vma->vm_flags = vm_flags;
    35   vma->vm_page_prot = vm_get_page_prot(vm_flags);
    36   vma->vm_pgoff = pgoff;
    37   INIT_LIST_HEAD(&vma->anon_vma_chain);
    38 
    39 
    40   if (file) {
    41     vma->vm_file = get_file(file);
    42     error = call_mmap(file, vma);
    43     addr = vma->vm_start;
    44     vm_flags = vma->vm_flags;
    45   } 
    46 ......
    47   vma_link(mm, vma, prev, rb_link, rb_parent);
    48   return addr;
    49 .....

    1、还记得咱们刚找到了虚拟内存区域的前一个 vm_area_struct,我们首先要看,是否能够基于它进行扩展,也即调用 vma_merge,和前一个 vm_area_struct 合并到一起。

    2、如果不能,就需要调用 kmem_cache_zalloc,在 Slub 里面创建一个新的 vm_area_struct对象,设置起始和结束位置,将它加入队列。如果是映射到文件,则设置 vm_file 为目标文件,

      调用 call_mmap。其实就是调用 file_operations 的 mmap 函数

    3、对于 ext4 文件系统,调用的是 ext4_file_mmap。从这个函数的参数可以看出,这一刻文件和内存开始发生关系了。这里我们将vm_area_struct 的内存操作设置为文件系统操作,也就是说,读写内存其实就是读写文件系统

    最终,vma_link 函数将新创建的 vm_area_struct 挂在了 mm_struct 里面的红黑树上。

    这个时候,从内存到文件的映射关系,至少要在逻辑层面建立起来。那从文件到内存的映射关系呢?vma_link 还做了另外一件事情,就是 __vma_link_file。这个东西要用于建立这层映射关系。

    对于打开的文件,会有一个结构 struct file 来表示。它有个成员指向 struct address_space 结构,这里面有棵变量名为 i_mmap 的红黑树,vm_area_struct 就挂在这棵树上。

     1 struct address_space {
     2   struct inode    *host;    /* owner: inode, block_device */
     3 ......
     4   struct rb_root    i_mmap;    /* tree of private and shared mappings */
     5 ......
     6   const struct address_space_operations *a_ops;  /* methods */
     7 ......
     8 }
     9 
    10 
    11 static void __vma_link_file(struct vm_area_struct *vma)
    12 {
    13   struct file *file;
    14 
    15 
    16   file = vma->vm_file;
    17   if (file) {
    18     struct address_space *mapping = file->f_mapping;
    19     vma_interval_tree_insert(vma, &mapping->i_mmap);
    20   }

    注意:

    目前为止,我们还没有开始真正访问内存!这个时候,内存管理并不直接分配物理内存,因为物理内存相对于虚拟地址空间太宝贵了,只有等你真正用的那一刻才会开始分配。

    这两种方式分配的都是虚拟内存,没有分配物理内存在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

    缺页中断后,执行了那些操作?

    当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作: 
    1、检查要访问的虚拟地址是否合法 
    2、查找/分配一个物理页 
    3、填充物理页内容(读取磁盘,或者直接置0,或者啥也不干) 
    4、
    建立映射关系(虚拟地址到物理地址) 
    重新执行发生缺页中断的那条指令 
    如果第3步,需要读取磁盘,那么这次缺页中断就是majflt,否则就是minflt。

    用户态缺页异常

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

     1 dotraplinkage void notrace
     2 do_page_fault(struct pt_regs *regs, unsigned long error_code)
     3 {
     4   unsigned long address = read_cr2(); /* Get the faulting address */
     5 ......
     6   __do_page_fault(regs, error_code, address);
     7 ......
     8 }
     9 
    10 
    11 /*
    12  * This routine handles page faults.  It determines the address,
    13  * and the problem, and then passes it off to one of the appropriate
    14  * routines.
    15  */
    16 static noinline void
    17 __do_page_fault(struct pt_regs *regs, unsigned long error_code,
    18     unsigned long address)
    19 {
    20   struct vm_area_struct *vma;
    21   struct task_struct *tsk;
    22   struct mm_struct *mm;
    23   tsk = current;
    24   mm = tsk->mm;
    25 
    26 
    27   if (unlikely(fault_in_kernel_space(address))) {
    28     if (vmalloc_fault(address) >= 0)
    29       return;
    30   }
    31 ......
    32   vma = find_vma(mm, address);
    33 ......
    34   fault = handle_mm_fault(vma, address, flags);
    35 ......

    1、在do_page_fault里面,先要判断缺页中断是否发生在内核,如果发生在内核则调用vmalloc_fault,这就是和咱们前面学过的虚拟内存的布局对应上了

    2、在内核里面,vmalloc区域需要内核页表映射到物理页,咱们这里把内核的这部分放放,接着看用户空间的部分

    3、接下来在用户空间里面,找到你访问的那个地址所在的区域 vm_area_struct,然后调用 handle_mm_fault 来映射这个区域。

     1 static int __handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
     2     unsigned int flags)
     3 {
     4   struct vm_fault vmf = {
     5     .vma = vma,
     6     .address = address & PAGE_MASK,
     7     .flags = flags,
     8     .pgoff = linear_page_index(vma, address),
     9     .gfp_mask = __get_fault_gfp_mask(vma),
    10   };
    11   struct mm_struct *mm = vma->vm_mm;
    12   pgd_t *pgd;
    13   p4d_t *p4d;
    14   int ret;
    15 
    16 
    17   pgd = pgd_offset(mm, address);
    18   p4d = p4d_alloc(mm, pgd, address);
    19 ......
    20   vmf.pud = pud_alloc(mm, p4d, address);
    21 ......
    22   vmf.pmd = pmd_alloc(mm, vmf.pud, address);
    23 ......
    24   return handle_pte_fault(&vmf);
    25 }

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

    1、pgd_t 用于全局页目录项,pud_t 用于上层页目录项,pmd_t 用于中间页目录项,pte_t 用于直接页表项。

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

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

     1 /*
     2  * Allocate a new mm structure and copy contents from the
     3  * mm structure of the passed in task structure.
     4  */
     5 static struct mm_struct *dup_mm(struct task_struct *tsk)
     6 {
     7   struct mm_struct *mm, *oldmm = current->mm;
     8   mm = allocate_mm();
     9   memcpy(mm, oldmm, sizeof(*mm));
    10   if (!mm_init(mm, tsk, mm->user_ns))
    11     goto fail_nomem;
    12   err = dup_mmap(mm, oldmm);
    13   return mm;
    14 }

    在这里,除了创建一个新的 mm_struct,并且通过 memcpy 将它和父进程的弄成一模一样之外,我们还需要调用 mm_init 进行初始化。接下来,

    mm_init 调用 mm_alloc_pgd,分配全局、页目录项,赋值给 mm_struct 的 pdg 成员变量。

    1 static inline int mm_alloc_pgd(struct mm_struct *mm)
    2 {
    3   mm->pgd = pgd_alloc(mm);
    4   return 0;
    5 }

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

     1 static void pgd_ctor(struct mm_struct *mm, pgd_t *pgd)
     2 {
     3   /* If the pgd points to a shared pagetable level (either the
     4      ptes in non-PAE, or shared PMD in PAE), then just copy the
     5      references from swapper_pg_dir. */
     6   if (CONFIG_PGTABLE_LEVELS == 2 ||
     7       (CONFIG_PGTABLE_LEVELS == 3 && SHARED_KERNEL_PMD) ||
     8       CONFIG_PGTABLE_LEVELS >= 4) {
     9     clone_pgd_range(pgd + KERNEL_PGD_BOUNDARY,
    10         swapper_pg_dir + KERNEL_PGD_BOUNDARY,
    11         KERNEL_PGD_PTRS);
    12   }
    13 ......
    14 }

    待续...

     

    mmap 的原理
  • 相关阅读:
    jsp.图书馆
    jsp第七次作业
    jsp第六次作业
    jsp第四次作业
    JSP第二次作业
    软件测试课堂练习
    Android第六次作业
    Android第五次作业
    Android第四次作业
    Android第三次作业
  • 原文地址:https://www.cnblogs.com/mysky007/p/12316485.html
Copyright © 2011-2022 走看看