深入理解Linux虚拟内存管理(七)(上):https://developer.aliyun.com/article/1597870
(5)删除内存区域
(a)do_munmap
这个函数的调用如图 4.10 所示。这个函数负责取消一个区域的映射。如果需要,这个解除映射操作将会产生多个 VMA。而且,如果需要,它可以只取消一部分。所以,完整的解除映射操作分为两个主要操作。这个函数负责找到受影响的 VMA ,而 unmap_fixup() 负责修整其余的 VMA。
这个函数分为许多小的部分,我们分别处理。粗略而言,这些小部分如下:
前序函数,找到开始操作的 VMA 。
把受解除映射操作影响的 VMA 移除出 mm ,并把它们放到一个由变量 free 为头的链表中。
循环遍历以 free 为头的链表,取消在区域中待解除映射的页面映射。并调用 unmap_fixup() 来修整映射。
验证与解除映射操作有关的 mm 和空闲内存。
// mm/mmap.c /* Munmap is split into 2 main parts -- this part which finds * what needs doing, and the areas themselves, which do the * work. This now handles partial unmappings. * Jeremy Fitzhardine <jeremy@sw.oz.au> */ // 参数如下: // mm 是进行 unmap 操作的进程的 mm。 // addr 是 unmap 的区域的起始地址。 // len 是区域长度。 int do_munmap(struct mm_struct *mm, unsigned long addr, size_t len) { struct vm_area_struct *mpnt, *prev, **npp, *free, *extra; // 保证地址都页对齐,并且待解除映射的区域不在内核虚拟地址空间。 if ((addr & ~PAGE_MASK) || addr > TASK_SIZE || len > TASK_SIZE-addr) return -EINVAL; // 保证要解除映射的区域是页对齐的。 if ((len = PAGE_ALIGN(len)) == 0) return -EINVAL; /* Check if this memory area is ok - put it on the temporary * list if so.. The checks here are pretty simple -- * every area affected in some way (by any overlap) is put * on the list. If nothing is put on, nothing is affected. */ // 找到包含起始地址的VMA以及它前面的VMA,这样在后面可以很容易地从链表中移除VMA。 mpnt = find_vma_prev(mm, addr, &prev); // 如果没有返回mpnt,则意味着地址必须是前面最近使用过的VMA,所以,地址 // 空间还没有使用过,返回就可以了。 if (!mpnt) return 0; /* we have addr < mpnt->vm_end */ // 如果返回的VMA从你试着解除映射的区域开始,这个区域也没有使用过,则只返回。 if (mpnt->vm_start >= addr+len) return 0; /* If we'll make "hole", check the vm areas limit */ // 第1部分检查VMA是否被部分解除映射。如果是,将在后面创建另外一个 VMA 来处理被拆开的区域, // 所以必须检查map_count以保证这个计数不会过大。 if ((mpnt->vm_start < addr && mpnt->vm_end > addr+len) && mm->map_count >= max_map_count) return -ENOMEM; /* * We may need one additional vma to fix up the mappings ... * and this is the last chance for an easy error exit. */ // 一旦获得了新的映射,现在就开始分配。因为如果将来分配的话,在发生错误 // 时很难恢复过来。 extra = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL); if (!extra) return -ENOMEM; // 这部分把受解除映射操作影响的VMA从mm移除,并把它们放到一个由变量free为头 // 的链表中。这样可以使修整这片区域更简单。 // npp在循环中变成链表中的下一个VMA。初始化时,它或者是当前的VMA(mpnt) // 或者变成了链表的第一个VMA。 npp = (prev ? &prev->vm_next : &mm->mmap); // free是受到解除映射操作的VMA的链表头节点。 free = NULL; // 上锁mm。 spin_lock(&mm->page_table_lock); // 循环遍历链表,直到当前VMA是过去待解除映射区域的末端 for ( ; mpnt && mpnt->vm_start < addr+len; mpnt = *npp) { // npp变成了链表的下一个VMA。 *npp = mpnt->vm_next; // 将当前VMA从mm中的线性链表中移除,并把它放到一个由free为头的链表 // 中。当前mpnt变成空闲链表的头节点。 mpnt->vm_next = free; free = mpnt; // 从红黑树中删除mpnt。 rb_erase(&mpnt->vm_rb, &mm->mm_rb); } // 移除缓存的结果,因为以前查询的结果是待解除映射的区域。 mm->mmap_cache = NULL; /* Kill the cache. */ // 释放mm。 spin_unlock(&mm->page_table_lock); /* Ok - we have the memory areas we should free on the 'free' list, * so release them, and unmap the page range.. * If the one of the segments is only being partially unmapped, * it will put new vm_area_struct(s) into the address space. * In that case we have to be careful with VM_DENYWRITE. */ // 遍历链表直到没有VMA剩下。 while ((mpnt = free) != NULL) { unsigned long st, end, size; struct file *file = NULL; // 将free指向链表中的下一个元素,剩下mpnt作为将要被移除的元素头。 free = free->vm_next; // st是待解除映射区域的起点。如果addr在VMA的起始位置,起点就是 // mpnt->vm_struct,否则,起点就是指定的地址。 st = addr < mpnt->vm_start ? mpnt->vm_start : addr; // 以相似的方式计算要映射的区域末端。 end = addr+len; end = end > mpnt->vm_end ? mpnt->vm_end : end; // 计算在这一遍中要解除映射的区域大小。 size = end - st; // 如果指定了 VM_DENYWRITE 标志,这个解除映射操作将创建一个空洞,而 // 且将映射一个文件。然后,将 i_writecount 减 1。在这个字段为负时,它记录着保护这个文件 // 没有以写方式打开的用户数。 if (mpnt->vm_flags & VM_DENYWRITE && (st != mpnt->vm_start || end != mpnt->vm_end) && (file = mpnt->vm_file) != NULL) { atomic_dec(&file->f_dentry->d_inode->i_writecount); } // 移除文件映射。如果文件还是被部分地映射,就在unmap_fixup() (见 D.2.5.2) 时再 // 次获得。 remove_shared_vm_struct(mpnt); // 减少map计数。 mm->map_count--; // 移除该区域中的所有页面。 zap_page_range(mm, st, size); /* * Fix the mapping, and free the old area if it wasn't reused. */ // 在删除后调用unmap_fixup(见D. 2. 5. 2)来修整区域。 extra = unmap_fixup(mm, mpnt, st, size, extra); // 将文件的写计数减1,因为该区域已经被解除映射。如果它仅是被部分地解 // 除映射,这个调用将权衡在行987而减1。 if (file) atomic_inc(&file->f_dentry->d_inode->i_writecount); } // validate_mm()是一个调试函数。如果它可用,则它会保证该mm的VMA树也有效。 validate_mm(mm); /* Release the extra vma struct if it wasn't used */ // 如果不需要额外的VMA,则在这里删除它。 if (extra) kmem_cache_free(vm_area_cachep, extra); // 释放所有在解除映射区域的页表。 free_pgtables(mm, prev, addr, addr+len); // 返回成功。 return 0; }
(b)unmap_fixup
这个函数在取消一块映射后修整区域。它传入受到解除映射操作的 VMA 链表,解除映射的区域和长度以及一个空的 VMA。如果创建了整个区域,这个 VMA 可能在修整区域时用到。这个函数主要处理四种情况:解除映射的区域;从开始到中间某个地方的局部解除映射的区域;从中间的某个地方到末尾的局部解除映射的区域;在区域中间创建一个空洞。我们分别处理这四种情况。
// mm/mmap.c /* Normal function to fix up a mapping * This function is the default for when an area has no specific * function. This may be used as part of a more specific routine. * This function works out what part of an area is affected and * adjusts the mapping information. Since the actual page * manipulation is done in do_mmap(), none need be done here, * though it would probably be more appropriate. * * By the time this function is called, the area struct has been * removed from the process mapping list, so it needs to be * reinserted if necessary. * * The 4 main cases are: * Unmapping the whole area * Unmapping from the start of the segment to a point in it * Unmapping from an intermediate point to the end * Unmapping between to intermediate points, making a hole. * * Case 4 involves the creation of 2 new areas, for each side of * the hole. If possible, we reuse the existing area rather than * allocate a new one, and the return indicates whether the old * area was reused. */ // 函数的参数如下: // mm 是解除了映射区域所属的mm。 // area 是受到解除映射操作影响的VMA的链表头。 // addr 是解除映射区域的首地址。 // len 是解除映射区域的长度。 // extra 是在区域中间建立空洞时传入的空VMA。 static struct vm_area_struct * unmap_fixup(struct mm_struct *mm, struct vm_area_struct *area, unsigned long addr, size_t len, struct vm_area_struct *extra) { struct vm_area_struct *mpnt; // 计算被解除映射区域的末地址。 unsigned long end = addr + len; // 减少进程用到的页面计数。 area->vm_mm->total_vm -= len >> PAGE_SHIFT; // 如果页面在内存中被锁住,这里将减少锁住页面的计数。 if (area->vm_flags & VM_LOCKED) area->vm_mm->locked_vm -= len >> PAGE_SHIFT; // 这是第一个,也是最简单的情况,这里整个区域都被解除映射。 /* Unmapping the whole area. */ // 如果addr是VMA的首部,且end是VMA的尾部,整个区域都被解除映射。这里很 // 有趣,因为,如果解除映射的是一个生成区域,就有可能end已经超出了VMA末端, // 但是这个VMA仍然全部被解除了映射。 if (addr == area->vm_start && end == area->vm_end) { // 如果VMA提供了结束操作,则在这里调用。 if (area->vm_ops && area->vm_ops->close) area->vm_ops->close(area); // 如果映射了一个文件或设备,则在这里调用fput(),它决定使用计数,并在引用 // 计数减为0时释放映射。 if (area->vm_file) fput(area->vm_file); // 将VMA释放回slab分配器。 kmem_cache_free(vm_area_cachep, area); // 返回额外的VMA,因为没有使用过它们。 return extra; } // 这一块处理在区域中间到末尾部分被映射的情况。 /* Work out to one of the ends. */ if (end == area->vm_end) { /* * here area isn't visible to the semaphore-less readers * so we don't need to update it under the spinlock. */ // 将VMA截断为addr。在这里,区域的页面已经被释放了,后面将会释放页表表项, // 所以不再需要做额外的工作。 area->vm_end = addr; // 如果一个文件/设备已经映射,它就由函数lock_vma_mappings()来上锁保护共享访问。 lock_vma_mappings(area); // 上锁mm。在函数后面,VMA的其他部分将被插入到mm中。 spin_lock(&mm->page_table_lock); } else if (addr == area->vm_start) { // 这一块处理在区域开始到中间的文件/设备被映射的情况。 // 增加文件/设备的偏移,这些偏移由这次解除映射的页面数表示。 area->vm_pgoff += (end - area->vm_start) >> PAGE_SHIFT; /* same locking considerations of the above case */ // 将VMA的首部移到被解除映射区域的末尾。 area->vm_start = end; // 上锁前面描述的文件/设备和mm。 lock_vma_mappings(area); spin_lock(&mm->page_table_lock); } else { // 这一块处理在部分解除映射的区域中创建空洞的情况。在这种情形下,需要一个额外的 // VMA来创建一个从解除映射的区域末尾到旧VMA末尾的新映射。 /* Unmapping a hole: area->vm_start < addr <= end < area->vm_end */ /* Add end mapping -- leave beginning for below */ // 保存额外的VMA,并使VMA为NULL,这样调用函数知道VMA已经在使用 // 且不能释放这个VMA。 mpnt = extra; extra = NULL; // 复制所有的VMA信息。 mpnt->vm_mm = area->vm_mm; mpnt->vm_start = end; mpnt->vm_end = area->vm_end; mpnt->vm_page_prot = area->vm_page_prot; mpnt->vm_flags = area->vm_flags; mpnt->vm_raend = 0; mpnt->vm_ops = area->vm_ops; mpnt->vm_pgoff = area->vm_pgoff + ((end - area->vm_start) >> PAGE_SHIFT); mpnt->vm_file = area->vm_file; mpnt->vm_private_data = area->vm_private_data; // 如果一个文件/设备已经被映射,这里通过get_file()获得一个引用。 if (mpnt->vm_file) get_file(mpnt->vm_file); // 如果提供了一个打开函数,则在这里调用它。 if (mpnt->vm_ops && mpnt->vm_ops->open) mpnt->vm_ops->open(mpnt); // 截断VMA从而使其在将被解除映射的区域首部截止。 area->vm_end = addr; /* Truncate area */ /* Because mpnt->vm_file == area->vm_file this locks * things correctly. */ // 与前两种情况相同,锁住文件和mm。 lock_vma_mappings(area); spin_lock(&mm->page_table_lock); // 将额外的VMA插入到mm中。 __insert_vm_struct(mm, mpnt); } // 重新插入VMA到mm中。 __insert_vm_struct(mm, area); // 解锁链表。 spin_unlock(&mm->page_table_lock); // 解锁锁住共享映射的自旋锁。 unlock_vma_mappings(area); // 如果额外VMA没有再使用,则返回。如果为NULL则返回NULL。 return extra; }
(6)删除所有的内存区域
(a)exit_mmap
这个函数仅遍历与给定 mm 相关的所有 VMA ,并取消对它们的映射。
// mm/mmap.c /* Release all mmaps. */ void exit_mmap(struct mm_struct * mm) { struct vm_area_struct * mpnt; // 如果体系结构支持段并且进程使用了内存段,release_segments()将释放在进程局部 // 描述表(LDT)上与进程相关的内存段。一些应用,如著名的WINE,就使用了这个特性。 release_segments(mm); // 上锁 mm。 spin_lock(&mm->page_table_lock); // mpnt变成链表上的第一个VMA。 mpnt = mm->mmap; // 从mm中释放VMA相关的信息,这样就可以解锁mm。 mm->mmap = mm->mmap_cache = NULL; mm->mm_rb = RB_ROOT; mm->rss = 0; // 解锁 mm。 spin_unlock(&mm->page_table_lock); // 清除mm统计信息。 mm->total_vm = 0; mm->locked_vm = 0; // 刷新CPU地址范围 flush_cache_mm(mm); // 遍历每个与mm相关的VMA。 while (mpnt) { // 记录下一个要清除的VMA,这个可以被删除。 struct vm_area_struct * next = mpnt->vm_next; // 记录待删除区域的起始、末尾和大小。 unsigned long start = mpnt->vm_start; unsigned long end = mpnt->vm_end; unsigned long size = end - start; // 如果有一个与这个VMA有关的结束操作,则在这里调用它。 if (mpnt->vm_ops) { if (mpnt->vm_ops->close) mpnt->vm_ops->close(mpnt); } // 减少map计数。 mm->map_count--; // 从共享映射链表中清除文件/设备映射。 remove_shared_vm_struct(mpnt); // 释放与该区域有关的所有页面。 zap_page_range(mm, start, size); // 如果这个区域中有一个文件/设备映射,则在这里释放它。 if (mpnt->vm_file) fput(mpnt->vm_file); // 释放VMA结构。 kmem_cache_free(vm_area_cachep, mpnt); // 转到下一个VMA。 mpnt = next; } // 刷新整个mm的TLB,因为它将被解除映射。 /* This is just debugging */ // 如果map_count是正的,则意味着map计数没有正确计数,在这里调用 // BUG()来进行标记。 if (mm->map_count) BUG(); // 调用clear_page_tables() (见D. 2. 6. 2)来清除与这片区域相关的页表。 clear_page_tables(mm, FIRST_USER_PGD_NR, USER_PTRS_PER_PGD); flush_tlb_mm(mm); }
(b)clear_page_tables
这是一个用于取消所有 PTE 映射和释放区域中页面的高层函数。它在需要销毁页表时使用,如在进程退回或区域被解除映射时。
// mm/memory.c /* * This function clears all user-level page tables of a process - this * is needed by execve(), so that old pages aren't in the way. */ void clear_page_tables(struct mm_struct *mm, unsigned long first, int nr) { // 获取被解除映射mm的PGD pgd_t * page_dir = mm->pgd; // 上锁页表。 spin_lock(&mm->page_table_lock); // 在指定范围内遍历所有PGD。对找到的每一个PGD,这里调用free_one_pgd() (见 D2.6.3)。 page_dir += first; do { free_one_pgd(page_dir); page_dir++; } while (--nr); // 解锁页表。 spin_unlock(&mm->page_table_lock); /* keep the page table cache within bounds */ // 检测可用PGD结构的高速缓存。如果在PGD快表中有太多的PGD,这里将回收一些 check_pgt_cache(); }
(c)free_one_pgd
整个函数销毁一个 PGD。对 PGD 中每一个 PMD 调用 free_one_pmd() 。
// mm/memory.c static inline void free_one_pgd(pgd_t * dir) { int j; pmd_t * pmd; // 如果在这里不存在PGD,则返回。 if (pgd_none(*dir)) return; // 如果PGD已经被损坏,设置错误标志并返回 if (pgd_bad(*dir)) { pgd_ERROR(*dir); pgd_clear(dir); return; } // 获取PGD中的第一个PMD。 pmd = pmd_offset(dir, 0); // 清除PGD表项。 pgd_clear(dir); // 对 PGD 中每个 PMD,在这里调用 free_one_pmd() (见 D. 2. 6, 4)。 for (j = 0; j < PTRS_PER_PMD ; j++) { prefetchw(pmd+j+(PREFETCH_STRIDE/16)); free_one_pmd(pmd+j); } // 将PMD页释放到PMD快表中,然后,调用check_pgt_cache()。如果在高速缓存中 // 有太多的PMD页面,它们将被回收。 pmd_free(pmd); }
(d)free_one_pmd
// mm/memory.c /* * Note: this doesn't free the actual pages themselves. That * has been handled earlier when unmapping all the memory regions. */ static inline void free_one_pmd(pmd_t * dir) { pte_t * pte; // 如果在这里不存在PMD,则返回。 if (pmd_none(*dir)) return; // 如果PMD已经被损坏,则设置错误标志并返回。 if (pmd_bad(*dir)) { pmd_ERROR(*dir); pmd_clear(dir); return; } // 获得PMD中第一个PTE。 pte = pte_offset(dir, 0); // 从页表中清除PMD。 pmd_clear(dir); // 调用pte_free()将PTE页面释放到PTE快表高速缓存。 // 然后,调用check_pgtcache(),如果在高速缓存中有太多的PTE页面,则回收它们。 pte_free(pte); }
3、查找内存区域
在这部分的函数在虚拟地址空间查找已映射区域和空闲区域。
(1)查找已映射内存区域
(a)find_vma
// mm/mmap.c /* Look up the first VMA which satisfies addr < vm_end, NULL if none. */ // 其中两个参数是高层待查找的 mm_struct 以及调用者感兴趣的地址。 struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr) { // 地址没有找到时,缺省返回NULL。 struct vm_area_struct *vma = NULL; // 保证调用者不会尝试找无效的mm。 if (mm) { /* Check the cache first. */ /* (Cache hit rate is typically around 35%.) */ // mmap_cache具有上次调用find_vma()的结果。这里可能不需要遍历整个红黑树。 vma = mm->mmap_cache; // 如果这是被检查的有效VMA,这里检查被查找的地址是否包含在其中。如果是, // VMA就是mmap_cache的那个地址,所以可以返回它,否则搜索这棵树。 if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) { // 从树根开始。 rb_node_t * rb_node; rb_node = mm->mm_rb.rb_node; vma = NULL; // 这部分遍历树。 while (rb_node) { struct vm_area_struct * vma_tmp; // 这个宏正如其名,返回这个树节点指向的VMA。 vma_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb); // 检查下一个要遍历的节点是在左树还是右树叶子。 if (vma_tmp->vm_end > addr) { vma = vma_tmp; // 如果当前VMA就是所请求的VMA,这里退出while循环。 if (vma_tmp->vm_start <= addr) break; rb_node = rb_node->rb_left; } else rb_node = rb_node->rb_right; } if (vma) // 如果VMA有效,这里设置mmap_cache ,这样可以找到find_vma()的下一个调用。 mm->mmap_cache = vma; } } // 返回包含地址的VMA,作为遍历树的副作用,返回与所请求地址最接近的VMA。 return vma; }
(b)find_vma_prev
// mm/mmap.c /* Same as find_vma, but also return a pointer to the previous VMA in *pprev. */ // 实际上这与所描述的find_vma()函数相同。仅有的区别是要记住上次访问的 // 节点,因为这里表示请求VMA前面的VMA。 struct vm_area_struct * find_vma_prev(struct mm_struct * mm, unsigned long addr, struct vm_area_struct **pprev) { if (mm) { /* Go through the RB tree quickly. */ struct vm_area_struct * vma; rb_node_t * rb_node, * rb_last_right, * rb_prev; rb_node = mm->mm_rb.rb_node; rb_last_right = rb_prev = NULL; vma = NULL; while (rb_node) { struct vm_area_struct * vma_tmp; vma_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb); if (vma_tmp->vm_end > addr) { vma = vma_tmp; rb_prev = rb_last_right; if (vma_tmp->vm_start <= addr) break; rb_node = rb_node->rb_left; } else { rb_last_right = rb_node; rb_node = rb_node->rb_right; } } if (vma) { // 如果VMA有一个左节点,则意味着必须遍历它。首先是左叶子,然后沿着每 // 个右叶子直至找到树底。 if (vma->vm_rb.rb_left) { rb_prev = vma->vm_rb.rb_left; while (rb_prev->rb_right) rb_prev = rb_prev->rb_right; } *pprev = NULL; // 从红黑树节点中分离VMA。 if (rb_prev) *pprev = rb_entry(rb_prev, struct vm_area_struct, vm_rb); // 一个调试用的检查。如果这里是前面的节点,它的下一个字段将指向返回的 // VMA。如果没有,则是一个bug。 if ((rb_prev ? (*pprev)->vm_next : mm->mmap) != vma) BUG(); return vma; } } *pprev = NULL; return NULL; }
(c)find_vma_intersection
// include/linux/mm.h /* Look up the first VMA which intersects the interval start_addr..end_addr-1, NULL if none. Assume start_addr < end_addr. */ static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr) { // 返回与起始地址最近的VMA。 struct vm_area_struct * vma = find_vma(mm,start_addr); // 如果返回了一个VMA并且结束地址还小于返回VMA的起始地址,则VMA没有交叉。 if (vma && end_addr <= vma->vm_start) vma = NULL; // 如果没有交叉则返回这个VMA。 return vma; }
(2)查找空闲内存区域
(a)get_unmapped_area
这个函数的调用图如图 4.4 所示。
// mm/mmap.c // 传入的参数如下: // file 是映射的文件或设备。 // addr 是要映射到的地址。 // len 是映射的长度。 // pgoff 是在被映射文件中的偏移。 // flags 是保护标志位。 unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags) { // 有效性检查。如果请求了放置映射在特定的位置,这里保证不会溢出地址空 // 间,并且是页对齐的。 if (flags & MAP_FIXED) { if (addr > TASK_SIZE - len) return -ENOMEM; if (addr & ~PAGE_MASK) return -EINVAL; return addr; } // 如果struct file提供了一个get_unmapped_area()函数,这里使用它。 if (file && file->f_op && file->f_op->get_unmapped_area) return file->f_op->get_unmapped_area(file, addr, len, pgoff, flags); // 使用 arch_get_unmapped_area() (见 D. 3. 2. 2 节)作为 // get_unmapped_area()函数的匿名版本。 return arch_get_unmapped_area(file, addr, len, pgoff, flags); }
(b)arch_get_unmapped_area
通过定义 HAVE_ARCH_UNMAPPED_AREA,体系结构可以选择性地指定这个函数。如果体系结构不指定,就使用这个版本。
// mm/mmap.c /* Get an address range which is currently unmapped. * For shmat() with addr=0. * * Ugly calling convention alert: * Return value with the low bits set means error value, * ie * if (ret & ~PAGE_MASK) * error = ret; * * This function "knows" that -ENOMEM has the bits set. */ // 如果这里没有定义,则意味着体系结构没有提供它自己的arch_get_unmapped_area(), // 所以使用这个函数。 #ifndef HAVE_ARCH_UNMAPPED_AREA // 这里的参数与 get_unmapped_area() 相同。 static inline unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags) { struct vm_area_struct *vma; // 有效性检查保证所需的映射长度不会太长。 if (len > TASK_SIZE) return -ENOMEM; // 如果提供了一个地址,这里使用它作为映射地址。 if (addr) { // 保证地址是页对齐的。 addr = PAGE_ALIGN(addr); // find_vma() 将返回与所请求地址最近的区域。 vma = find_vma(current->mm, addr); // 保证这里的映射不会与另外一个区域重叠。如果没有重叠,就返回它,因为可 // 以安全地使用它。否则,忽略它。 if (TASK_SIZE - len >= addr && (!vma || addr + len <= vma->vm_start)) return addr; } // TASK_UNMAPPED_BASE是查询要使用的空闲区域的起点。 addr = PAGE_ALIGN(TASK_UNMAPPED_BASE); // 从TASK_UNMAPPED_BASE开始,线性查找VMA直到在其中找到足够大的 // 区域来存放新映射。这实际上是首次适应查询。 for (vma = find_vma(current->mm, addr); ; vma = vma->vm_next) { /* At this point: (!vma || addr < vma->vm_end). */ if (TASK_SIZE - len < addr) return -ENOMEM; if (!vma || addr + len <= vma->vm_start) return addr; addr = vma->vm_end; } } #else // 如果提供了一个外部函数,还需要在这里声明。 extern unsigned long arch_get_unmapped_area(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); #endif
① ⇒ find_vma
find_vma 函数
4、对内存区域上锁和解锁
这部分包含与上锁和解锁一块区域相关的函数。它们最繁杂的部分是在操作发生时需要怎样修整区域。
(1)对内存区域上锁
(a)sys_mlock
这个函数的调用图如图 4.9 所示。这就是用于上锁物理内存中一片内存区域的系统调用 mlock() 。这个函数仅检查以保证没有超过进程和用户数极限,并且要上锁的这个区域是页对齐的。
// mm/mlock.c asmlinkage long sys_mlock(unsigned long start, size_t len) { unsigned long locked; unsigned long lock_limit; int error = -ENOMEM; // 获得信号量。因为可能在这里睡眠,所以不可以使用自旋锁。 down_write(¤t->mm->mmap_sem); // 将长度向上取整到页面边界。 len = PAGE_ALIGN(len + (start & ~PAGE_MASK)); // 将地址向下取整到页面边界。 start &= PAGE_MASK; // 计算要上锁多少页面。 locked = len >> PAGE_SHIFT; // 计算在这个进程中要上锁的页面总数。 locked += current->mm->locked_vm; // 计算要锁住的页面数极限。 lock_limit = current->rlim[RLIMIT_MEMLOCK].rlim_cur; lock_limit >>= PAGE_SHIFT; /* check against resource limits */ // 不允许进程锁住过多的页面。 if (locked > lock_limit) goto out; /* we may lock at most half of physical memory... */ /* (this check is pretty bogus, but doesn't hurt) */ // 不允许进程映射多于物理内存半数的页面。 if (locked > num_physpages/2) goto out; // 调用 do_mlock() (见D.4.1.4),它首先找到与要上锁区域最近的VMA, 然后调用 // mlock_fixup() (见 D. 4. 3. 1) 。 error = do_mlock(start, len, 1); out: // 释放信号量。 up_write(¤t->mm->mmap_sem); // 从do_mlock()返回成功或错误代码 return error; }
(b)sys_mlockall
这是系统调用 mlockall(),它试着锁定在内存中调用进程的所有页面。如果指定了 MCL_CURRENT,所有的当前页面都会被上锁。如果指定了 MCL_FUTURE,所有的映射都会被锁定。这些标志位可以以或操作连接。这个函数保证标志位以及进程限制正确,然后调用 do_mlockall() 。
// mm/mlock.c asmlinkage long sys_mlockall(int flags) { unsigned long lock_limit; // 缺省时,这里返回-EINVAL表明无效的参数。 int ret = -EINVAL; // 获取当前mm_struct信号量。 down_write(¤t->mm->mmap_sem); // 保证指定了一些有效的标志。如果没有,则跳转到out来解锁信号量并且返回-EINVAL。 if (!flags || (flags & ~(MCL_CURRENT | MCL_FUTURE))) goto out; // 检查进程限制,确定可以上锁多少页面。 lock_limit = current->rlim[RLIMIT_MEMLOCK].rlim_cur; lock_limit >>= PAGE_SHIFT; // 从这里开始,缺省的错误是-ENOMEM。 ret = -ENOMEM; // 如果锁定的大小超过了设置的限定,则返回 if (current->mm->total_vm > lock_limit) goto out; /* we may lock at most half of physical memory... */ /* (this check is pretty bogus, but doesn't hurt) */ // 不允许进程锁定一半以上的物理内存。这是一个无效的检查,因为四个进程每 // 个上锁1/4的物理内存时,将超过这个限制。但还是接受这个检查,因为仅有根进程允许锁定 // 内存,所以不太可能发生这样的错误。 if (current->mm->total_vm > num_physpages/2) goto out; // 调用核心函数 do_mlockall() (见 D. 4. 1. 3)。 ret = do_mlockall(flags); out: // 解锁信号量并返回。 up_write(¤t->mm->mmap_sem); return ret; }
(c)do_mlockall
// mm/mlock.c static int do_mlockall(int flags) { int error; unsigned int def_flags; struct vm_area_struct * vma; // 调用进程必须是两种情况之一,是根进程或者具有CAP_IPC_LOCK能力。 if (!capable(CAP_IPC_LOCK)) return -EPERM; def_flags = 0; // MCL_FUTURE 标志位意思是将来的所有的页面都必须锁定,所以,如果设置 // 了该位,VMA 的 def_flags 必须为 VM_LOCKED if (flags & MCL_FUTURE) def_flags = VM_LOCKED; current->mm->def_flags = def_flags; error = 0; // 遍历所有的VMA。 for (vma = current->mm->mmap; vma ; vma = vma->vm_next) { unsigned int newflags; // 在当前的VMA标志中设置VM_LOCKED标志位。 newflags = vma->vm_flags | VM_LOCKED; // 如果没有设置MCL_CURRENT标志位而请求给所有当前页面上锁,则这里将 // 清除VM_LOCKED标志位。在这里的逻辑是这样的:不需要上锁的代码可以使用这部分功 // 能,只是没有标志位。 if (!(flags & MCL_CURRENT)) newflags &= ~VM_LOCKED; // 调用mlock_fixup() (见D. 4. 3. 1),它将调整区域来匹配上锁。 error = mlock_fixup(vma, vma->vm_start, vma->vm_end, newflags); // 任何时候如果返回了非0值,就停止上锁。注意已经上锁的VMA不会被解锁。 if (error) break; } // 返回成功或者错误代码。 return error; }
(d)do_mlock
这个函数负责根据参数值来决定上锁或解锁一块区域。我们把它分成两个部分,第 1 部分是保证区域是页对齐的(虽然只有该函数的调用者做相同的事),然后找到将要调整的 VMA。 第 2 部分接着设置合适的标志位,然后对每个受锁定影响的 VMA 调用 mlock_fixup() 。
mm/mlock.c // 这一块对请求按页对齐并找到VMA。 static int do_mlock(unsigned long start, size_t len, int on) { unsigned long nstart, end, tmp; struct vm_area_struct * vma, * next; int error; // 仅有根进程可以上锁页面。 if (on && !capable(CAP_IPC_LOCK)) return -EPERM; // 将长度按页对齐。这是一个冗余操作,因为在父函数中长度已经是页对齐的。 len = PAGE_ALIGN(len); // 计算上锁的区域末端,并保证这是一个有效的区域。如果不是,则返回-EINVAL。 end = start + len; if (end < start) return -EINVAL; // 如果上锁的区域大小为0,则在这里返回。 if (end == start) return 0; // 找到那些受该上锁操作影响的VMA。 vma = find_vma(current->mm, start); // 如果该地址范的VMA不存在,则返回-ENOMEM, if (!vma || vma->vm_start > start) return -ENOMEM; // 这一块遍历由这个上锁操作影响的VMA并对每个VMA调用mlock_fixup() // 遍历所需VMA以上锁页面。 for (nstart = start ; ; ) { unsigned int newflags; /* Here we know that vma->vm_start <= nstart < vma->vm_end. */ // 设置在VMA上的VM_LOCKED标志位。 newflags = vma->vm_flags | VM_LOCKED; // 如果有一个解锁,则移除这个标志位。 if (!on) newflags &= ~VM_LOCKED; // 如果这个VMA是最后一个受到解锁影响的VMA,则在这里调用mlock_fixup。 // 并传入上锁的末地址,然后返回。 if (vma->vm_end >= end) { error = mlock_fixup(vma, nstart, end, newflags); break; } // 这是整个需要上锁的VMA。为了上锁,将这个VMA的末端作为一个参数传 // 入mlock_fixup() (见D. 4. 3. 1),而不是实际上锁的末尾。 // tmp是在这个VMA中映射的末端。 tmp = vma->vm_end; // next是受到上锁影响的下一个VMA。 next = vma->vm_next; // 在这个 VMA 上调用 mlock_fixup (见 D. 4. 3. 1) 。 error = mlock_fixup(vma, nstart, tmp, newflags); // 如果发生错误,则在这里跳出循环,请记住VMA已经被锁定但是还没有修整好。 if (error) break; // 下一个起始地址是下一个VMA的起点。 nstart = tmp; // 移到下一个VMA。 vma = next; // 如果没有VMA,在这里返回-ENOMEM。下一个条件是,在mlock_fixup()实 // 现中断或有重叠的VMA时,区域将会很分散。 if (!vma || vma->vm_start != nstart) { error = -ENOMEM; break; } } // 返回错误或成功代码。 return error; }
(2)对区域解锁
(a)sys_munlock
在这里对请求按页对齐,然后调用 do_mlock() ,它开始修整区域的工作。
// mm/mlock.c asmlinkage long sys_munlock(unsigned long start, size_t len) { int ret; // 获取保护mm_struct的信号量 down_write(¤t->mm->mmap_sem); // 将区域长度向上取整为最接近的页面边界 len = PAGE_ALIGN(len + (start & ~PAGE_MASK)); // 将区域的起点向下取整为最接近的页面边界。 start &= PAGE_MASK; // 调用do_mlock() (见D. 4. L 4),将0作为第3个参数传入来解锁这片区域。 ret = do_mlock(start, len, 0); // 释放信号量。 up_write(¤t->mm->mmap_sem); // 返回成功或者失败代码。 return ret; }
(2)对区域解锁
(a)sys_munlock
在这里对请求按页对齐,然后调用 do_mlock() ,它开始修整区域的工作。
// mm/mlock.c asmlinkage long sys_munlock(unsigned long start, size_t len) { int ret; // 获取保护mm_struct的信号量 down_write(¤t->mm->mmap_sem); // 将区域长度向上取整为最接近的页面边界 len = PAGE_ALIGN(len + (start & ~PAGE_MASK)); // 将区域的起点向下取整为最接近的页面边界。 start &= PAGE_MASK; // 调用do_mlock() (见D. 4. L 4),将0作为第3个参数传入来解锁这片区域。 ret = do_mlock(start, len, 0); // 释放信号量。 up_write(¤t->mm->mmap_sem); // 返回成功或者失败代码。 return ret; }
(b)sys_munlockall
这个函数很小。如果 mlockall() 的标志位为 0,其意思是不需要当前页面处于当前状态,页不需要锁定其他的映射,这也意味着 VM_LOCKED 标志位将从所有的 VMA 中移除。
// mm/mlock.c asmlinkage long sys_munlockall(void) { int ret; // 获取保护mm_struct的信号量。 down_write(¤t->mm->mmap_sem); // 调用do_mlockall() (见D. 4. L 3),传入 0 作为标志位,这将把 VM_LOCKED 从所有 // 的VMA中移除。 ret = do_mlockall(0); // 释放信号量。 up_write(¤t->mm->mmap_sem); // 返回错误或者成功代码。 return ret; }
(3)上锁/解锁后修整区域
(a)mlock_fixup
必须说明的是,这个函数识别 4 种不同类型的锁。第 1 种是锁定整个 VMA 时,调用 mlock_fixup_all() 。第 2 种是仅有 VMA 的首部受影响,它由 mlock_fixup_start() 处理。 第 3 种是锁定末尾的区域,这由 mlock_fixup_end() 处理。第 4 种是利用 mlock_fixup_middle() 来锁定中间部分的区域。
// mm/mlock.c static int mlock_fixup(struct vm_area_struct * vma, unsigned long start, unsigned long end, unsigned int newflags) { int pages, retval; // 如果没有发生改变则直接返回。 if (newflags == vma->vm_flags) return 0; // 如果上锁的起点是VMA的开始部分,则意味着或者是整个区域被锁定,或者仅是首部被锁定。 if (start == vma->vm_start) { // 如果是整个VMA被锁定,这里就调用 mlock_fixup_all() if (end == vma->vm_end) retval = mlock_fixup_all(vma, newflags); else // 如果被锁定的VMA中开始部分与锁定的开始部分相匹配,则这里调用 mlock_fixup_start() retval = mlock_fixup_start(vma, end, newflags); } else { // 这意味着或者在末尾的区域要被锁定,或者在中间的区域要被锁定。 // 如果锁定的末端与VMA的末端相匹配,这里调用mlock + mlock_fixup_middle() if (end == vma->vm_end) retval = mlock_fixup_end(vma, start, newflags); else retval = mlock_fixup_middle(vma, start, end, newflags); } // 在这里,修整函数成功时返回0。如果成功修整该区域且该区域已经被标记为 // 锁定,就在这里调用make_pages_present()做一些基本的检查,然后调用get_user_pages()。 // get_user_pages()与缺页中断处理程序一样陷入所有页面。 if (!retval) { /* keep track of amount of locked VM */ pages = (end - start) >> PAGE_SHIFT; if (newflags & VM_LOCKED) { pages = -pages; make_pages_present(start, end); } vma->vm_mm->locked_vm -= pages; } return retval; }
(b)mlock_fixup_all
// mm/mlock.c static inline int mlock_fixup_all(struct vm_area_struct * vma, int newflags) { // 这个函数很小,它利用自旋锁锁住VMA,设置新标志位,释放锁并返回成功。 spin_lock(&vma->vm_mm->page_table_lock); vma->vm_flags = newflags; spin_unlock(&vma->vm_mm->page_table_lock); return 0; }
(c)mlock_fixup_start
这个有点复杂,它需要一个新的 VMA 来表示受影响的区域。旧 VMA 的起始位置移到前面。
// mm/mlock.c static inline int mlock_fixup_start(struct vm_area_struct * vma, unsigned long end, int newflags) { struct vm_area_struct * n; // 从slab分配器中为受影响的区域分配一个VMA。 n = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL); if (!n) return -EAGAIN; // 复制所需的信息。 *n = *vma; n->vm_end = end; n->vm_flags = newflags; n->vm_raend = 0; // 如果VMA有一个文件或设备映射,get_file() 将增加引用计数。 if (n->vm_file) get_file(n->vm_file); // 如果提供了一个open()函数,在这里调用它。 if (n->vm_ops && n->vm_ops->open) n->vm_ops->open(n); // 更新将在上锁区域末端的旧VMA的文件偏移或设备映射偏移。 vma->vm_pgoff += (end - vma->vm_start) >> PAGE_SHIFT; // 若该VMA是一个共享区域,lock_vma_mappings()将上锁任何文件。 lock_vma_mappings(vma); // 上锁父mm_struct,更新其起点为受影响区域的末端,将一个新的VMA加入进程 // 链表(见D. 2. 2.1)并释放锁。 spin_lock(&vma->vm_mm->page_table_lock); vma->vm_start = end; __insert_vm_struct(current->mm, n); spin_unlock(&vma->vm_mm->page_table_lock); // 利用unlock_vma_mappings()解锁文件映射。 unlock_vma_mappings(vma); // 返回成功。 return 0; }
(d)mlock_fixup_end
除了受影响区域是在 VMA 末端之外,这个函数实际上与 mlock_fixup_start() 相同。
// mm/mlock.c static inline int mlock_fixup_end(struct vm_area_struct * vma, unsigned long start, int newflags) { struct vm_area_struct * n; // 从slab分配器上为受影响区域分配一个VMA。 n = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL); if (!n) return -EAGAIN; // 向文件或设备映射中复制所需信息并更新其中的偏移。 *n = *vma; n->vm_start = start; n->vm_pgoff += (n->vm_start - vma->vm_start) >> PAGE_SHIFT; n->vm_flags = newflags; n->vm_raend = 0; // 如果VMA有一个文件或设备映射,get_file()将引用计数加1。 if (n->vm_file) get_file(n->vm_file); // 如果提供了一个open()函数,这里将调用它 if (n->vm_ops && n->vm_ops->open) n->vm_ops->open(n); // 如果该VMA是一个共享区域,lock_vma_mappings()将上锁任何文件。 lock_vma_mappings(vma); // 上锁父mm_struct,更新其起点为受影响区域的末端,将一个新的VMA加入进程 // 链表(见D.2.2.1节)并释放锁。 spin_lock(&vma->vm_mm->page_table_lock); vma->vm_end = start; __insert_vm_struct(current->mm, n); spin_unlock(&vma->vm_mm->page_table_lock); // 利用unlock_vma_mappings()解锁文件映射。 unlock_vma_mappings(vma); // 返回成功。 return 0; }
(e)mlock_fixup_middle
除了需要两个新区域来修整映射外,这个函数与前面两个修整函数类似。
// mm/mlock.c static inline int mlock_fixup_middle(struct vm_area_struct * vma, unsigned long start, unsigned long end, int newflags) { struct vm_area_struct * left, * right; // 从slab分配器中分配两块新的VMA。 left = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL); if (!left) return -EAGAIN; right = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL); if (!right) { kmem_cache_free(vm_area_cachep, left); return -EAGAIN; } // 将信息从旧VMA中复制到新的VMA中。 *left = *vma; *right = *vma; // 左区域的末端是受影响区域的始端。 left->vm_end = start; // 右区域的始端是受影响区域的末端。 right->vm_start = end; // 更新文件偏移。 right->vm_pgoff += (right->vm_start - left->vm_start) >> PAGE_SHIFT; // 旧VMA现在是受影响区域,所以这里更新它的标志位。 vma->vm_flags = newflags; // 将预读窗口设为0,以保证不属于它们区域的页面不会突然预读。 left->vm_raend = 0; right->vm_raend = 0; // 如果有文件/设备映射,则增加它们的引用计数。 if (vma->vm_file) atomic_add(2, &vma->vm_file->f_count); // 对两个新映射调用open()函数。 if (vma->vm_ops && vma->vm_ops->open) { vma->vm_ops->open(left); vma->vm_ops->open(right); } // 取消预读窗口,更新将处于上锁区域始端文件中的偏移。 vma->vm_raend = 0; vma->vm_pgoff += (start - vma->vm_start) >> PAGE_SHIFT; // 上锁共享文件/设备映射。 lock_vma_mappings(vma); // 上锁父mm_struct,更新VMA并将其插入到这两个新区域到进程链表中,然后再次释放锁。 spin_lock(&vma->vm_mm->page_table_lock); vma->vm_start = start; vma->vm_end = end; vma->vm_flags = newflags; __insert_vm_struct(current->mm, left); __insert_vm_struct(current->mm, right); spin_unlock(&vma->vm_mm->page_table_lock); // 解锁共享区域。 unlock_vma_mappings(vma); // 返回成功。 return 0; }
深入理解Linux虚拟内存管理(七)(下):https://developer.aliyun.com/article/1597891