13 分配线性地址区间

来源:互联网 发布:mobi域名在哪里注册好 编辑:程序博客网 时间:2024/06/05 00:24

前面讲了那么多线性区底层分配的细节,现在让我们讨论怎样分配一个新的线性地址区间。为了做到这点,do_mmap()函数为当前进程创建并初始化一个新的线性区。不过,分配成功之后,可以把这个新的线性区与进程已有的其他线性区进行合并。


static inline unsigned long do_mmap(struct file *file, unsigned long addr,
 unsigned long len, unsigned long prot,
 unsigned long flag, unsigned long offset)
{
 unsigned long ret = -EINVAL;
 if ((offset + PAGE_ALIGN(len)) < offset)
  goto out;
 if (!(offset & ~PAGE_MASK))
  ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
 return ret;
}

 

do_mmap()函数的参数比较多,我们来一一分解:

file和offset:如果新的线性区将把一个文件映射到内存,则使用文件描述符指针file和文件偏移量offset。这个主题将在回收页框的相关专题进行讨论。在这里,我们假定不需要内存映射,所以file和offset都为空(NULL和0)。

addr:这个线性地址指定从何处开始查找一个空闲的区间。

len:线性地址区间的长度。

prot:这个参数指定这个线性区所包含页的访问权限。可能的标志有PROT_READ、PROT_WRITE、PROT_EXEC和PROT_NONE。前三个标志与标志VM_READ、VM_WRITE及VM_EXEC的意义一样。PROT_NONE表示进程没有以上三个访问权限中的任意一个。

flag:这个参数指定线性区的其他标志:
MAP_GROWSDOWN、MAP_LOCKED、MAP_DENYWRITE和MAP_EXECUTABLE
它们的含义与“线性区数据结构”博文中所列出标志的含义相同。
MAP_SHARED和MAP_PRIVATE
前一个标志指定线性区中的页可以被几个进程共享;后一个标志作用和两个标志都指向vm_area_struct描述符中的VM_SHARED标志。
MAX_FIXED
区间的起始地址必须是由参数addr所指定的。
MAX_ANONYMOUS
没有文件与这个线性区相关联。
MAP_NORESERVE
函数不必预先检查空闲页框的数目。
MAP_POPULATE
函数应该为线性区建立的映射提前分配需要的页框。该标志仅对映射文件的线性区和IPC共享的线性区有意义。
MAX_NONBLOCK
只有在MAP_POPULATE标志置位时才有意义:提前分配页框时,函数肯定不阻塞。

 

我们看到do_mmap()函数对offset的值进行一些初步检查,然后执行do_mmap_pgoff()函数。这里假设新的线性地址区间映射的不是磁盘文件,仅对实现匿名线性区的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;
 struct inode *inode;
 unsigned int vm_flags;
 int correct_wcount = 0;
 int error;
 struct rb_node ** rb_link, * rb_parent;
 int accountable = 1;
 unsigned long charged = 0, reqprot = prot;
 /* 首先检查参数的值是否正确,所提的请求是否能被满足。尤其是要检查以下不能满足请求的条件:*/
 if (file) { /* 如果是映射磁盘文件,则检查: */
  if (is_file_hugepages(file)) /* 是否是大块文件 */
   accountable = 0;

  if (!file->f_op || !file->f_op->mmap) /* 文件必须有自己的mmap方法 */
   return -ENODEV;

  if ((prot & PROT_EXEC) && /* 文件必须是不可执行的 */
      (file->f_vfsmnt->mnt_flags & MNT_NOEXEC))
   return -EPERM;
 }
 /*
  * Does the application expect PROT_READ to imply PROT_EXEC?
  *
  * (the exception is when the underlying filesystem is noexec
  *  mounted, in which case we dont add PROT_EXEC.)
  * 上面的英文解释看不懂,但是可以肯定是跟文件映射相关,不理睬*/
 if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
  if (!(file && (file->f_vfsmnt->mnt_flags & MNT_NOEXEC)))
   prot |= PROT_EXEC;

 if (!len) /* 检查len,如果是0则错误: */
  return -EINVAL;

 if (!(flags & MAP_FIXED)) /* 检查flags,如果不是MAP_FIXED则说明区间的起始地址不是由参数addr所指定的: */
  addr = round_hint_to_min(addr);  /* 此时就需要把addr设置成mmap_min_addr */

 error = arch_mmap_check(addr, len, flags); /* 执行一系列处理器相关检查,x86体系为空函数 */
 if (error)
  return error;

 /* Careful about overflows.. 先做个页面对齐调整,在检测防止内存溢出*/
 len = PAGE_ALIGN(len);
 if (!len || len > TASK_SIZE)
  return -ENOMEM;

 /* offset overflow? 同样检查最后那个页的页内偏移是否有溢出,这里为0,当然不会啦*/
 if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
               return -EOVERFLOW;

 /* Too many mappings? 进程已经映射了过多的线性区,因此mm内存描述符的map_count字段的值可能超过了允许的最大值*/
 if (mm->map_count > sysctl_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.
  * 上面的一系列检查都通过了,那么调用“线性区的底层处理”博文中的函数获得新线性区的线性地址区间。
  * 由于是非文件映射,所以最终调用的是current->mm->get_unmapped_area,
  * 即执行内存描述符的get_unmapped_area方法
  */
 addr = get_unmapped_area_prot(file, addr, len, pgoff, flags, prot & PROT_EXEC);
 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.
  * 通过把存放在prot和flags参数中的值进行组合来计算新线性区描述符的标志:
  */
 vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
   mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;

 /* 注意,只有在prot中设置了相应的PROT_READ、PROT_WRITE和PROT_EXEC标志,
  * calc_vm_prot_bits()函数才在vm_flags中设置VM READ, VM WRITE和VM EXEC标志;
  * 只有在flags设置了相应的MAP_GROWSDOWN,MAP_DENYWRITE,MAP_EXECUTABLE和MAP_LOCKED标志,
  * calc_vm_flag_bits()也才在VM_flags中设置VM_GROWSDOWN、VM_DENYWRITE、VM_EXECUTABLE和VM_LOCKED标志。
  * 在vm_flags中还有几个标志被置为1:VM_MAYREAD、VM_MAYWRITE、VM_MAYEXEC,
  * 在mm->def_flags中所有线性区的默认标志,以及如果线性区的页与其他进程共享时的VM_SHARED和VM_MAYSHARE。
  */
 if (flags & MAP_LOCKED) {
  if (!can_do_mlock())
   return -EPERM;
  vm_flags |= VM_LOCKED;
 }
 /* mlock MCL_FUTURE? */
 if (vm_flags & VM_LOCKED) {
  /* flag参数指定新线性地址区间的页必须被锁在RAM中,
    * 但不允许进程创建上锁的线性区,
   * 或者进程加锁页的总数超过了保存在进程描述符signal-> rlim[RLIMIT_MEMLOCK].rlim_cur字段中的阈值。
   */
  unsigned long locked, lock_limit;
  locked = len >> PAGE_SHIFT;
  locked += mm->locked_vm;
  lock_limit = current->signal->rlim[RLIMIT_MEMLOCK].rlim_cur;
  lock_limit >>= PAGE_SHIFT;
  if (locked > lock_limit && !capable(CAP_IPC_LOCK))
   return -EAGAIN;
 }

 inode = file ? file->f_dentry->d_inode : NULL;

 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(inode) && (file->f_mode & FMODE_WRITE))
    return -EACCES;

   /*
    * Make sure there are no mandatory locks on the file.
    */
   if (locks_verify_locked(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 {
  switch (flags & MAP_TYPE) {
  case MAP_SHARED:
   vm_flags |= VM_SHARED | VM_MAYSHARE;
   break;
  case MAP_PRIVATE:
   /*
    * Set pgoff according to addr for anon_vma.
    */
   pgoff = addr >> PAGE_SHIFT;
   break;
  default:
   return -EINVAL;
  }
 }

 error = security_file_mmap_addr(file, reqprot, prot, flags, addr, 0);
 if (error)
  return error;

 /* Clear old maps */
 error = -ENOMEM;
munmap_back:
 /* 确定处于新区间之前的线性区对象的位置,以及在红-黑树中新线性区的位置: */
 vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);
 if (vma && vma->vm_start < addr + len) {
  /* find_vmaprepare()函数也检查是否还存在与新区间重叠的线性区。
    * 这种情况发生在函数返回一个非空的地址,这个地址指向一个线性区,而该区的起始位置位于新区间结束地址之前的时候。
    * 在这种情况下,调用do_munmap()删除新的区间,然后重复整个步骤。
   */
  if (do_munmap(mm, addr, len))
   return -ENOMEM;
  goto munmap_back;
 }

 

 /* Check against address space limit. */
 if (!may_expand_vm(mm, len >> PAGE_SHIFT))
  /* 检查插人新的线性区是否引起进程地址空间的大小
    * (mm->total_vm<<PAGE_SHIFT) + len超过存放在进程描述符signal->rlim[RLIMIT_AS].rlim_cur字段中的阈值。
    * 如果是,就返回出错码-ENOMEM。注意,这个检查只在这里进行,而不在前面与其他检查一起进行,
    * 因为一些线性区可能在刚才调用do_munmap()时候被删除。
   */
  return -ENOMEM;

 if (accountable && (!(flags & MAP_NORESERVE) ||
       sysctl_overcommit_memory == OVERCOMMIT_NEVER)) {
  /* 如果在flags参数中没有设置MAP_NORESERVE标志,新的线性区包含私有可写页,
    * 并且没有足够的空闲页框,则返回出错码-ENOMEM;这最后一个检查是由security_vm_enough_memory()函数实现的。
   */
  if (vm_flags & VM_SHARED) {
   /* Check memory availability in shmem_file_setup? */
   vm_flags |= VM_ACCOUNT;
  } else if (vm_flags & VM_WRITE) {
   /*
    * Private writable mapping: check memory availability
    */
   charged = len >> PAGE_SHIFT;
   if (security_vm_enough_memory(charged))
    return -ENOMEM;
   vm_flags |= VM_ACCOUNT;
  }
 }

 /*
  * Can we just expand an old private anonymous mapping?
  * The VM_SHARED test is necessary because shmem_zero_setup
  * will create the file object for a shared anonymous map below.
  * 如果新区间是私有的(没有设置VM_SHARED),且映射的不是磁盘上的一个文件,
  * 那么,调用vma_merge()检查前一个线性区是否可以以这样的方式进行扩展来包含新的区间。
  * 当然,前一个线性区必须与在vm_flags局部变量中存放标志的那些线性区具有完全相同的标志。
  * 如果前一个线性区可以扩展,那么,vma_merge()也试图把它与随后的线性区进行合并
  * (这发生在新区间填充两个线性区之间的空洞,且三个线性区全部具有相同的标志的时候)。
  * 万一在扩展前一个线性区时获得成功,则跳到out。
  */
 if (!file && !(vm_flags & VM_SHARED) &&
     vma_merge(mm, prev, addr, addr + len, vm_flags,
     NULL, NULL, pgoff, NULL))
  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分配函数kmem_cache_alloc()为新的线性区分配一个vm_area_struct数据结构。
  */
 vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
 if (!vma) {
  error = -ENOMEM;
  goto unacct_error;
 }

 /* 初始化新的线性区对象(由vma指向): */
 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 &
    (VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)];
 vma->vm_pgoff = pgoff;

 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 (vm_flags & VM_SHARED) {
  /* 如果MAP_SHARED标志被设置(以及新的线性区不映射磁盘上的文件),则该线性区是一个共享匿名区:
   * 调用shmem_zero_setup()对它进行初始化。共享匿名区主要用于进程间通信。
   */
  error = shmem_zero_setup(vma);
  if (error)
   goto free_vma;
 }

 /* We set VM_ACCOUNT in a shared mapping's vm_flags, to inform
  * shmem_zero_setup (perhaps called through /dev/zero's ->mmap)
  * that memory reservation must be checked; but that reservation
  * belongs to shared memory object, not to vma: so now clear it.
  */
 if ((vm_flags & (VM_SHARED|VM_ACCOUNT)) == (VM_SHARED|VM_ACCOUNT))
  vma->vm_flags &= ~VM_ACCOUNT;

 /* Can addr have changed??
  *
  * Answer: Yes, several device drivers can do it in their
  *         f_op->mmap method. -DaveM
  */
 addr = vma->vm_start;
 pgoff = vma->vm_pgoff;
 vm_flags = vma->vm_flags;

 if (vma_wants_writenotify(vma))
  vma->vm_page_prot =
   protection_map[vm_flags & (VM_READ|VM_WRITE|VM_EXEC)];

 if (!file || !vma_merge(mm, prev, addr, vma->vm_end,
   vma->vm_flags, NULL, file, pgoff, vma_policy(vma))) {
  file = vma->vm_file;
  /* 调用vma_link()把新线性区插人到线性区链表和红-黑树中(参见前面“线性区的底层处理函数”博文)。*/
  vma_link(mm, vma, prev, rb_link, rb_parent);
  if (correct_wcount)
   atomic_inc(&inode->i_writecount);
 } else {
  if (file) {
   if (correct_wcount)
    atomic_inc(&inode->i_writecount);
   fput(file);
  }
  mpol_free(vma_policy(vma));
  kmem_cache_free(vm_area_cachep, vma);
 }
out: 
 /* 增加存放在内存描述符total_vm字段中的进程地址空间的大小。 */
 mm->total_vm += len >> PAGE_SHIFT;
 vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
 if (vm_flags & VM_LOCKED) {
  /* 如果设置了VM_LOCKED标志,就调用make_pages_present()连续分配线性区的所有页,并把它们锁在RAM中:*/
  mm->locked_vm += len >> PAGE_SHIFT;
  make_pages_present(addr, addr + len);
  /* make_pages_present()函数按如下方式调用get_user_pages():
   *    write = (vma->vm_flags & VM_WRITE) != 0;
   *    get_user_pages(current, current->mm, addr, len, write, 0, NULL, NULL);
   * get_user_pages()函数在addr和addr+len之间页的所有起始线性地址上循环;
   * 对于其中的每个页,该函数调用follow_page()检查在当前页表中是否有到物理页的映射。
   * 如果没有这样的物理页存在,则get_user_pages()调用handle_mm_fault(),
   * 以后我们会看到,后一个函数分配一个页框并根据内存描述符的vm_flags字段设置它的页表项。
   */
 }
 if (flags & MAP_POPULATE) {
  up_write(&mm->mmap_sem);
  sys_remap_file_pages(addr, len, 0,
     pgoff, flags & MAP_NONBLOCK);
  down_write(&mm->mmap_sem);
 }
 /* 最后,函数通过返回新线性区的线性地址而终止。 */
 return addr;

unmap_and_free_vma:
 if (correct_wcount)
  atomic_inc(&inode->i_writecount);
 vma->vm_file = NULL;
 fput(file);

 /* Undo any partial mapping done by a device driver. */

 unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
 charged = 0;
free_vma:
 kmem_cache_free(vm_area_cachep, vma);
unacct_error:
 if (charged)
  vm_unacct_memory(charged);
 return error;

原创粉丝点击