深入理解Linux虚拟内存管理(一)3

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

深入理解Linux虚拟内存管理(一)2:https://developer.aliyun.com/article/1597682

4.4 内存区域

linux内核编程之二:vm_area_struct / vm_struct 结构体

   进程的地址空间很少能全部用满,一般都只是用到了其中一些分离的区域。区域由 vm_area_struct 来表示。区域之间不会交叉,它们各自代表一个有着相同属性和用途的地址集

合。如一个被装载到进程堆空间的只读共享库就包含在这样的一个区域。一个进程所有已被映射的区域都可以在 /proc/PID/maps 里看到,其中 PID 是该进程的进程号。


   一个区域可能有许多与它相关的结构,如图 4.2 所示。在图的顶端,有一个 vm_area_struct 结构,它足以用来表示一个匿名区域。


   如果一个文件被映射到内存,则可以通过 vm_file 字段得到 struct file。 vm_file 字段有一个指针指向 struct inode,索引节点用于找到 struct address_space,而 struct address_space 中包含与文件有关的所有信息,包括一系列指向与文件系统相关操作函数的指针,如读磁盘页面和写磁盘页面的操作。


   vm_area_struct 结构在  中声明如下:

/*
 * This struct defines a memory VMM memory area. There is one of these
 * per VM-area/task.  A VM area is any part of the process virtual memory
 * space that has a special rule for the page-fault handlers (ie a shared
 * library, the executable area etc).
 */
struct vm_area_struct {
  struct mm_struct * vm_mm; /* The address space we belong to. */
  unsigned long vm_start;   /* Our start address within vm_mm. */
  unsigned long vm_end;   /* The first byte after our end address
             within vm_mm. */

  /* linked list of VM areas per task, sorted by address */
  struct vm_area_struct *vm_next;

  pgprot_t vm_page_prot;    /* Access permissions of this VMA. */
  unsigned long vm_flags;   /* Flags, listed below. */

  rb_node_t vm_rb;

  /*
   * For areas with an address space and backing store,
   * one of the address_space->i_mmap{,shared} lists,
   * for shm areas, the list of attaches, otherwise unused.
   */
  struct vm_area_struct *vm_next_share;
  struct vm_area_struct **vm_pprev_share;

  /* Function pointers to deal with this struct. */
  struct vm_operations_struct * vm_ops;

  /* Information about our backing store: */
  unsigned long vm_pgoff;   /* Offset (within vm_file) in PAGE_SIZE
             units, *not* PAGE_CACHE_SIZE */
  struct file * vm_file;    /* File we map to (can be NULL). */
  unsigned long vm_raend;   /* XXX: put full readahead info here. */
  void * vm_private_data;   /* was vm_pte (shared mem) */
};

  下面是对该结构中字段的简要解释。


vm_mm:这个 VMA 所属的 mm_struct。


vm_start:这个区域的起始地址。


vm_end:这个区间的结束地址。


vm_next:在一个地址空间中的所有 VMA 都按地址空间次序通过该字段简单地链接在一起。可以很有趣地发现该 VMA 链表在内核中所使用的单个链表中是非常少见的。


vm_page_prot:这个 VMA 对应的每个 PTE 里的保护标志位。其不同的位在表 3.1 中

有描述。


vm_flags:这个 VMA 的保护标志位和属性标志位。它们都定义在  中,

描述见表 4.3。

vm_rb:同链表一样,所有的 VMA 都存储在一个红黑树上以加快查找速度。这对于在发生缺页时能快速找到正确的区域非常重要,尤其是大量的映射区域。


vm_next_share:这个指针把由文件映射而来的 VMA 共享区域链接在一起(如共享库)。


vm_pprev_share:vm_next_share 的辅助指针。


vm_ops:包含指向与磁盘作同步操作时所需函数的指针。vm_ops 字段包含有指向 open(),close() 和 nopage() 的函数指针。


vm_pgoff:在已被映射文件里对齐页面的偏移。


vm_file:指向被映射的文件的指针。


vm_raend:预读窗口的结束地址。在发生错误时,一些额外的页面将被收回,这个值决定。

了这些额外页面的个数。


vm_private_data:一些设备驱动私有数据的存储,与内存管理器无关。


   所有的区域按地址排序由指针 vm_next 链接成一个链表。寻找一个空闲的区间时,只需要遍历这个链表即可。但若在发生缺页中断时搜索 VMA 以找到一个指定区域,则是一个频繁操作。正因为如此,才有了红黑树,因为它平均只需要 O(logN) 的遍历时间。红黑树通过排序使得左结点的地址小于当前结点的地址,而当前结点的地址又小于右结点的地址。

4.4.1 内存区域的操作

    VMA 提供三个操作函数,分别是 open()close()nopage()VMA 通过类型

vm_operations_structvma->vm_ops 提供上述几个操作函数 。该结构包含三个函数指针,它在 中的声明如下所示:

/*
 * These are the virtual MM functions - opening of an area, closing and
 * unmapping it (needed to keep files on disk up-to-date etc), pointer
 * to the functions called when a no-page or a wp-page exception occurs. 
 */
struct vm_operations_struct {
  void (*open)(struct vm_area_struct * area);
  void (*close)(struct vm_area_struct * area);
  struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, 
              int unused);
};


   每当创建或者删除一个区域时系统会调用 open() 和 close() 函数。只有一小部分设备使用这些操作函数,如文件系统和 system v 的共享区域。在打开或关闭区域时,需要执行额外的操作。例如 ,system V 中 open() 回调函数会递增使用共享段的 VMA 的数量。


   我们关心的主要操作函数是 nopage() 回调函数,在发生缺页中断时 do_no_page() 会使用该回调函数。该回调函数负责定位该页面在高速缓存中的位置,或者分配一个新页面并填充请求的数据,然后返回该页面。


   大多数被映射的文件会用到名为 generic_file_vm_ops 的 vm_operations_struct() 。它只注册一个函数名为 filemap_nopage() 的 nopage() 函数。该 nopage() 函数或者定位该页面在页面高速缓存中的位置,或者从磁盘上读取数据。该结构在 mm/filemap.c 中声明如下:

static struct vm_operations_struct generic_file_vm_ops = {
  nopage:   filemap_nopage,
};

4.4.2 有后援文件/设备的内存区域

    如表 4.2 所列,在有后援文件的区域中,vm_file 引出了相关的 address_space

address_space 结构包含一些与文件系统相关的信息,如必须写回到磁盘的脏页面数目 。该结构在 中声明如下:

struct address_space {
  struct list_head  clean_pages;  /* list of clean pages */
  struct list_head  dirty_pages;  /* list of dirty pages */
  struct list_head  locked_pages; /* list of locked pages */
  unsigned long   nrpages;  /* number of total pages */
  struct address_space_operations *a_ops; /* methods */
  struct inode    *host;    /* owner: inode, block_device */
  struct vm_area_struct *i_mmap;  /* list of private mappings */
  struct vm_area_struct *i_mmap_shared; /* list of shared mappings */
  spinlock_t    i_shared_lock;  /* and spinlock protecting it */
  int     gfp_mask; /* how to allocate the pages */
};

各字段简要描述如下。


clean_pages:不需要后援存储器同步的干净页面链表。

dirty_pages:需要后援存储器同步的脏页面链表。

locked_pages:在内存中被锁住的页面链表。

nrpages:在地址空间中正被使用且常驻内存的页面数。

a_ops:是一个操纵文件系统的函数结构。每一个文件系统都提供其自身的 address_space_operations,即便在某些时候它们使用普通的函数。

host:这个文件的索引节点。

i_mmap:使用 address_space 的私有映射链表。

i_mmap_shared:该地址空间中共享映射的 VMA 链表。

i_shared_lock:保护此结构的自旋锁。

gfp_mask:调用 __alloc_pages() 所要用到的掩码。

   内存管理器需要定期将信息写回磁盘。但是内存管理器并不知道也不关心信息如何写回到磁盘,因此需要 a_ops 结构来调用相关的函数。它在  中的声明如下所示:

struct address_space_operations {
  int (*writepage)(struct page *);
  int (*readpage)(struct file *, struct page *);
  int (*sync_page)(struct page *);
  /*
   * ext3 requires that a successful prepare_write() call be followed
   * by a commit_write() call - they must be balanced
   */
  int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
  int (*commit_write)(struct file *, struct page *, unsigned, unsigned);
  /* Unfortunately this kludge is needed for FIBMAP. Don't use it */
  int (*bmap)(struct address_space *, long);
  int (*flushpage) (struct page *, unsigned long);
  int (*releasepage) (struct page *, int);
#define KERNEL_HAS_O_DIRECT /* this is for modules out of the kernel */
  int (*direct_IO)(int, struct inode *, struct kiobuf *, unsigned long, int);
#define KERNEL_HAS_DIRECT_FILEIO /* Unfortunate kludge due to lack of foresight */
  int (*direct_fileIO)(int, struct file *, struct kiobuf *, unsigned long, int);
  void (*removepage)(struct page *); /* called when page gets removed from the inode */
};

  该结构中的字段都是函数指针,它们描述如下。


writepage:把一个页面写到磁盘。写到文件里的偏移量保存在页结构中。寻找磁盘块的工作则由具体文件系统的代码完成,参考 buffer.c:block_write_full_page() 。

readpage:从磁盘读一个页面。参考 buffer.c:block_read_full_page()。

sync_page:同步一个脏页面到磁盘。参考 buffer.c :blobk_sync_page() 。

prepare_write:在复制用户空间数据到将要写回磁盘的页面之前,调用此函数。对于一个日志文件系统,该操作保证了文件系统日志是最新的。而对于一般的文件系统而言,它也确保了分配所需要的缓冲区页面。参考 buffer.c:block_prepare_write() 。

commit_write:在从用户空间复制数据之后,会调用该函数将数据提交给磁盘。参考 buffer.c:block_commit_write() 。

bmap:映射一个磁盘块使得裸设备 I/O 操作能够执行。虽然它在后援为一个交换文件而非交换分区时也用于换出页面,但它还是主要与具体的文件系统有关。

flushpage:该函数确保在释放一个页面之前已不存在等待该页面的 I/O 操作。参考 buffer.c:discard_bh_page() 。

releasepage:该函数在释放掉页面之前将刷新所有与这个页面有关的缓冲区。参考 try_to_free_buffers()。

direct_IO:该函数在对一个索引节点执行直接 I/O 时使用。由于 #define 的存在,因此外部模块可以决定在编译时间该函数是否可用,因为它仅仅在 2.4.21 中被引入。

direct_fileIO:用于对 struct file 进行直接 I/O。并且,#define 也由于外部模块而存在,因为该 API 只在 2.4.22 中被引入。

removepage:一个候选回调函数,当页面从页面高速缓存中由 remove_page_from_inode_queue() 移除时使用。

4.4.3 创建内存区域

   系统调用 mmap() 为一个进程创建新的内存区域。x86 中,mmap() 会调用 sys_mmap2(),而 sys_mmap2() 进一步调用 do_mmap2(),三个函数都使用相同的参数。do_mmap2() 负责获得 do_mmap_pgoff() 所需要的参数。而 do_mmap_pgoff() 才是所有体系结构中创建新区域的主要函数。


   do_mmap2() 首先清空 flags 参数中的 MAP_DENYWRITE 和 MAP_EXECUTABLE 位,因为 Linux 用不到这两个标志位,这一点在 mmap() 的操作手册里有说明。如果映射一个文件,do_mmap2() 将通过文件描述符查找到相应的 struct file,并在调用 do_mmap_pgoff() 前获得 mm_struct mmap_sem 信号量。


   do_mmap_pgoff 首先做一些合法检查。它首先检查在文件或设备被映射时,相应的文件系统和设备的操作函数是否有效。然后,它检查需要映射的大小是否与页面对齐,并且保证

不会在内核空间创建映射。最后,它必须保证映射的大小不会超过 pgoff 的范围以及这个进程没有过多的映射区域。


   这个函数的余下部分比较大,大致有以下几个步骤:


参数的合法检查。

找出内存映射所需的空闲线性地址空间。如果系统提供了基于文件系统和设备的 get_unmapped_area() 函数,那么会调用它,否则将使用 arch_get_unmapped_area() 函数。

获得 VM 标志位,并根据文件存取权限对它们进行检查。

如果在映射的地方有旧区域存在,系统会修正它,以便新的映射能用这一部分的区域。

从 slab 分配器里分配一个 vm_area_struct,并填充它的各个字段。

把新的 VMA 链接到链表中。

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

更新数据并返回。

(1)用户空间 mmap 函数

mmap

一文搞懂 mmap 涉及的所有内容

       #include <sys/mman.h>

       void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);
       int munmap(void *addr, size_t length);

start:用户进程中要映射的用户空间的起始地址,通常为NULL(由内核来指定)

length:要映射的内存区域的大小

prot:期望的内存保护标志

flags:指定映射对象的类型

fd:文件描述符(由open函数返回)

offset:设置在内核空间中已经分配好的的内存区域中的偏移,例如文件的偏移量,大小为PAGE_SIZE的整数倍

返回值:mmap()返回被映射区的指针,该指针就是需要映射的内核空间在用户空间的虚拟地址

(2)内核空间响应函数
(a)sys_mmap2
// arch/i386/kernel/sys_i386.c
asmlinkage long sys_mmap2(unsigned long addr, unsigned long len,
  unsigned long prot, unsigned long flags,
  unsigned long fd, unsigned long pgoff)
{
  return do_mmap2(addr, len, prot, flags, fd, pgoff);
}
(b)do_mmap2
static inline long do_mmap2(
  unsigned long addr, unsigned long len,
  unsigned long prot, unsigned long flags,
  unsigned long fd, unsigned long pgoff)
{
  int error = -EBADF;
  struct file * file = NULL;

  flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE);
  if (!(flags & MAP_ANONYMOUS)) {
    file = fget(fd);
    if (!file)
      goto out;
  }

  down_write(&current->mm->mmap_sem);
  error = do_mmap_pgoff(file, addr, len, prot, flags, pgoff);
  up_write(&current->mm->mmap_sem);

  if (file)
    fput(file);
out:
  return error;
}
(c)do_mmap_pgoff
// mm/mmap.c
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;

  if (file && (!file->f_op || !file->f_op->mmap))
    return -ENODEV;

  if (!len)
    return addr;

  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? */
  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.
   */
  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.
   */
  vm_flags = calc_vm_flags(prot,flags) | mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;

  /* mlock MCL_FUTURE? */
  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.. */
      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. */
      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:
      if (!(file->f_mode & FMODE_READ))
        return -EACCES;
      break;

    default:
      return -EINVAL;
    }
  } else {
    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:
  vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);
  if (vma && vma->vm_start < addr + len) {
    if (do_munmap(mm, addr, len))
      return -ENOMEM;
    goto munmap_back;
  }

  /* Check against address space limit. */
  if ((mm->total_vm << PAGE_SHIFT) + len
      > current->rlim[RLIMIT_AS].rlim_cur)
    return -ENOMEM;

  /* Private writable mapping? Check memory availability.. */
  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.
   */
  vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
  if (!vma)
    return -ENOMEM;

  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;
    if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
      goto free_vma;
    if (vm_flags & VM_DENYWRITE) {
      error = deny_write_access(file);
      if (error)
        goto free_vma;
      correct_wcount = 1;
    }
    vma->vm_file = file;
    get_file(file);
    error = file->f_op->mmap(file, vma);
    if (error)
      goto unmap_and_free_vma;
  } else if (flags & MAP_SHARED) {
    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
   */
  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();
    }
  }

  vma_link(mm, vma, prev, rb_link, rb_parent);
  if (correct_wcount)
    atomic_inc(&file->f_dentry->d_inode->i_writecount);

out:  
  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;

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);
free_vma:
  kmem_cache_free(vm_area_cachep, vma);
  return error;
}
(d)file->f_op->mmap 的一个实例

goldfish中的实现

// drivers/staging/android/binder.c
static struct file_operations binder_fops = {
  .owner = THIS_MODULE,
  .poll = binder_poll,
  .unlocked_ioctl = binder_ioctl,
  .mmap = binder_mmap,
  .open = binder_open,
  .flush = binder_flush,
  .release = binder_release,
};

static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
  int ret;
  struct vm_struct *area;
  struct binder_proc *proc = filp->private_data;
  const char *failure_string;
  struct binder_buffer *buffer;

  if ((vma->vm_end - vma->vm_start) > SZ_4M)
    vma->vm_end = vma->vm_start + SZ_4M;

  if (binder_debug_mask & BINDER_DEBUG_OPEN_CLOSE)
    printk(KERN_INFO
      "binder_mmap: %d %lx-%lx (%ld K) vma %lx pagep %lx\n",
      proc->pid, vma->vm_start, vma->vm_end,
      (vma->vm_end - vma->vm_start) / SZ_1K, vma->vm_flags,
      (unsigned long)pgprot_val(vma->vm_page_prot));

  if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) {
    ret = -EPERM;
    failure_string = "bad vm_flags";
    goto err_bad_arg;
  }
  vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;

  if (proc->buffer) {
    ret = -EBUSY;
    failure_string = "already mapped";
    goto err_already_mapped;
  }

  area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
  if (area == NULL) {
    ret = -ENOMEM;
    failure_string = "get_vm_area";
    goto err_get_vm_area_failed;
  }
  proc->buffer = area->addr;
  proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;

#ifdef CONFIG_CPU_CACHE_VIPT
  if (cache_is_vipt_aliasing()) {
    while (CACHE_COLOUR((vma->vm_start ^ (uint32_t)proc->buffer))) {
      printk(KERN_INFO "binder_mmap: %d %lx-%lx maps %p bad alignment\n", proc->pid, vma->vm_start, vma->vm_end, proc->buffer);
      vma->vm_start += PAGE_SIZE;
    }
  }
#endif
  proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);
  if (proc->pages == NULL) {
    ret = -ENOMEM;
    failure_string = "alloc page array";
    goto err_alloc_pages_failed;
  }
  proc->buffer_size = vma->vm_end - vma->vm_start;

  vma->vm_ops = &binder_vm_ops;
  vma->vm_private_data = proc;

  if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) {
    ret = -ENOMEM;
    failure_string = "alloc small buf";
    goto err_alloc_small_buf_failed;
  }
  buffer = proc->buffer;
  INIT_LIST_HEAD(&proc->buffers);
  list_add(&buffer->entry, &proc->buffers);
  buffer->free = 1;
  binder_insert_free_buffer(proc, buffer);
  proc->free_async_space = proc->buffer_size / 2;
  barrier();
  proc->files = get_files_struct(current);
  proc->vma = vma;

  /*printk(KERN_INFO "binder_mmap: %d %lx-%lx maps %p\n", proc->pid, vma->vm_start, vma->vm_end, proc->buffer);*/
  return 0;

err_alloc_small_buf_failed:
  kfree(proc->pages);
  proc->pages = NULL;
err_alloc_pages_failed:
  vfree(proc->buffer);
  proc->buffer = NULL;
err_get_vm_area_failed:
err_already_mapped:
err_bad_arg:
  printk(KERN_ERR "binder_mmap: %d %lx-%lx %s failed %d\n", proc->pid, vma->vm_start, vma->vm_end, failure_string, ret);
  return ret;
}

static struct vm_operations_struct binder_vm_ops = {
  .open = binder_vma_open,
  .close = binder_vma_close,
};

static void binder_vma_open(struct vm_area_struct *vma)
{
  struct binder_proc *proc = vma->vm_private_data;
  if (binder_debug_mask & BINDER_DEBUG_OPEN_CLOSE)
    printk(KERN_INFO
      "binder: %d open vm area %lx-%lx (%ld K) vma %lx pagep %lx\n",
      proc->pid, vma->vm_start, vma->vm_end,
      (vma->vm_end - vma->vm_start) / SZ_1K, vma->vm_flags,
      (unsigned long)pgprot_val(vma->vm_page_prot));
  dump_stack();
}

static void binder_vma_close(struct vm_area_struct *vma)
{
  struct binder_proc *proc = vma->vm_private_data;
  if (binder_debug_mask & BINDER_DEBUG_OPEN_CLOSE)
    printk(KERN_INFO
      "binder: %d close vm area %lx-%lx (%ld K) vma %lx pagep %lx\n",
      proc->pid, vma->vm_start, vma->vm_end,
      (vma->vm_end - vma->vm_start) / SZ_1K, vma->vm_flags,
      (unsigned long)pgprot_val(vma->vm_page_prot));
  proc->vma = NULL;
  binder_defer_work(proc, BINDER_DEFERRED_PUT_FILES);
}

4.4.4 查找已映射内存区域

4.4.5 查找空闲内存区域

   映射内存时,先得获取足够大的空闲区域,get_unmapped_area() 就用于查找一个空闲区域。


   调用图如图 4.4 所示,获取空闲区域并不复杂。get_unmapped_area() 有很多参数:struct file 表示映射的文件或设备;pgoff 表示文件的偏移量;address 表示请求区域的起始地址;length 表示请求区域的长度;flags 表示此区域的保护标志位。


   如果映射的是设备,比如视频卡,就还要使用 f_op->get_unmapped_area()。这是因为设备或文件有额外的操作要求,而通用代码并不能完成额外的要求,比如映射的地址必须对齐到一个特殊的虚拟地址。


   如果没有特殊的要求,系统将调用体系结构相关的函数 arch_get_unmapped_area() 。并不是所有的体系结构都提供自己的函数,这时将调用 mm/mmap.c 中提供的通用版本函数。

4.4.6 插入内存区域

4.4.7 合并邻接区域

  如果文件和权限都匹配,Linux 一般使用函数 merge_segments() [Hac02] 合并相邻的内存区域。其目的是减少 VMA 的数量,因为大量的操作会导致创建大量的映射,如系统调用 sys_mprotect()。该合并操作的开销很大,它会遍历大部分的映射,接着被删掉,特别是存在大量的映射时,merge_segments() 将会花费很长时间。


   目前与上述函数等价的函数是 vma_merge(),它仅在 2 个地方用到。第 1 个是在 sys_mmap() 中,当映射匿名区域时会调用它,因为匿名区域通常可以合并。第 2 个是在 do_brk() 中,给区域扩展一个新分配的区域后,应该将这 2 个区域合并,这时就调用 vma_merge()。


   vma_merge() 不仅合并两个线性区,还检查现有的区域能否安全地扩展到新区域,从而无需创建一个新区域。在没有文件或设备映射且两个区域的权限相同时,一个区域才能扩展其他地方也可能合并区域,只不过没有显式地调用函数。第 1 个就是在修正区域过程中系统调用 sys_mprotect() 时,如果两个区域的权限一致,就要确定它们是否要合并。第 2 个是在 move_vma() 中,它很可能把相似的区域移到一起。

4.4.8 重映射和移动内存区域

   系统调用 mremap() 用于扩展或收缩现有区域,由函数 sys_mremap() 实现。在一个区域扩展时有可能移动该区域,或者在一个区域将要与其他区域相交并且没有指定标志位 MREMAP_FIXED 时,也可能移动该区域。调用图如图 4.6 所示。


   移动一个区域时,do_mremap() 将首先调用 get_unmapped_area(),找到一个足以容纳扩展后的映射空闲区域,然后调用 move_vma() 把旧 VMA 移到新位置。move_vma() 调用图如图 4.7 所示。


   move_vma() 首先检查新区域能否和相邻的 VMA 合并。如果不能合并,将创建一个新 VMA,每次分配一个 PTE。然后调用 move_page_tables() (调用图如图 4.8 所示),将旧映射中所有页表项都复制过来。尽管可能有更好的移动页表的方法,但是采用这种方法使得错误恢复更直观,因为相对而言,回溯更直接。


   页面的内容并没有被复制,而是调用 zap_page_range() 将旧映射中的所有页都交换出去或者删除,通常缺页中断处理代码会将辅存、文件中的页交换回至内存,或调用设备相关函数 do_nopage() 。

4.4.9 对内存区域上锁

4.4.10 对区域解锁

    系统调用 munlock()munlockall() 分别是相应的解锁函数,它们分别由 sys_munlock()sys_munlockall() 实现。它们比上锁函数简单得多,因为不必做过多的检查,它们都依赖于同一个函数 do_mmap() 来修整区域。

4.4.11 上锁后修正区域

   在上锁或解锁时,VMA 将受到 4 个方面的影响,每个必须由 mlock_fixup() 修正。 上锁可能影响到所有的 VMA 时,系统就要调用 mlock_fixuo_all() 进行修正。第 2 个条件是被锁住区域地址的起始值,由 mlock_fixup_start() 处理,Linux 需要分配一个新的 VMA 来映射新区域。第 3 个条件是被锁住区域地址的结束值,由 mlock_fixup_end() 处理。 最后,mlock_fixup_middle() 处理映射区域的中间部分,此时需要分配 2 个新的 VMA。


   值得注意的是创建上锁的 VMA 时从不合并,即使该区域被解锁也不能合并。一般而言,已经锁住某个区域的进程没必要再次锁住同一区域,因为经常开销处理器计算能力来合并和

分裂区域是不值得的。

4.4.12 删除内存区域

   do_munmap() 负责删除区域。它相对于其他的区域操作函数,比较简单,它基本 上可分为 3 个部分。第 1 部分是修整红黑树,第 2 部分是释放和对应区域相关的页面和页表项,第 3 部分是如果生成了空洞就修整区域。


   为保证红黑树已排好序,所有要删除的 VMA 都添加到称为 free 的链表,然后利用 rb_erase() 从红黑树中删除。如果区域还存在,那么在后来的修整中将以它们的新地址添加到系统中。


   接下来遍历 free 所指向的 VMA 链表,即使删除线性区的一部分,系统也会调用 remove_shared_vm_struct() 把共享文件映射删掉。再次说明,如果仅是部分分删除,在修整中也会重新创建。zap_page_range() 删掉所有与区域相关的页面,而部分删除则调用 unmap_fixup 处理。


   最后调用 free_pgtables() 释放所有和对应区域相关的页表项。这里注意到页表项并没有彻底释放完很重要。它反而释放所有的 PGD 和页目录项,因此,如果仅有一半的 PGD 用于映射,则不需要释放页表项。这是因为释放少量的页表项和数据结构代价很大,而且这些结构很小,另外它们还可能会再次被使用。

4.4.13 删除所有的内存区域

  进程退出时,必须删除与其 mm_struct 相关联的所有 VMA,由函数 exit_mmap() 负责操作。这是个非常简单的函数,在遍历 VMA 链表前将刷新 CPU 高速缓存,依次删除每一个 VMA 并释放相关的页面,然后刷新 TLB 和删除页表项,这个过程在代码注释中有详细描述。

4.5 异常处理

   VM 中很重要的一个部分就是如何捕获内核地址空间异常,这并不是内核的 bug。这部分不讨论如何处理诸如除数为零的异常错误,仅关注由于页面中断而产生的异常。有两种情况会发生错误的引用。第 1 种情况是进程通过系统调用向内核传递了一个无效的指针,内核必须能够安全地陷入,因为开始只检查地址是否低于 PAGE_OFFSET。第 2 种情况是内核使用 copy_from_user() 或 copy_to_user() 读写用户空间的数据。


   编译时,连接器将在代码段中的 __ex_table 处创建异常表,异常表开始于 __start_ex_table,结束于 __stop_ex_table。每个表项的类型是 exception_table_entry,由可执行点和修整子程序二者组成。在产生异常时,缺页中断处理程序不能处理,它调用 search_exception_table() 查看是否为引起中断的指令提供了修整子程序,若系统支持模块,还要搜索每个模块的异常表。

4.6 缺页中断

   进程线性地址空间里的页面不必常驻内存。例如,进程的分配请求并被立即满足,空间仅保留为满足 vm_area_struct 的空间。其也非常驻内存页面的例子有,页面可能被交换到后援存储器,还有就是写一个只读页面。


   和大多操作系统一样,Linux 采用请求调页技术来解决非常驻页面的问题。在操作系统捕捉到由硬件发出的缺页中断异常时,它才会从后援存储器中调入请求的页面。由后援存储器的特征可知,采取页面预取技术可以减少缺页中断[MM87],但是 Linux 在这方面相当原始。在将一个页面读入交换区时,swapin_readahead() 会预取该页面后多达 2page_cluster 的页面,并放置在交换区中。不幸的是,很快要用到的页面只有一次机会邻近交换区,从而导致预约式换页技术很低效。Linux 将采用适合应用程序的预约式换页策略[KMC02]。


   有两种类型的缺页中断,分别是主缺页中断和次缺页中断。当要费时地从磁盘中读取数据时,就会产生主缺页中断,其他的就是次缺页中断,或者是轻微的缺页中断。Linux 通过字段 task_struct→maj_fault 和 task_struct→min_fault 来统计各自的数目。

4.6.1 处理缺页中断

4.6.2 请求页面分配


(1)处理匿名页面

   如果 vm_area_struct→ vm_ops 字段没有被填充或者没有提供 nopage() 函数,则调用 do_anonymous_page() 处理匿名访问。只有两中处理方式,第一次读和第一次写。由于是匿名页面,第一次读很简单,因为不存在数据,所以系统一般使用映射 PTE 的全零页 empty_zero_page,并且 PTE 是写保护的,所以如果进程要写页面就发生另一个缺页中断。在 x86 中,函数 mem_init() 负责把全局零页面归零。


   如果是第一次写页面,就调用 alloc_page() 分配一个由 clear_user_highpage() 用零填充的空闲页(见第 7 章)。假定成功地分配了这样一个页面,mm_struct 中的 Resident Set Size (RSS) 字段将递增;在一些体系结构中,为保证高速缓存的一致性,当一个页面插入进程空间时要调用 flush_page_to_ram() 。然后页面插入到 LRU 链表中,以后就可以被页面回收代码处理。最后需要更新进程的页表项来反映新映射。

(2)处理文件/设备映射页

   如果被文件或设备映射,VMA 中的 vm_operation_struct 将提供 nopage() 函数。如果是文件映射,函数 filemap_nopage() 将替代 nopage() 分配一个页面并从磁盘中读取一个页面大小的数据。如果页面由虚文件映射而来,就使用函数 shmem_nopage() (见第 12 章)。每种设备驱动程序将提供不同的 nopage() 函数,内部如何实现对我们来说并不重要,只要知道该函数返回一个可用的 struct page 即可。


   在返回页面时,要先做检查以确定分配是否成功,如果不成功就返回相应的错误。然后检查提前 COW 失效是否发生。如果是向页面写,而在受管 VMA 中没有包括 VM_SHARED 标志,就会发生提前 COW 失效。提前 COW 失效是指分配一个新页面,在减少 nopage() 返回页面的引用计数前就将数据交叉地复制过来。

4.6.3 请求换页

4.6.4 写时复制(COW)页

深入理解Linux虚拟内存管理(一)4:https://developer.aliyun.com/article/1597699

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