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

目录
相关文章
|
3月前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
193 6
|
12天前
|
缓存 Java Linux
如何解决 Linux 系统中内存使用量耗尽的问题?
如何解决 Linux 系统中内存使用量耗尽的问题?
|
12天前
|
缓存 Linux
如何检查 Linux 内存使用量是否耗尽?
何检查 Linux 内存使用量是否耗尽?
|
22天前
|
算法 Linux 开发者
深入探究Linux内核中的内存管理机制
本文旨在对Linux操作系统的内存管理机制进行深入分析,探讨其如何通过高效的内存分配和回收策略来优化系统性能。文章将详细介绍Linux内核中内存管理的关键技术点,包括物理内存与虚拟内存的映射、页面置换算法、以及内存碎片的处理方法等。通过对这些技术点的解析,本文旨在为读者提供一个清晰的Linux内存管理框架,帮助理解其在现代计算环境中的重要性和应用。
|
4天前
|
存储 算法 安全
深入理解Linux内核的内存管理机制
本文旨在深入探讨Linux操作系统内核的内存管理机制,包括其设计理念、实现方式以及优化策略。通过详细分析Linux内核如何处理物理内存和虚拟内存,揭示了其在高效利用系统资源方面的卓越性能。文章还讨论了内存管理中的关键概念如分页、交换空间和内存映射等,并解释了这些机制如何协同工作以提供稳定可靠的内存服务。此外,本文也探讨了最新的Linux版本中引入的一些内存管理改进,以及它们对系统性能的影响。
|
27天前
|
存储 缓存 监控
|
2月前
|
存储 缓存 监控
Linux中内存和性能问题
【10月更文挑战第5天】
40 4
|
2月前
|
算法 Linux
Linux中内存问题
【10月更文挑战第6天】
54 2
|
25天前
|
缓存 算法 Linux
Linux内核中的内存管理机制深度剖析####
【10月更文挑战第28天】 本文深入探讨了Linux操作系统的心脏——内核,聚焦其内存管理机制的奥秘。不同于传统摘要的概述方式,本文将以一次虚拟的内存分配请求为引子,逐步揭开Linux如何高效、安全地管理着从微小嵌入式设备到庞大数据中心数以千计程序的内存需求。通过这段旅程,读者将直观感受到Linux内存管理的精妙设计与强大能力,以及它是如何在复杂多变的环境中保持系统稳定与性能优化的。 ####
30 0
|
2月前
|
存储 缓存 固态存储