深入理解Linux虚拟内存管理(七)(中)

简介: 深入理解Linux虚拟内存管理(七)

深入理解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 所示。

4.4.5 查找空闲内存区域

// 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(&current->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(&current->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(&current->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(&current->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(&current->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(&current->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(&current->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(&current->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(&current->mm->mmap_sem);
  // 调用do_mlockall() (见D. 4. L 3),传入 0 作为标志位,这将把 VM_LOCKED 从所有
  // 的VMA中移除。
  ret = do_mlockall(0);
  // 释放信号量。
  up_write(&current->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

目录
相关文章
|
9天前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
|
18天前
|
Linux 调度
深入理解Linux虚拟内存管理(七)(下)
深入理解Linux虚拟内存管理(七)
33 4
|
18天前
|
存储 Linux 索引
深入理解Linux虚拟内存管理(九)(中)
深入理解Linux虚拟内存管理(九)
17 2
|
18天前
|
Linux 索引
深入理解Linux虚拟内存管理(九)(上)
深入理解Linux虚拟内存管理(九)
23 2
|
18天前
|
机器学习/深度学习 消息中间件 Unix
深入理解Linux虚拟内存管理(九)(下)
深入理解Linux虚拟内存管理(九)
15 1
|
18天前
|
Linux
深入理解Linux虚拟内存管理(七)(上)
深入理解Linux虚拟内存管理(七)
24 1
|
16天前
|
缓存 Linux 调度
Linux服务器如何查看CPU占用率、内存占用、带宽占用
Linux服务器如何查看CPU占用率、内存占用、带宽占用
54 0
|
18天前
|
Linux API
深入理解Linux虚拟内存管理(六)(下)
深入理解Linux虚拟内存管理(六)
12 0
|
18天前
|
Linux
深入理解Linux虚拟内存管理(六)(中)
深入理解Linux虚拟内存管理(六)
18 0
|
18天前
|
存储 Linux Serverless
深入理解Linux虚拟内存管理(六)(上)
深入理解Linux虚拟内存管理(六)
15 0