趣谈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 的原理
posted @ 2020-02-16 13:09  坚持,每天进步一点点  阅读(471)  评论(0编辑  收藏  举报