13 分配線性地址區間

前面講了那麼多線性區底層分配的細節,現在讓我們討論怎樣分配一個新的線性地址區間。爲了做到這點,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;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章