趣谈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往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系), 如下图:
2、进程调用A=malloc(30K)以后,内存空间如图2:
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 }
待续...