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

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

一、进程内存描述符

1、 进程内存描述符

(1)初始化一个描述符

传送门 4.3.2 初始化一个描述符

    系统中的 mm_struct 开始称为 init_mm ,它在编译时由宏 INIT_MM() 静态初始化。

// include/linux/sched.h
#define INIT_MM(name) \
{             \
  mm_rb:    RB_ROOT,      \
  pgd:    swapper_pg_dir,     \
  mm_users: ATOMIC_INIT(2),     \
  mm_count: ATOMIC_INIT(1),     \
  mmap_sem: __RWSEM_INITIALIZER(name.mmap_sem), \
  page_table_lock: SPIN_LOCK_UNLOCKED,    \
  mmlist:   LIST_HEAD_INIT(name.mmlist),  \
}

// arch/i386/kernel/init_task.c
struct mm_struct init_mm = INIT_MM(init_mm);


    新 mm_struct 在建立以后,是它们父 mm_struct 的备份,并且它们由 copy_mm()init_mm() 初始化的字段来复制。

(2)复制一个描述符

(a)copy_mm

    这个函数为给定的进程复制一份 mm_struct 。它仅在创建一个新进程后且需要它自己的 mm_struct 时由 do_fork() 调用。

// kernel/fork.c
// 这一块重置没有被子 mm_struct 继承的字段并找到一个复制源 mm 的字段。
// 这些参数是为克隆而传入的标志位和那些复制 mm_struct 的进程。
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
  struct mm_struct * mm, *oldmm;
  int retval;

  // 初始化与内存管理相关的task_struct字段。
  tsk->min_flt = tsk->maj_flt = 0;
  tsk->cmin_flt = tsk->cmaj_flt = 0;
  tsk->nswap = tsk->cnswap = 0;

  tsk->mm = NULL;
  tsk->active_mm = NULL;

  /*
   * Are we cloning a kernel thread?
   *
   * We need to steal a active VM for that..
   */
  // 借用当前运行进程的mm来复制。   
  oldmm = current->mm;
  // 一个没有mm的内核线程,所以它可以立即返回。
  if (!oldmm)
    return 0;

  // 如果设置了 CLONE_VM 标志位,子进程将与父进程共享mm。像 pthreads 这
  // 样的用户需要这样做。mm_users 字段加1, 以使mm不会过早销毁。
  // good_mm 标记设置 tsk->mm 和 tsk->active_mm, 并返回成功
  if (clone_flags & CLONE_VM) {
    atomic_inc(&oldmm->mm_users);
    mm = oldmm;
    goto good_mm;
  }

  retval = -ENOMEM;
  // 分配新的mm。
  mm = allocate_mm();
  if (!mm)
    goto fail_nomem;

  /* Copy the current MM stuff.. */
  // 复制父mm, 并利用mm_init()来初始化特定进程的mm字段。
  memcpy(mm, oldmm, sizeof(*mm));
  if (!mm_init(mm))
    goto fail_nomem;
  // 为那些无法自动管理其MMU的体系结构初始化MMU上下文。
  if (init_new_context(tsk,mm))
    goto free_pt;
  // 调用dup_mmap(),它负责复制所有VMA父进程用到的区域。
  down_write(&oldmm->mmap_sem);
  retval = dup_mmap(mm);
  up_write(&oldmm->mmap_sem);

  // 一旦成功,dup_mmap() 返回 0。如果失败,标记 free_pt 将调用 mmput() 。它将
  // mm 的使用计数减 1
  if (retval)
    goto free_pt;

  /*
   * child gets a private LDT (if there was an LDT in the parent)
   */
  // 基于父进程为新进程复制LDT。 
  copy_segments(tsk, mm);

good_mm:
  // 设置新mm,active_mm 并返回成功。
  tsk->mm = mm;
  tsk->active_mm = mm;
  return 0;

free_pt:
  mmput(mm);
fail_nomem:
  return retval;
}
(b)mm_init

    这个函数初始化特定进程的 mm 字段。

// kernel/fork.c
#define free_mm(mm) (kmem_cache_free(mm_cachep, (mm)))

static struct mm_struct * mm_init(struct mm_struct * mm)
{
  // 设置用户数为l。
  atomic_set(&mm->mm_users, 1);
  // 设置mm的引用计数为1。
  atomic_set(&mm->mm_count, 1);
  // 初始化保护VMA链表的信号量。
  init_rwsem(&mm->mmap_sem);
  // 初始化保护写访问的自旋锁。
  mm->page_table_lock = SPIN_LOCK_UNLOCKED;
  // 为该结构分配新的PGD。
  mm->pgd = pgd_alloc(mm);
  mm->def_flags = 0;
  if (mm->pgd)
    return mm;
  free_mm(mm);
  return NULL;
}
① ⇒ pgd_alloc

    pgd_alloc 函数

② ⇒ kmem_cache_free

    kmem_cache_free 函数

(3)分配一个描述符

    提供了两个函数来分配一个 mm_struct。虽然有点容易混淆,但它们实际上都是一样的。allocate_mm() 将从 slab 分配器中分配一个 mm_structmm_alloc() 将分配结构,然后调用 mm_init() 来初始化。

(a)allocate_mm
// kernel/fork.c
#define allocate_mm() (kmem_cache_alloc(mm_cachep, SLAB_KERNEL))
// 从slab分配器分配一个mm_struct。
(b)mm_alloc
// kernel/fork.c
/*
 * Allocate and initialize an mm_struct.
 */
struct mm_struct * mm_alloc(void)
{
  struct mm_struct * mm;
  // 从slab分配器分配一个mm_struct。
  mm = allocate_mm();
  if (mm) {
  // 将结构的所有字段归0。
    memset(mm, 0, sizeof(*mm));
  // 进行基本的初始化。    
    return mm_init(mm);
  }
  return NULL;
}

(4)销毁一个描述符

    mm 的一个新用户利用如下调用将使用计数加1:

   atomic_inc( &mm->mm_users );

   利用 mmput() 将使用计数减 1。如果 mm_users 计数减到 0,将调用 exit_mmap() 将所有的已映射区域删除,而且所有的页表也将会销毁,因为已经没有任何使用用户空间部分的使用者。mm_count 计数由 mmdrop() 减 1,因为页表和 VMA 的使用者都记为一个 mm_struct 使用者。在 mm_count 减到 0 时,mm_struct 将会被销毁。

(a)mmput

// kernel/fork.c
/*
 * Decrement the use count and release all resources for an mm.
 */
void mmput(struct mm_struct *mm)
{
  // 在获取 mmlist_lock 锁时,原子性地将 mm_users 字段减 1。若计数减到 0 则返回该锁。
  if (atomic_dec_and_lock(&mm->mm_users, &mmlist_lock)) {
// 如果使用计数减到0,需要将mm和相关的结构移除。
// swap_mm 时由 vmscan 代码换出最后的mm。如果当前进程是最后换出的
// mm,这里将移到链表的下一个表项。
    extern struct mm_struct *swap_mm;
    if (swap_mm == mm)
      swap_mm = list_entry(mm->mmlist.next, struct mm_struct, mmlist);
  // 从链表中移除这个mm。      
    list_del(&mm->mmlist);
  // 减少列表中的mm计数y并释放mmlist锁。   
    mmlist_nr--;
    spin_unlock(&mmlist_lock);
  // 移除所有的相关映射
    exit_mmap(mm);
  // 删除 mm    
    mmdrop(mm);
  }
}
(b)mmdrop
// include/linux/sched.h
static inline void mmdrop(struct mm_struct * mm)
{
  // 原子性地将引用计数减 1, 如果 mm 由延迟 tlb 交换进程所用,引用计数将比较高。
  if (atomic_dec_and_test(&mm->mm_count))
  // 如果引用计数减为 0, 这里将调用 __mmdrop() 。
    __mmdrop(mm);
}
(c)__mmdrop
// kernel/fork.c
#define free_mm(mm) (kmem_cache_free(mm_cachep, (mm)))
/*
 * Called when the last reference to the mm
 * is dropped: either by a lazy thread or by
 * mmput. Free the page directory and the mm.
 */
inline void __mmdrop(struct mm_struct *mm)
{
  // 保证 init_mm 没有被销毁。
  BUG_ON(mm == &init_mm);
  // 删除PGD表项。
  pgd_free(mm->pgd);
  // 删除LDT(局部描述表)。
  destroy_context(mm);
  // 在 mm 上调用 kmem_cache_free(), 利用slab分配器将其释放。
  free_mm(mm);
}
① ⇒ pgd_free

    pgd_free 函数

② ⇔ destroy_context
// include/asm-i386/mmu_context.h
/*
 * possibly do the LDT unload here?
 */
#define destroy_context(mm)   do { } while(0)
③ ⇒ kmem_cache_free

    kmem_cache_free 函数

2、创建内存区域

    这一大部分讨论创建、删除和处理内存区域。

(1)创建一个内存区域

    创建内存区域的主要调用图如图 4.3 所示。

    传送门 4.4.3 创建内存区域

(a)do_mmap

  这是一个对 do_mmap_pgoff() 的简单封装函数,它完成如下大部分工作。

// include/linux/mm.h
static inline unsigned long do_mmap(struct file *file, unsigned long addr,
  unsigned long len, unsigned long prot,
  unsigned long flag, unsigned long offset)
{
  // 缺省时,这里返回-EINVAL。
  unsigned long ret = -EINVAL;
  // 保证区域的大小不会超过地址空间的总大小。
  if ((offset + PAGE_ALIGN(len)) < offset)
    goto out;
  // 按页面排列offset,并且调用do_mmap_pgoff()来映射该区域。 
  if (!(offset & ~PAGE_MASK))
    ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
  return ret;
}
(b)do_mmap_pgoff

    这个函数非常大,所以把它分成几个部分。粗略说来,有如下部分:

  • 参数的有效检查。

找到一块足够大的线性地址空间来完成内存映射。如果提供了文件系统或设备特定的 get_unmapped_area(),就使用这个函数。否则,调用 arch_get_unmapped_area() 。

计算 VM 标志位,检查它们是否违反了文件访问权限。

如果在要映射的地方存在一个旧的区域,修整它从而使它适合新的映射。

从 slab 分配器中分配 vm_area_struct 并填充表项。

链接到新的 VMA 。

调用文件系统或者设备相关的 mmap() 函数。

  • 更新统计并退出。
// mm/mmap.c
// 与 mmap 系统调用参数有直接相关的参数如下:
//  file 是若有后援文件映射的 mmap 的文件结构。
//  addr 是映射的请求地址。
//  len 是要 mmap 的字节数。
//  prot 是该区域的权限。
//  flags 是映射的标志位。
//  pgoff 是开始 mmap 的文件偏移
unsigned long do_mmap_pgoff(struct file * file, unsigned long addr, unsigned long len,
  unsigned long prot, unsigned long flags, unsigned long pgoff)
{
  struct mm_struct * mm = current->mm;
  struct vm_area_struct * vma, * prev;
  unsigned int vm_flags;
  int correct_wcount = 0;
  int error;
  rb_node_t ** rb_link, * rb_parent;

  // 如果映射了一个文件或设备,这里确定提供了一个文件系统或者设备相关的
  // mmap 函数。对多数文件系统而言,这里将调用 generic_file_mmap() (见D. 6.2.1)。
  if (file && (!file->f_op || !file->f_op->mmap))
    return -ENODEV;
  // 保证长度为 0 的 mmap() 不被请求。
  if (!len)
    return addr;
  // 保证映射限于地址空间的用户空间部分。在x86中,内核空间开始于 PAGE_OFF_SET (3 GB)。
  len = PAGE_ALIGN(len);

  if (len > TASK_SIZE || len == 0)
    return -EINVAL;

  /* offset overflow? */
  // 保证映射不会超过最大可能的文件大小。
  if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
    return -EINVAL;

  /* Too many mappings? */
  // 仅允许max_map_count数量个映射。缺省时,这个数值为 DEFAULT_MAX_MAP_COUNT
  // 或者 65536 个映射。
  if (mm->map_count > max_map_count)
    return -ENOMEM;

  /* Obtain the address to map to. we verify (or select) it and ensure
   * that it represents a valid section of the address space.
   */
  // 在基本的有效性检查后,这个函数将调用设备或文件相关的 get_unmapped_area()
  // 数。如果没有设备相关的函数,则调用 arch_get_unmapped_area()。
  // 这个函数在 D3.2.2 进行描述。 
  addr = get_unmapped_area(file, addr, len, pgoff, flags);
  if (addr & ~PAGE_MASK)
    return addr;

  /* Do simple checking here so the lower-level routines won't have
   * to. we assume access permissions have been handled by the open
   * of the memory object, so we don't do any here.
   */
  // calc_vm_flags() 将 prot 和 flags 从用户空间转换为 VM 的相应标志位。 
  vm_flags = calc_vm_flags(prot,flags) | mm->def_flags | VM_MAYREAD | 
              VM_MAYWRITE | VM_MAYEXEC;

  /* mlock MCL_FUTURE? */
  // 检查将来所有的映射是否都在内存中被上锁。如果是,这里保证进程不会上锁
  // 比允许它上锁还要多的内存。如果多上锁,就返回-EAGAIN。
  if (vm_flags & VM_LOCKED) {
    unsigned long locked = mm->locked_vm << PAGE_SHIFT;
    locked += len;
    if (locked > current->rlim[RLIMIT_MEMLOCK].rlim_cur)
      return -EAGAIN;
  }

// 如果一个文件有内存映射,这里将检查文件的访问权限。
  if (file) {
    switch (flags & MAP_TYPE) {
    case MAP_SHARED:
  // 如果请求写访问,这里保证文件已经以写方式打开。    
      if ((prot & PROT_WRITE) && !(file->f_mode & FMODE_WRITE))
        return -EACCES;

      /* Make sure we don't allow writing to an append-only file.. */
  // 相似地,如果文件以追加方式打开,这里保证它不能被写入。但这里并不检查
  // prot, 因为prot字段仅应用于映射那些需要检查的已打开文件。      
      if (IS_APPEND(file->f_dentry->d_inode) && (file->f_mode & FMODE_WRITE))
        return -EACCES;

      /* make sure there are no mandatory locks on the file. */
  // 如果文件是强制上锁的,这里返回-EAGAIN从而调用者将试着用第2种类型。      
      if (locks_verify_locked(file->f_dentry->d_inode))
        return -EAGAIN;
  // 修整标志位以与文件标志位一致。
      vm_flags |= VM_SHARED | VM_MAYSHARE;
      if (!(file->f_mode & FMODE_WRITE))
        vm_flags &= ~(VM_MAYWRITE | VM_SHARED);

      /* fall through */
    case MAP_PRIVATE:
  // 保证文件可以在mmaping前可以读。    
      if (!(file->f_mode & FMODE_READ))
        return -EACCES;
      break;

    default:
      return -EINVAL;
    }
  } else {
// 如果文件映射用于匿名使用,如果请求映射是MAP_PRIVATE,这里将修整标
// 志位以使标志位一致。   
    vm_flags |= VM_SHARED | VM_MAYSHARE;
    switch (flags & MAP_TYPE) {
    default:
      return -EINVAL;
    case MAP_PRIVATE:
      vm_flags &= ~(VM_SHARED | VM_MAYSHARE);
      /* fall through */
    case MAP_SHARED:
      break;
    }
  }

  /* Clear old maps */
munmap_back:
  // find_vma_prepare() (见D. 2. 2. 2) 遍历对应于某个给定地址 VMA 的 RB 树。
  vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);
  // 如果找到一个VMA,并且是新映射的一部分,这里就移除旧的映射,因为新的
  // 映射将涵盖这两部分。
  if (vma && vma->vm_start < addr + len) {
    if (do_munmap(mm, addr, len))
      return -ENOMEM;
    goto munmap_back;
  }

  /* Check against address space limit. */
  // 保证新映射将不会超过允许一个进程拥有的总VM,现在还不清楚为什么不在 前面进行这项检查。
  if ((mm->total_vm << PAGE_SHIFT) + len
      > current->rlim[RLIMIT_AS].rlim_cur)
    return -ENOMEM;

  /* Private writable mapping? Check memory availability.. */
  // 如果调用者没有特别请求那些没有利用MAP_NORESERVE检查的空闲空
  // 间,并且这是一个私有映射,就在这里保证当前条件下有足够的内存来满足映射。
  if ((vm_flags & (VM_SHARED | VM_WRITE)) == VM_WRITE &&
      !(flags & MAP_NORESERVE)         &&
      !vm_enough_memory(len >> PAGE_SHIFT))
    return -ENOMEM;

  /* Can we just expand an old anonymous mapping? */
  // 如果两个相邻的内存映射是匿名的,并且可被视为一个,这里将扩展旧的映射
  // 而不是创建一个新映射。
  if (!file && !(vm_flags & VM_SHARED) && rb_parent)
    if (vma_merge(mm, prev, rb_parent, addr, addr + len, vm_flags))
      goto out;

  /* Determine the object being mapped and call the appropriate
   * specific mapper. the address has already been validated, but
   * not unmapped, but the maps are removed from the list.
   */
  // 从slab分配器中分配一个vm_area_struct。 
  vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
  if (!vma)
    return -ENOMEM;
  // 填充基本的vm_area_struct字段。
  vma->vm_mm = mm;
  vma->vm_start = addr;
  vma->vm_end = addr + len;
  vma->vm_flags = vm_flags;
  vma->vm_page_prot = protection_map[vm_flags & 0x0f];
  vma->vm_ops = NULL;
  vma->vm_pgoff = pgoff;
  vma->vm_file = NULL;
  vma->vm_private_data = NULL;
  vma->vm_raend = 0;

// 如果这个文件被映射,填充文件相关的字段。
  if (file) {
    error = -EINVAL;
  // 这两个是一个文件映射无效的标志位,所以这里释放 vm_area_struct 并返回。    
    if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
      goto free_vma;
  // 这个标志位由系统调用 mmap() 清除,也由那些直接调用这个函数的内核模块
  // 清除。以前,如果写了底层的函数就会返回一个 -ETXTBUSY 为调用进程。     
    if (vm_flags & VM_DENYWRITE) {
      error = deny_write_access(file);
      if (error)
        goto free_vma;
      correct_wcount = 1;
    }
  // 填充 vm_file 字段。   
    vma->vm_file = file;
  // 将文件使用计数加1。   
    get_file(file);
  // 调用文件系统或设备相关的mmap()函数。在许多文件系统中,这里将调用
  // generic_file_mmap() (见 D.6.2.1) 。
    error = file->f_op->mmap(file, vma);
  // 如果调用了一个错误,这里将转到 unmap_and_free_vma 清除并返回错误。  
    if (error)
      goto unmap_and_free_vma;
  } else if (flags & MAP_SHARED) {
  // 如果这是一个匿名共享映射,则区域由 shmem_zero_setup() 来创建和建造(见L.7.1小节)。
  // 匿名共享也由一个虚拟tmpfs文件系统后援,所以它们可以很好地在交换区同步。其
  // 中的写回函数是shmem_writepage() (见L.6.1小节)
    error = shmem_zero_setup(vma);
    if (error)
      goto free_vma;
  }

  /* Can addr have changed??
   *
   * Answer: Yes, several device drivers can do it in their
   *         f_op->mmap method. -DaveM
   */
  // 如果地址发生改变,则意味着设备相关的mmap设备把VMA地址移到其他地
  // 方。利用函数 find_vma_prepare() (见D.2.2.2) 来找到 VMA 移到的位置。   
  if (addr != vma->vm_start) {
    /*
     * It is a bit too late to pretend changing the virtual
     * area of the mapping, we just corrupted userspace
     * in the do_munmap, so FIXME (not in 2.4 to avoid breaking
     * the driver API).
     */
    struct vm_area_struct * stale_vma;
    /* Since addr changed, we rely on the mmap op to prevent 
     * collisions with existing vmas and just use find_vma_prepare 
     * to update the tree pointers.
     */
    addr = vma->vm_start;
    stale_vma = find_vma_prepare(mm, addr, &prev,
            &rb_link, &rb_parent);
    /*
     * Make sure the lowlevel driver did its job right.
     */
    if (unlikely(stale_vma && stale_vma->vm_start < vma->vm_end)) {
      printk(KERN_ERR "buggy mmap operation: [<%p>]\n",
        file ? file->f_op->mmap : NULL);
      BUG();
    }
  }

  // 连接新的 vm_area_struct。
  vma_link(mm, vma, prev, rb_link, rb_parent);
  // 更新文件写计数。
  if (correct_wcount)
    atomic_inc(&file->f_dentry->d_inode->i_writecount);

out:  
  // 更新进程 mm_struct 的统计计数并返回新地址。
  mm->total_vm += len >> PAGE_SHIFT;
  if (vm_flags & VM_LOCKED) {
    mm->locked_vm += len >> PAGE_SHIFT;
    make_pages_present(addr, addr + len);
  }
  return addr;

// 如果文件在失败前已经部分地映射,则将到达这里。在这里将更新写统计计
// 数,然后所有的用户页面都由zap_page_range()移除。
unmap_and_free_vma:
  if (correct_wcount)
    atomic_inc(&file->f_dentry->d_inode->i_writecount);
  vma->vm_file = NULL;
  fput(file);

  /* Undo any partial mapping done by a device driver. */
  zap_page_range(mm, vma->vm_start, vma->vm_end - vma->vm_start);
  
// 这里的goto用于在vm_area_struct创建后映射失败的情况下。在返回错误前
// 它退回到slab分配器。 
free_vma:
  kmem_cache_free(vm_area_cachep, vma);
  return error;
}


① ⇒ get_unmapped_area

    get_unmapped_area 函数

② ⇔ find_vma_prepare

    find_vma_prepare 函数

(2)插入一个内存区域

    insert_vm_struct() 的调用图如图 4.5 所示。

(a)__insert_vm_struct

    这是将一个新 VMA 插入地址空间的顶层函数。另外有个类似的函数称为 insert_vm_struct ,在这里不详细描述,因为它们惟一的区别就是有一行代码增加了 map_count

// mm/mmap.c
/* Insert vm structure into process list sorted by address
 * and into the inode's i_mmap ring.  If vm_file is non-NULL
 * then the i_shared_lock must be held here.
 */
// 这里的参数表示线性地址空间的mm_struct和待插入的vm_area_struct。 
void __insert_vm_struct(struct mm_struct * mm, struct vm_area_struct * vma)
{
  struct vm_area_struct * __vma, * prev;
  rb_node_t ** rb_link, * rb_parent;

  // find_vma_prepare() (见D.2.2.2) 定位新VMA可以被插入的位置。它将被插入在
  // prev 和 __vma 之间,并且也返回红黑树的所需节点。
  __vma = find_vma_prepare(mm, vma->vm_start, &prev, &rb_link, &rb_parent);
  // 这里检查保证所返回的VMA是无效的。实际上,如果没有手动地向地址空
  // 间插入伪VMA,则这个条件不可能发生。
  if (__vma && __vma->vm_start < vma->vm_end)
    BUG();
  // 这个函数完成把VMA结构链接到线性链表以及红黑树中的实际工作。  
  __vma_link(mm, vma, prev, rb_link, rb_parent);
  // 增加 map_count 来显示增加了一个新的映射。这一行没有在 insert_vm_struct() 中提供。
  mm->map_count++;
  // validate_mm()是一个调试红黑树的宏。如果设置了 DEBUG_MM_RB 标志位,将
  // 遍历VMA的线性链表和红黑树以保证它有效。由于这个遍历函数是一个递归函数,所以很
  // 重要的是它必须在真正需要的时候才调用,因为大量的映射将导致栈溢出。如果没有设立这
  // 个标志位,validate_mm() 不会做任何事。
  validate_mm(mm);
}

void insert_vm_struct(struct mm_struct * mm, struct vm_area_struct * vma)
{
  struct vm_area_struct * __vma, * prev;
  rb_node_t ** rb_link, * rb_parent;

  __vma = find_vma_prepare(mm, vma->vm_start, &prev, &rb_link, &rb_parent);
  if (__vma && __vma->vm_start < vma->vm_end)
    BUG();
  vma_link(mm, vma, prev, rb_link, rb_parent);
  validate_mm(mm);
}
(b)find_vma_prepare

    这个函数负责在给定地址找到一个合适的位置来插入 VMA 。它通过实际的返回和函数参数返回一系列的信息。待插入 VMA 前面的节点也被返回。pprev 是前一个节点,这里需要它是因为链表是个单链表。rb_linkrb_parent 分别是父节点和叶节点,VMA 插入两者之间。

// mm/mmap.c
// 函数的参数在前面已经说明。
static struct vm_area_struct * find_vma_prepare(struct mm_struct * mm, unsigned long addr,
            struct vm_area_struct ** pprev,
            rb_node_t *** rb_link, rb_node_t ** rb_parent)
{
  struct vm_area_struct * vma;
  rb_node_t ** __rb_link, * __rb_parent, * rb_prev;

  // 初始化查找。
  __rb_link = &mm->mm_rb.rb_node;
  rb_prev = __rb_parent = NULL;
  vma = NULL;

  while (*__rb_link) {
    struct vm_area_struct *vma_tmp;

    __rb_parent = *__rb_link;
    vma_tmp = rb_entry(__rb_parent, struct vm_area_struct, vm_rb);

  // 这是与在 find_vma() 中相似的一棵遍历树。惟一的实际区别在于最后一个被
  // 访问的节点由 __rb_link 和 __rb_parent 变量记录。
    if (vma_tmp->vm_end > addr) {
      vma = vma_tmp;
      if (vma_tmp->vm_start <= addr)
        return vma;
      __rb_link = &__rb_parent->rb_left;
    } else {
      rb_prev = __rb_parent;
      __rb_link = &__rb_parent->rb_right;
    }
  }

  *pprev = NULL;
  // 通过红黑树获取后继 VMA。
  if (rb_prev)
    *pprev = rb_entry(rb_prev, struct vm_area_struct, vm_rb);
  *rb_link = __rb_link;
  *rb_parent = __rb_parent;
  // 返回前继 VMA。
  return vma;
}
(c)vma_link

    这是一个把 VMA 链接到相应链表中的高层函数。它负责获取所需的锁来保证插入的安全性。

// mm/mmap.c
// mm 是 VMA 要插入的地址空间。prev 是 VMA 线性链表中的后接 VMA 。rb_link 和 rb_parent 
// 是需要完成 rb 插入的节点。
static inline void vma_link(struct mm_struct * mm, struct vm_area_struct * vma, 
    struct vm_area_struct * prev, rb_node_t ** rb_link, rb_node_t * rb_parent)
{
  // 这个函数获取自旋锁来保护表示内存映射文件的 address_space。
  lock_vma_mappings(vma);
  // 获取保护整个mm_struct的页表锁。
  spin_lock(&mm->page_table_lock);
  // 插入 VMA。
  __vma_link(mm, vma, prev, rb_link, rb_parent);
  // 释放保护mm_struct的锁。
  spin_unlock(&mm->page_table_lock);
  // 解锁文件的 address_space。
  unlock_vma_mappings(vma);

  // 增加这个mm中的映射数量。
  mm->map_count++;
  // 如果设置了 DEBUG_MM_RB 标志位,将检查RB树和链表以保证它们有效。
  validate_mm(mm);
}
(d)__vma_link

    这里仅是调用 3 个负责把 VMA 链接到 3 个链表中的辅助函数,这些链表把 VMA 链到一起。

// mm/mmap.c
static void __vma_link(struct mm_struct * mm, struct vm_area_struct * vma,  
      struct vm_area_struct * prev, rb_node_t ** rb_link, rb_node_t * rb_parent)
{
  // 通过vm_next字段将VMA链接到该mm中VMA的一个线性链表中。
  __vma_link_list(mm, vma, prev, rb_parent);
  // 把VMA链接到mm中VMA的红黑树中,红黑树的根节点在vm_rb字段中。
  __vma_link_rb(mm, vma, rb_link, rb_parent);
  // 链接VMA到共享映射VMA链接中。通过这个函数以及vm_next_share和vm_pprev_share字段,
  // 内存映射的文件跨越多个mm而链接到一起
  __vma_link_file(vma);
}
(e)__vma_link_list
// mm/mmap.c
static inline void __vma_link_list(struct mm_struct * mm, struct vm_area_struct * vma, 
    struct vm_area_struct * prev, rb_node_t * rb_parent)
{
  // 如果prev为NULL,就仅把VMA插入到链表中。
  if (prev) {
    vma->vm_next = prev->vm_next;
    prev->vm_next = vma;
  } else {
  // 如果不为NULL,则这里是第1次映射,而且链表的第1个元素存放在 mm_struct 中。
    mm->mmap = vma;
  // VMA存放为一个父节点。   
    if (rb_parent)
      vma->vm_next = rb_entry(rb_parent, struct vm_area_struct, vm_rb);
    else
      vma->vm_next = NULL;
  }
}
(f)__vma_link_rb

    这个函数的主要工作存放于 中,本书将不会详细讨论。

// mm/mmap.c
static inline void __vma_link_rb(struct mm_struct * mm, struct vm_area_struct * vma,
         rb_node_t ** rb_link, rb_node_t * rb_parent)
{
  rb_link_node(&vma->vm_rb, rb_parent, rb_link);
  rb_insert_color(&vma->vm_rb, &mm->mm_rb);
}
(g)__vma_link_file

    这个函数将 VMA 链接到一个共享文件映射的链表中。

static inline void __vma_link_file(struct vm_area_struct * vma)
{
  struct file * file;

  file = vma->vm_file;
  // 检查VMA是否有一个共享文件映射。如果没有,则函数不再做任何事。
  if (file) {
  // 从VMA中摘取与映射有关的信息。
    struct inode * inode = file->f_dentry->d_inode;
    struct address_space *mapping = inode->i_mapping;
    struct vm_area_struct **head;

  // 如果即使在具有写权限时该映射也不可写,将i_writecount字段减1。该字段
  // 的负值表示文件是内存映射的,可能不可写。所以试着以写方式打开文件将失败。
    if (vma->vm_flags & VM_DENYWRITE)
      atomic_dec(&inode->i_writecount);
  // 检查以保证这是一个共享映射。
    head = &mapping->i_mmap;
    if (vma->vm_flags & VM_SHARED)
      head = &mapping->i_mmap_shared;
      
    /* insert vma into inode's share list */
  // 将VMA插入到一个共享映射链表中。
    if((vma->vm_next_share = *head) != NULL)
      (*head)->vm_pprev_share = &vma->vm_next_share;
    *head = vma;
    vma->vm_pprev_share = head;
  }
}

(3)合并相邻区域

(a)vma_merge

    这个函数检查由 prev 指向的区域可能向前扩展到从 addrend 之间的区域,而不是分配一个新的 VMA 。如果无法向前扩展,就检查 VMA 头,看它是否可以向后扩展。

// mm/mmap.c
// 参数如下:
//  mm 是 VMA 所属的 mm。
//  prev 是我们所感兴趣地址前的 VMA。
//  rb_parent 是由 find_vma_prepare() 返回的父 RB 节点。
//  addr 是待合并区域的起始地址。
//  end 是待合并区域的结束地址。
//  vm_flags 是待合并区域的权限标志位。
static int vma_merge(struct mm_struct * mm, struct vm_area_struct * prev,
         rb_node_t * rb_parent, unsigned long addr, unsigned long end, 
         unsigned long vm_flags)
{
  // 这是mm的锁。
  spinlock_t * lock = &mm->page_table_lock;
  // 如果没有传递prev,则意味着正测试且待合并的VMA位于addr到end区域
  // 之前。VMA的那个表项从rb_parent前部摘取。
  if (!prev) {
    prev = rb_entry(rb_parent, struct vm_area_struct, vm_rb);
    goto merge_next;
  }
// 检查prev指向的区域是否可以扩展到覆盖当前区域。  
  // 函数can_vma_merge()检查那些带有vm_flag标志位的权限以及那些没有文件映射
  // VMA(例如,它是匿名的)的prev的权限。如果为真,prev的区域可以扩展。
  if (prev->vm_end == addr && can_vma_merge(prev, vm_flags)) {
    struct vm_area_struct * next;
  // 锁定mm。
    spin_lock(lock);
  // 把VMA区域的末端(vm_end)扩展到新的映射末端(end)。
    prev->vm_end = end;
  // next是现在新扩展后的VMA前面的VMA。 
    next = prev->vm_next;
  // 检查扩展后的区域是否可以与前面的VMA合并。 
    if (next && prev->vm_end == next->vm_start && can_vma_merge(next, vm_flags)) {
  // 如果可以,这里持续扩展区域以覆盖下一个VMA。  
      prev->vm_end = next->vm_end;
  // 由于已经合并了一个VMA,有一个区域就变成了无效的,可以链接出去。    
      __vma_unlink(mm, next, prev);
  // 没有进一步调整mm结构,所以释放锁。     
      spin_unlock(lock);
  // 少了一个映射的区域,map_count减1。
      mm->map_count--;
  // 删除描述合并了的VMA的结构。    
      kmem_cache_free(vm_area_cachep, next);
  // 返回成功。    
      return 1;
    }
    spin_unlock(lock);
    return 1;
  }

  // 如果到了这一行,则意味着prev指向的区域不能向前扩展,所以检查前面的区域是
  // 否可以向后扩展。
  prev = prev->vm_next;
  if (prev) {
 merge_next:
    if (!can_vma_merge(prev, vm_flags))
      return 0;
  // 除了不是用调整的vm_end来覆盖end, 还可以使用vm_start扩展来覆盖addr,
  // 这里与前面一块的思想相同。
    if (end == prev->vm_start) {
      spin_lock(lock);
      prev->vm_start = addr;
      spin_unlock(lock);
      return 1;
    }
  }

  return 0;
}
(b)can_vma_merge

    这个小函数检查给定 VMA 的权限是否符合 vm_flags 的权限。

// include/linux/mm.h
static inline int can_vma_merge(struct vm_area_struct * vma, unsigned long vm_flags)
{
  // 一目了然。如果没有文件或设备映射(如是匿名的)以及两块区域的VMA标志位匹
  // 配,则返回true。
  if (!vma->vm_file && vma->vm_flags == vm_flags)
    return 1;
  else
    return 0;
}

(4)重映射并移动一个内存区域

(a)sys_mremap

    这个函数的调用图如图 4.6 所示。这是一个重映射内存区域的系统服务调用。

mremap()

// mm/mremap.c
// 参数与在 mremap() 帮助页中描述的一样。
// old_address(addr):旧地址已经被 page aligned 页对齐
// old_size(old_len):VMA 虚拟内存块的大小
// new_size(new_len):mremap 操作后需要的 VMA 大小
// flags:默认情况下,没有足够空间expand,mremap()失败
// new_address(new_addr):可选择映射地址的位置,也可以NULL,系统返回映射地址。
asmlinkage unsigned long sys_mremap(unsigned long addr,
  unsigned long old_len, unsigned long new_len,
  unsigned long flags, unsigned long new_addr)
{
  unsigned long ret;
  // 获得mm信号量。
  down_write(&current->mm->mmap_sem);
  // do_mremap() (见D. 2. 4. 2)是重映射一块区域的高层函数。
  ret = do_mremap(addr, old_len, new_len, flags, new_addr);
  // 释放mm信号量。
  up_write(&current->mm->mmap_sem);
  // 返回重映射状态。
  return ret;
}
(b)do_mremap

    这个函数完成重映射,修改尺寸以及移动一块内存区域的实际工作。它相当长,但也可以分成几个独立的部分,在这里分别处理。粗略地说,主要的任务如下:

  • 检查使用标志位以及页面排列长度。
  • 处理 MAP_FIXED 的设置和移到新地方的区域位置。
  • 如果是收缩一个区域,则允许它无条件发生。
  • 如果增大区域或者移动区域,提前进行一系列检查来保证允许移动以及移动的安全性。
  • 处理已经扩展了区域但是不能移动的情况。
  • 最后,处理区域需要改变大小和移动的情况。
// mm/mremap.c
/*
 * Expand (or shrink) an existing mapping, potentially moving it at the
 * same time (controlled by the MREMAP_MAYMOVE flag and available VM space)
 *
 * MREMAP_FIXED option added 5-Dec-1999 by Benjamin LaHaise
 * This option implies MREMAP_MAYMOVE.
 */
// 函数的参数如下:
//  addr 是旧的起始地址。
//  old_len 是旧的区域长度。
//  new_len 是新的区域长度。
//  flags 是传入的是一个可选标志位。如果设置了 MREMAP_MAYMOVE, 则意味
//        着如果当前空间没有足够的线性地址空间时,那么允许移动区域。如果设置了
//        MREMAP_FIXED,则意味着整个区域将移动到指定的new_addr并具有新长度。
//        从new_addr到new_addr + new_len的区域将通过do_munmap()来解除映射。
//  new_addr 是如果区域移动后新区域的地址。 
unsigned long do_mremap(unsigned long addr,
  unsigned long old_len, unsigned long new_len,
  unsigned long flags, unsigned long new_addr)
{
  struct vm_area_struct *vma;
  // 这里缺省返回-EINVAL报告无效的参数。
  unsigned long ret = -EINVAL;
  // 保证没有使用那两个允许使用的标志位之外的标志位。
  if (flags & ~(MREMAP_FIXED | MREMAP_MAYMOVE))
    goto out;
  // 传入的地址必须与页对齐。
  if (addr & ~PAGE_MASK)
    goto out;
  // 传入与页对齐的区域长度。
  old_len = PAGE_ALIGN(old_len);
  new_len = PAGE_ALIGN(new_len);

// 这一块处理区域地址固定并且必须全部移动的情况。它保证将移动的区域是安全的且明
// 确被解除映射的。
  /* new_addr is only valid if MREMAP_FIXED is specified */
  // MREMAP_FIXED是表明地址为固定的标志位。
  if (flags & MREMAP_FIXED) {
  // 指定的new_addr必须是页对齐的。
    if (new_addr & ~PAGE_MASK)
      goto out;
  // 如果指定了MREMAP_FIXED,也必须使用MAYMOVE标志位。
    if (!(flags & MREMAP_MAYMOVE))
      goto out;
  // 保证可变大小的区域不能超过 TASK_SIZE 。
    if (new_len > TASK_SIZE || new_addr > TASK_SIZE - new_len)
      goto out;

    /* Check if the location we're moving into overlaps the
     * old location at all, and fail if it does.
     */
  // 如注释,所使用的两个区域不能重叠。     
    if ((new_addr <= addr) && (new_addr+new_len) > addr)
      goto out;

    if ((addr <= new_addr) && (addr+old_len) > new_addr)
      goto out;
  // 对将要使用的区域解除映射。假定调用者确定这块区域没有被其他重要进程使用。
    do_munmap(current->mm, new_addr, new_len);
  }

  /*
   * Always allow a shrinking remap: that just unmaps
   * the unnecessary pages..
   */
  // 这里,可变大小区域的地址是返回值。 
  ret = addr;
  // 如果旧的长度超过新的长度,将缩小区域。
  if (old_len >= new_len) {
  // 取消那些没有使用区域的映射。
    do_munmap(current->mm, addr+new_len, old_len - new_len);
  // 如果不移动区域,或者由于MREMAP_FIXED没有使用或者新地址与旧地址
  // 不匹配,则跳到out,在那里返回地址。  
    if (!(flags & MREMAP_FIXED) || (new_addr == addr))
      goto out;
  }

// 这一块进行一系列的检查来保证增大或者移动区域是安全的。
  /*
   * Ok, we need to grow..  or relocate.
   */
  // 在这里,缺省的动作是返回-EFAULT,这将导致一个段错误,因为正使用的内存区
  // 域是无效的。 
  ret = -EFAULT;
  // 找到所请求地址的VMA。
  vma = find_vma(current->mm, addr);
  // 如果返回的VMA和这个地址不匹配,则返回一个无效地址作为异常。
  if (!vma || vma->vm_start > addr)
    goto out;
  /* We can't remap across vm area boundaries */
  // 如果传入的 old_len 超过了 VMA 的长度,则意味着使用者尝试重映射多个区域,而这是不允许的。
  if (old_len > vma->vm_end - addr)
    goto out;
  // 如果VMA已经被显式地标记为不可改变大小,这里将报异常。 
  if (vma->vm_flags & VM_DONTEXPAND) {
    if (new_len > old_len)
      goto out;
  }
  // 如果该VMA的页面必须在内存中上锁,这里重新计算将在内存中锁定的页面
  // 数量。如果这样的页面已经超过资源的极限集,这里将返回EAGAIN,向调用者表明这片区
  // 域已经锁定,并且无法改变大小。
  if (vma->vm_flags & VM_LOCKED) {
    unsigned long locked = current->mm->locked_vm << PAGE_SHIFT;
    locked += new_len - old_len;
    ret = -EAGAIN;
    if (locked > current->rlim[RLIMIT_MEMLOCK].rlim_cur)
      goto out;
  }
  // 这里返回的缺省值用于表明没有足够的内存。
  ret = -ENOMEM;
  // 保证用户不会超过使用它们所允许分配的内存。
  if ((current->mm->total_vm << PAGE_SHIFT) + (new_len - old_len)
      > current->rlim[RLIMIT_AS].rlim_cur)
    goto out;
  /* Private writable mapping? Check memory availability.. */
  // 保证在利用 vm_enough_memory() (见M. 1. 1小节) 重新划定大小后有足够的
  // 内存来满足请求。
  if ((vma->vm_flags & (VM_SHARED | VM_WRITE)) == VM_WRITE &&
      !(flags & MAP_NORESERVE)         &&
      !vm_enough_memory((new_len - old_len) >> PAGE_SHIFT))
    goto out;

// 这一块处理区域正拓展却无法移动的情况。
  /* old_len exactly to the end of the area..
   * And we're not relocating the area.
   */
  // 如果它是要重映射的满区域且…… 
  if (old_len == vma->vm_end - addr &&
  // 区域明确没有被移动且……
      !((flags & MREMAP_FIXED) && (addr != new_addr)) &&
  // 正扩展的区域无法移动,则……    
      (old_len != new_len || !(flags & MREMAP_MAYMOVE))) {
  // 设置TASK_SIZE可用的最大地址,在x86上为 3GB。    
    unsigned long max_addr = TASK_SIZE;
  // 如果有另外一个区域,这里设置下一块区域的最大地址。
    if (vma->vm_next)
      max_addr = vma->vm_next->vm_start;
    /* can we just expand the current mapping? */
// 只有新划分的区域与下一个VMA没有重叠时才允许扩展。 
    if (max_addr - addr >= new_len) {
  // 计算所需的额外页面数。  
      int pages = (new_len - old_len) >> PAGE_SHIFT;
  // 上锁mm自旋锁。
      spin_lock(&vma->vm_mm->page_table_lock);
  // 扩展 VMA。
      vma->vm_end = addr + new_len;
  // 释放mm自旋锁。
      spin_unlock(&vma->vm_mm->page_table_lock);
  // 更新mm统计。
      current->mm->total_vm += pages;
// 如果该区域的页面在内存中锁定,这里使它们保持原样。
      if (vma->vm_flags & VM_LOCKED) {
        current->mm->locked_vm += pages;
        make_pages_present(addr + old_len,
               addr + new_len);
      }
  // 返回重新划分大小的区域地址。
      ret = addr;
      goto out;
    }
  }

// 为了扩展区域,分配一个新区域,将旧的区域移到其中。
  /*
   * We weren't able to just expand or shrink the area,
   * we need to create a new one and move it..
   */
  // 缺省动作是返回信息,提示无内存可用。 
  ret = -ENOMEM;
  // 检查以保证允许移动区域。
  if (flags & MREMAP_MAYMOVE) {
  // 如果没有指定MREMAP_FIXED,这里意味着没有提供新地址,所以必须找到一个。
    if (!(flags & MREMAP_FIXED)) {
      unsigned long map_flags = 0;
  // 保留 MAP_SHARED 选项。
      if (vma->vm_flags & VM_SHARED)
        map_flags |= MAP_SHARED;
  // 找到一片足够大小的用于扩展且未映射的内存区域。
      new_addr = get_unmapped_area(vma->vm_file, 0, new_len, vma->vm_pgoff, map_flags);
  // 返回值是新区域的地址。
      ret = new_addr;
  // 对不是页对齐的返回地址,将需要get_unmapped_area。进行打散操作。这里
  // 将可能发生错误百出的设备驱动程序没有正确实现get_unmapped_area()的情况。
      if (new_addr & ~PAGE_MASK)
        goto out;
    }
  // 调用move_vma()来移动区域。
    ret = move_vma(vma, addr, old_len, new_len, new_addr);
  }
out:
  // 如果成功则返回地址,否则返回错误码。
  return ret;
}
(c)move_vma

    这个函数的调用图如图 4.7 所示。这个函数负责从一个 VMA 移动所有的页表项到另一片区域。如果需要的话,将为这块移动的区域分配一块新 VMA。就像前面的函数一样,它也是很长的,但我们可以把它分成如下不同的部分:

  • 函数的前面部分找到在待移动区域前面的 VMA ,以及待映射区域前面的 VMA
  • 处理新地址在两块现存 VMA 之间的情况。它决定了前面的区域是否可以向前扩展到前一个区域或者后面的区域可以向后扩展来覆盖一块新的映射区域。
  • 处理新地址是在链表中最后那个 VMA 的情况。它决定前面的区域是否可以向前扩展。
  • 如果一块区域不可以扩展,则从 slab 分配器中分配一块新的 VMA
  • 调用 move_page_tables() ,如果分配了一块新的 VMA 则填充新 VMA 内容,更新统计数量然后返回。
// mm/mremap.c
// 参数如下:
//  vma 是移动地址所属的VMA。
//  addr 是移动区域的首地址。
//  old_len 是待移动区域的旧长度。
//  new_len 是待移动区域的新长度。
//  new_addr 是重定位的新地址。
static inline unsigned long move_vma(struct vm_area_struct * vma,
  unsigned long addr, unsigned long old_len, unsigned long new_len,
  unsigned long new_addr)
{
  struct mm_struct * mm = vma->vm_mm;
  struct vm_area_struct * new_vma, * next, * prev;
  int allocated_vma;

  new_vma = NULL;
  // 找到将移动地址前面的 VMA, 它由 prev 指向,在新映射后的区域由 next 指向,返回 next。
  next = find_vma_prev(mm, new_addr, &prev);
// 在这一块,新地址在两个存在的VMA之间。检查前面的区域是否可以扩展来覆盖整个
// 映射,然后检查它是否也可以覆盖下一个VMA。如果它不能扩展,就检查下一个区域是否可
// 以向后扩展。
  if (next) {
  // 如果前面的区域与待映射的地址邻接,并且可能合并,则进入这一块,试着扩展区域。
    if (prev && prev->vm_end == new_addr &&
        can_vma_merge(prev, vma->vm_flags) && !vma->vm_file && 
                !(vma->vm_flags & VM_SHARED)) {
  // 上锁mm。
      spin_lock(&mm->page_table_lock);
  // 扩展前面的区域来覆盖整个新地址。
      prev->vm_end = new_addr + new_len;
  // 解锁mm。
      spin_unlock(&mm->page_table_lock);
  // 新VMA现在是刚才扩展区域前面的VMA。
      new_vma = prev;
  // 保证VMA链表是完整的。有时一块逻辑遭到损坏的设备驱动将可能导致这种情况发生。
      if (next != prev->vm_next)
        BUG();
  // 检查这片区域是否可以向前扩展来包含下一个区域。
      if (prev->vm_end == next->vm_start && can_vma_merge(next, prev->vm_flags)) {
  // 如果可以,则锁定mm。
        spin_lock(&mm->page_table_lock);
  // 进一步扩展VMA来覆盖下一个VMA。
        prev->vm_end = next->vm_end;
  // 有一个额外的VMA,所以从链表中移除它。
        __vma_unlink(mm, next, prev);
  // 解锁mm。        
        spin_unlock(&mm->page_table_lock);
  // 已经减少了一个映射,所以更新map_count。
        mm->map_count--;
  // 释放由内存映射使用的内存。        
        kmem_cache_free(vm_area_cachep, next);
      }
    } 
// 如果prev区域不能向前扩展,检查next指向的区域是否可以向后扩展来覆盖新的映射
    else if (next->vm_start == new_addr + new_len &&
         can_vma_merge(next, vma->vm_flags) && !vma->vm_file && 
              !(vma->vm_flags & VM_SHARED)) {
  // 如果可以,则锁定 mm
      spin_lock(&mm->page_table_lock);
  // 向后扩展映射。      
      next->vm_start = new_addr;
  // 解锁 mm。
      spin_unlock(&mm->page_table_lock);
  // 现在表示新映射VMA的是next.    
      new_vma = next;
    }
  } else {
// 这一块用于新映射区域是最后一块VMA(next为NULL)的情况,检查前面的区域是否可以扩展。
  // 获取前面映射的区域。
    prev = find_vma(mm, new_addr-1);
  // 检查区域是否可以被映射。
    if (prev && prev->vm_end == new_addr &&
        can_vma_merge(prev, vma->vm_flags) && !vma->vm_file && 
            !(vma->vm_flags & VM_SHARED)) {
  // 上锁mm。
      spin_lock(&mm->page_table_lock);
  // 扩展前面的区域来覆盖新的映射。
      prev->vm_end = new_addr + new_len;
  // 解锁mm。
      spin_unlock(&mm->page_table_lock);
  // 现在表示新映射VMA的是prev。
      new_vma = prev;
    }
  }

// 设置标志位以表明没有分配新VMA。
  allocated_vma = 0;
  // 如果一个VMA还没有扩展来覆盖新的映射区,则 ...
  if (!new_vma) {
  // 从一个slab分配器来分配新的VMA。
    new_vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
  // 如果不能分配,跳转到out,在那里返回失败。
    if (!new_vma)
      goto out;
  // 设置标志位以表明已经分配新VMA。
    allocated_vma = 1;
  }

  // move_page_tables() (见D. 2. 4. 6)负责复制所有的页表项,返回0表示成功。
  if (!move_page_tables(current->mm, new_addr, addr, old_len)) {
    unsigned long vm_locked = vma->vm_flags & VM_LOCKED;

// 如果分配了一个新的VMA,这里填充所有相关的内容细节,包括文件/设备表
// 项,并调用insert_vm_struct() (见D.2.2.1)来将其插入到各种VMA链表中。
    if (allocated_vma) {
      *new_vma = *vma;
      new_vma->vm_start = new_addr;
      new_vma->vm_end = new_addr+new_len;
      new_vma->vm_pgoff += (addr-vma->vm_start) >> PAGE_SHIFT;
      new_vma->vm_raend = 0;
      if (new_vma->vm_file)
        get_file(new_vma->vm_file);
      if (new_vma->vm_ops && new_vma->vm_ops->open)
        new_vma->vm_ops->open(new_vma);
      insert_vm_struct(current->mm, new_vma);
    }
  // 取消旧区域的映射,因为不再需要它们了。
    do_munmap(current->mm, addr, old_len);
  // 更新该进程的total_vm 大小。旧的区域大小无关紧要,因为这里是在do_munmap()
  // 中进行的。
    current->mm->total_vm += new_len >> PAGE_SHIFT;
  // 如果VMA有VM_LOCKED标志,在一片区域中的所有页面都利用
  // make_pages_present()来设置为当前。
    if (vm_locked) {
      current->mm->locked_vm += new_len >> PAGE_SHIFT;
      if (new_len > old_len)
        make_pages_present(new_addr + old_len,
               new_addr + new_len);
    }
  // 返回新区域的地址。
    return new_addr;
  }
  // 这是一个错误路径,如果分配了一块VMA,则删除它。
  if (allocated_vma)
    kmem_cache_free(vm_area_cachep, new_vma);
 out:
  // 返回内存溢出错误。
  return -ENOMEM;
}
(d)make_pages_present

    这个函数使所有在 addrend 之间的页面变为当前。这里假定这两个地址处于同一个 VMA 中。

// mm/memory.c
int make_pages_present(unsigned long addr, unsigned long end)
{
  int ret, len, write;
  struct vm_area_struct * vma;
  // 利用find_vma() 找到包含首地址的VMA。
  vma = find_vma(current->mm, addr);
  // 记录写访问在write中是否允许。
  write = (vma->vm_flags & VM_WRITE) != 0;
  // 如果起始地址在结束地址之后,则产生BUG()。
  if (addr >= end)
    BUG();
  // 如果范围超过一个VMA,则这是一个bug。    
  if (end > vma->vm_end)
    BUG();
  // 计算陷入的区域长度。 
  len = (end+PAGE_SIZE-1)/PAGE_SIZE-addr/PAGE_SIZE;
  // 在请求区域所有页面中调用get_user_pages()陷入。这里返回陷入的页面数。
  ret = get_user_pages(current, current->mm, addr,
      len, write, 0, NULL, NULL);
  return ret == len ? 0 : -1;
}
(e)get_user_pages

    这个函数用于陷入用户页面,并可能用于属于其他进程的页面,例如,在 ptrace() 要用到。

// mm/memory.c
/*
 * Please read Documentation/cachetlb.txt before using this function,
 * accessing foreign memory spaces can cause cache coherency problems.
 *
 * Accessing a VM_IO area is even more dangerous, therefore the function
 * fails if pages is != NULL and a VM_IO area is found.
 */
// 参数如下:
//  tsk 是陷入页面的进程。
//  mm 是管理待陷入地址空间的mm_struct。
//  start 是开始陷入的位置。
//  len 是以页计数的陷入区域长度。
//  write 是表明是否可以写待陷入页面。
//  force 是表明即使区域有VM_MAYREAD和VM_MAYWRITE标志,页面也应该陷入。
//  pages 是一个结构页面数组,它可能为NULL。如果指定了这个数组,则它可以填
//        充待陷入的struct pages。
//  vmas 是与pages数组类似,如果指定了这个数组,它可以填充受到陷入影响的 VMA。 
int get_user_pages(struct task_struct *tsk, struct mm_struct *mm, unsigned long start,
    int len, int write, int force, struct page **pages, struct vm_area_struct **vmas)
{
  int i;
  unsigned int flags;

  /*
   * Require read or write permissions.
   * If 'force' is set, we only require the "MAY" flags.
   */
  // 如果write设置为1,则设置所需的标志为VM_WRITE 和VM_MAYWRITE标志,
  // 否则利用相应的读标志。 
  flags = write ? (VM_WRITE | VM_MAYWRITE) : (VM_READ | VM_MAYREAD);
  // 如果指定了 force 值,这里仅需要MAY标志。
  flags &= force ? (VM_MAYREAD | VM_MAYWRITE) : (VM_READ | VM_WRITE);
  i = 0;

// 外层循环将遍历受陷入影响的VMA。
  do {
    struct vm_area_struct * vma;
  // 找到start当前值影响的VMA。这个参数在第PAGE_SIZE步加1。
    vma = find_extend_vma(mm, start);
  // 如果这个地址上的VMA不存在,或者调用者已经为一块I/O映射的区域指定了一
  // 个struct page (因此不是由后援内存支持的),或者VMA没有所需的标志位,则返
  // 回-EFAULT。
    if ( !vma || (pages && vma->vm_flags & VM_IO) || !(flags & vma->vm_flags) )
      return i ? : -EFAULT;
  // 上锁页表自旋锁。
    spin_lock(&mm->page_table_lock);
    do {
      struct page *map;
// follow_page (见C. 2. 1小节)遍历页表并返回表示映射在start处页面帧的
// struct page。这里的循环仅在没有PTE时才进入,并且将一直循环直到找到一个拥有页表自
// 旋锁的PTE。
      while (!(map = follow_page(mm, start, write))) {
  // 解锁页表自旋锁,因为handle_mm_fault()将要睡眠。
        spin_unlock(&mm->page_table_lock);
  // 如果没有页面,这里调用handle_mm_fault() (见D. 5. 3. 1)来陷入。
        switch (handle_mm_fault(mm, vma, start, write)) {
  // 更新task_struct统计并且表明是否发生了主/次异常。
        case 1:
          tsk->min_flt++;
          break;
        case 2:
          tsk->maj_flt++;
          break;
  // 如果异常地址无效,在这里返回-EFAULT。
        case 0:
          if (i) return i;
          return -EFAULT;
        default:
          if (i) return i;
  // 如果系统内存不足,返回-ENOMEM。          
          return -ENOMEM;
        }
  // 重新上锁页表。这个循环将检查一遍以保证页面都实际存在。
        spin_lock(&mm->page_table_lock);
      }
// 如果调用者请求,则获取受到函数影响的struct pages的pages数组。每个结
// 构都通过page_cache_get()来引用这个数组。
      if (pages) {
        pages[i] = get_page_map(map);
        /* FIXME: call the correct function,
         * depending on the type of the found page
         */
        if (!pages[i])
          goto bad_page;
        page_cache_get(pages[i]);
      }
  // 相似地,这里记录受影响的VMA。
      if (vmas)
        vmas[i] = vma;
  // 把 i 加1, i 是在请求区域的页面数量。
      i++;
  // 在页面划分的那一步把 start 加1。
      start += PAGE_SIZE;
  // 将必须陷入的页面数减 1
      len--;
  // 不断遍历VMA直到请求页被陷入。
    } while(len && start < vma->vm_end);
  // 释放页表自旋锁。
    spin_unlock(&mm->page_table_lock);
  } while(len);
out:
  // 返回在区域中存在的页面数。
  return i;

  /*
   * We found an invalid page in the VMA.  Release all we have
   * so far and fail.
   */
// 只有找到了一个表明不存在页面帧的struct page才可能到这里。
bad_page:
  spin_unlock(&mm->page_table_lock);
  // 如果找到了这样一个struct page,则释放存放在pages数组中的所有页面。
  while (i--)
    page_cache_release(pages[i]);
  // 返回-EFAULT。
  i = -EFAULT;
  goto out;
}
(f)move_page_tables

    这个函数的调用图如图 4.8 所示。这个函数负责从 old_addr 指向的区域把所有的页表项复制到 new_addr 所指向的区域。

它只是每次复制一个表项。在这个函数完成时,返回所有在旧区域的表项。还没有更有效的方式来完成这个操作,但这里却很容易从错误中恢复。

// mm/mremap.c
// 参数分别为进程的mm、新地址、旧地址以及要移动表项的区域长度。
static int move_page_tables(struct mm_struct * mm,
  unsigned long new_addr, unsigned long old_addr, unsigned long len)
{
  unsigned long offset = len;
  // flush_cache_range()将刷新在这个区域的所有CPU高速缓存。必须首先调用这个函
  // 数,因为在某些体系结构中,比如著名的Sparc中,在刷新TLB前需要存在一个虚拟到物理地
  // 址的映射。
  flush_cache_range(mm, old_addr, old_addr + len);

  /*
   * This is not the clever way to do this, but we're taking the
   * easy way out on the assumption that most remappings will be
   * only a few pages.. This also makes error recovery easier.
   */
// 遍历该区域中的每个页面,并调用move_one_pte() (见D. 2. 4. 7)来移除PTE。
// 这里将转换大量遍历的页表并完成得更好,但这是个很少使用的操作。
  while (offset) {
    offset -= PAGE_SIZE;
    if (move_one_page(mm, old_addr + offset, new_addr + offset))
      goto oops_we_failed;
  }
  // 刷新旧区域的TLB。
  flush_tlb_range(mm, old_addr, old_addr + len);
  // 返回成功。
  return 0;

  /*
   * Ok, the move failed because we didn't have enough pages for
   * the new page table tree. This is unlikely, but we have to
   * take the possibility into account. In that case we just move
   * all the pages back (this will work, because we still have
   * the old page tables)
   */
oops_we_failed:
// 这一块将所有的PTE移回去。在这里并不需要flush_tlb_range(),因为这块
// 区域还没有使用过,所以TLB表项应该还不存在。
  flush_cache_range(mm, new_addr, new_addr + len);
  while ((offset += PAGE_SIZE) < len)
    move_one_page(mm, new_addr + offset, old_addr + offset);
  // 销毁那些多分配的页面。
  zap_page_range(mm, new_addr, len);
  // 返回失败。
  return -1;
}
(g)move_one_page

    这个函数负责在调用 get_one_pte() 找到正确的 PTE 和调用 copy_one_pte() 来复制 PTE 之前获取一个自旋锁。

// mm/mremap.c
static int move_one_page(struct mm_struct *mm, unsigned long old_addr, 
            unsigned long new_addr)
{
  int error = 0;
  pte_t * src;
  // 获取mm锁。
  spin_lock(&mm->page_table_lock);
  // 调用get_one_pte()(见D.2.4.8),它遍历页表来获得正确的PTE。
  src = get_one_pte(mm, old_addr);
  // 如果存在PTE,则在这里为复制目的地分配一个PTE,并调用
  // copy_one_pte() (见D. 2.4・10)复制到PTE。
  if (src)
    error = copy_one_pte(mm, src, alloc_one_pte(mm, new_addr));
  // 释放mm锁。
  spin_unlock(&mm->page_table_lock);
  // 返回copy_one_pte()的返回值。如果在85行的alloc_one_pte() (见D. 2. 4. 9)
  // 失败就返回错误。
  return error;
}
(h)get_one_pte

    这是一个非常简单的遍历页表函数。

// mm/mremap.c
static inline pte_t *get_one_pte(struct mm_struct *mm, unsigned long addr)
{
  pgd_t * pgd;
  pmd_t * pmd;
  pte_t * pte = NULL;
  // 获取该地址的PGD。
  pgd = pgd_offset(mm, addr);
  // 如果不存在PGD,则返回NULL因为也不会存在PTE。
  if (pgd_none(*pgd))
    goto end;
  // 如果PGD被损坏,这里标记在该区域发生了错误,并清除PGD的内容,然后返回 NULL。
  if (pgd_bad(*pgd)) {
    pgd_ERROR(*pgd);
    pgd_clear(pgd);
    goto end;
  }
  // 与PGD的处理一样,返回正确的PMD。
  pmd = pmd_offset(pgd, addr);
  if (pmd_none(*pmd))
    goto end;
  if (pmd_bad(*pmd)) {
    pmd_ERROR(*pmd);
    pmd_clear(pmd);
    goto end;
  }
  // 获取PTE,如果PTE存在,则可以返回。
  pte = pte_offset(pmd, addr);
  if (pte_none(*pte))
    pte = NULL;
end:
  return pte;
}
(i)alloc_one_pte

    这个函数在需要的情况下在区域中分配一个 PTE

// mm/mremap.c
static inline pte_t *alloc_one_pte(struct mm_struct *mm, unsigned long addr)
{
  pmd_t * pmd;
  pte_t * pte = NULL;
  // 如果不存在PMD,这里分配一个。
  pmd = pmd_alloc(mm, pgd_offset(mm, addr), addr);
  // 如果存在PMD,这里分配一个PTE项。然后检查在函数copy_one_pte()中的操
  // 作是否成功。
  if (pmd)
    pte = pte_alloc(mm, pmd, addr);
  return pte;
}
 
(j)copy_one_pte

    这个函数将一个 PTE 的内容复制到另外一个 PTE

// mm/mremap.c
static inline int copy_one_pte(struct mm_struct *mm, pte_t * src, pte_t * dst)
{
  int error = 0;
  pte_t pte;
  // 如果源PTE不存在,这里仅返回0,提示复制成功。
  if (!pte_none(*src)) {
  // 获取PTE并将其从旧地址移除。
    pte = ptep_get_and_clear(src);
  // 如果dst不存在,则意味着对alloc_one_pte()的调用失败,复制也将失败,必须中止操作。
    if (!dst) {
      /* No dest?  We must put it back. */
      dst = src;
      error++;
    }
  // 把PTE移到新地址。
    set_pte(dst, pte);
  }
  // 如果出错则返回错误信息。
  return error;
}

深入理解Linux虚拟内存管理(七)(中):https://developer.aliyun.com/article/1597882

目录
相关文章
|
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天前
|
Linux
深入理解Linux虚拟内存管理(七)(中)
深入理解Linux虚拟内存管理(七)
21 2
|
18天前
|
机器学习/深度学习 消息中间件 Unix
深入理解Linux虚拟内存管理(九)(下)
深入理解Linux虚拟内存管理(九)
15 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