进程分配用户空间或是文件或者设备文件空间映射函数分析(二)

 我们在前面文章中大概分析了do_mmap2()函数的大概内容,在这里我把里面涉及到的某些函数进行更具体的分析,我们在确定线性地址申请之前要进行一系列的判断,最重要的判断就要数 find_vma_prepare()这个函数了,我们来看看它在do_mmap_pgoff()函数中的位置吧。

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;
 }
这里可以看出它是个跳转循环,目的就是当发现有区域重叠后,我们把重叠的区域进行删除,接着我们把从新检测是否还有区域的重叠。find_vma_prepare()函数就是起到这个作用的。我们来仔细看看这个函数的具体代码吧。

static struct vm_area_struct *
find_vma_prepare(struct mm_struct *mm, unsigned long addr,
  struct vm_area_struct **pprev, struct rb_node ***rb_link,
  struct rb_node ** rb_parent)
{
 struct vm_area_struct * vma;
 struct rb_node ** __rb_link, * __rb_parent, * rb_prev;

 __rb_link = &mm->mm_rb.rb_node;//一上来我们要从这棵黑红数的树根开始进行我们说的上述检查。一般树根有struct rb_root结构体描述,当前进程的struct mm_struct mm里面的mm_rb就是这个树根。描述树根的结构体里面有成员struct rb_node结构体,这个结构体就是描述这个树根对应的节点啦,mm_rb.rb_node这个变量就是这个类型变量了。这样,__rb_link就指向了这个节点啦。
 rb_prev = __rb_parent = NULL;
 vma = NULL;

 while (*__rb_link) {//只要__rb_link指针所指向的struct vm_area_struct结构体不为空时,我们继续循环。
  struct vm_area_struct *vma_tmp;

  __rb_parent = *__rb_link;
  vma_tmp = rb_entry(__rb_parent, struct vm_area_struct, vm_rb);//这个函数就是通过树节点来找到对应的struct vm_area_struct结构体的。每个进程都可以通过mm->mm_rb先找到这棵黑红树,这样我们就从这棵树的根节点开始进行检测,节点和对应区域的描述结构体struct vm_area_struct的vm_rb都是struct rb_node结构体,都是指向同一个位置。其实每个区域通过vm_area_struct结构体来描述的,其中的vm_rb也就是这个区域与当前进程的mm里面的树发生联系的,其实这棵数从树根开始按从低地址到高地址的顺序连接所有的树节点,每个树节点就是每个区域的vm_rb变量啦。这样我们就可以通过访问节点来访问到每个区域了。

  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_prev表示是这个区域的前一个区域。
   __rb_link = &__rb_parent->rb_right;//我们来遍历一下右数啦。
  }
 }

 *pprev = NULL;//如果循环后还是发现没有重叠的话
 if (rb_prev)如果发现我们的申请区域比所有的区域都要高地址的话
  *pprev = rb_entry(rb_prev, struct vm_area_struct, vm_rb);//我们用pprev来指向我们这个区域的前一个区域。
 *rb_link = __rb_link;//应该为NULL
 *rb_parent = __rb_parent;
 return vma;这里返回的话,一定是null。
}

接下来我们继续来说一个函数,这个函数没做什么事情,就是根据你申请地址线性区域空间在原来树的位置,如果在该区域在不可以被合并的前提下,我们就把其申请好的并初始化号额struct vm_area_struct 结构接到这棵树上,这个函数就是vm_link(),我们来看看他在do_mmap_pgoff()函数中的位置:

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(mm, vma, prev, rb_link, rb_parent);
  if (correct_wcount)
   atomic_inc(&inode->i_writecount);
 } else {

。。。。。。。

可以看出的是,if开始的时候尝试了合并,但是合并失败了,在此之前已经试过一次合并。有人要问,为什么要进行两次合并。我这里有必要仔细说一下,我分两种情况诉说一下。如果我们是为文件或者是设备文件空间进行映射的话,我们肯定不会做第一次的合并,我们来看看这个if判断语句前面的第一个条件是什么:!file。而且后续都是“并且”的关系了。所以我们不管可不可以合并成功,我们都先向高速缓存vm_area_cachep申请vm_area_struct描述区域的结构体。初始化和建立映射之后,我们再来判断我们这个vm_area_cachep是否可以被其他的区域进行合并。如果不行,我们就要使用到vm_link()函数来“挂”了。第二种情况就很简单了,希望读者自己看代码自己进行分析了。

我们现在来说说这个函数吧~

static void vma_link(struct mm_struct *mm, struct vm_area_struct *vma,
   struct vm_area_struct *prev, struct rb_node **rb_link,
   struct rb_node *rb_parent)
{
 struct address_space *mapping = NULL;

 if (vma->vm_file)//如果申请的区域是用来文件映射
  mapping = vma->vm_file->f_mapping;//找到映射文件的struct address_space结构指针

 if (mapping)//如果发现文件的地址空间描述结构指针非空
  spin_lock(&mapping->i_mmap_lock);//我们将文件地址空间上锁
 anon_vma_lock(vma);判断该域是否是匿名区域,而不是文件映射区域时,我们将vma所属的struct anon_vma结构的匿名区域上锁。

 __vma_link(mm, vma, prev, rb_link, rb_parent);//基本上这里要完成3个链表的链接,第一个就是vma->vm_next.我们根据申请区域所在位置(可以在最尾或是最前,说是中间),来把这个结构体添加到mm->mmap链表中。第二个链表其实我们之前叫做黑红树,我们就是把struct vm_area_struct结构体作为一个树节点挂在所属进程的mm_struct结构体内的黑红树上。第三个链表就是和匿名页有关系,vm_area_struct结构体内有一个anon_vm变量就是该结构体链接到匿名内存页struct anon_vma的指针,我们会把引用该匿名内存页的struct vm_area_struct结构体通过anon_vma_node成员链接到该匿名内存页anon_vma的head所指链表上。 
 __vma_link_file(vma);//将vma连接到文件的线性区域优先搜索树链表中。

 anon_vma_unlock(vma);
 if (mapping)
  spin_unlock(&mapping->i_mmap_lock);

 mm->map_count++;//将进程的线性地址空间映射计数器加1.
 validate_mm(mm);
}
接下来,我们要说的就是为线性地址空间的每一页在物理内存中申请相应的物理页,同时还要完成页表的映射。这个函数就是make_pages_present()函数,这个函数在do_mmap_pgoff()函数的位置如下:

if (vm_flags & VM_LOCKED) {
  mm->locked_vm += len >> PAGE_SHIFT;
  make_pages_present(addr, addr + len);
 }。。。。

这里你可以看出,如果设置了VM_LOCKED标志时,表示我们申请的线性空间是否要锁在物理内存中。如果要求锁在物理内存中时,我们就要为之做两样事情,第一样就是在物理内存中申请相应的物理内存页,第二件事情就是我们要建立页表进行映射。我们现在来看下它的具体代码吧。

int make_pages_present(unsigned long addr, unsigned long end)
{
 int ret, len, write;
 struct vm_area_struct * vma;

 vma = find_vma(current->mm, addr);//这个函数你会发现和我在这片文章中第一个函数分析中有很多相似,这里就是找到这个addr所在的区域,然后返回这个区域的描述结构体struct vm_area_struct.
 if (!vma)
  return -1;
 write = (vma->vm_flags & VM_WRITE) != 0;
 if (addr >= end)//起始位置小于结束为止,崩溃。
  BUG();
 if (end > vma->vm_end)//结束位置大于区域的结束位置。
  BUG();
 len = (end+PAGE_SIZE-1)/PAGE_SIZE-addr/PAGE_SIZE;//这里是要统计申请的线性空间起始是占有多少页数的。
 ret = get_user_pages(current, current->mm, addr,
   len, write, 0, NULL, NULL);//这个函数返回成功申请的物理内存页的数量是多少。
 if (ret < 0)
  return ret;
 return ret == len ? 0 : -1;
}

在上面的函数中我们看到了get_user_pages()函数,我们接下来仔细分析一下这个函数主要完成一些什么工作啦。

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;
 flags = write ? (VM_WRITE | VM_MAYWRITE) : (VM_READ | VM_MAYREAD);//这里是为了判断申请的区域是否可写,如果可以的话,我们改变flags标志。
 flags &= force ? (VM_MAYREAD | VM_MAYWRITE) : (VM_READ | VM_WRITE);
 i = 0;

 do {
  struct vm_area_struct * vma;

  vma = find_extend_vma(mm, start);//这个函数有点意思,如果申请的区域的开始位置在某区域的开头的以后的位置话,我们可以直接返回这个区域对应的vm_area_struct结构体,但是如果发现申请的区域的起始位置超出了,这样我们就要调用make_pages_present()函数申请物理页了,这里有两个方式,如果你设置了向上伸展的方式话,你就用make_pages_present(addr,start)。反之你就调用make_pages_present(addr,prev->end),反正最后我们找到了addr对应的struct vm_area_struct结构体。
  if (!vma && in_gate_area(tsk, start)) {//这个对于ARM的话肯定不满足条件,我们不会做下面的语句。
   unsigned long pg = start & PAGE_MASK;
   struct vm_area_struct *gate_vma = get_gate_vma(tsk);
   pgd_t *pgd;
   pmd_t *pmd;
   pte_t *pte;
   if (write) /* user gate pages are read-only */
    return i ? : -EFAULT;
   if (pg > TASK_SIZE)
    pgd = pgd_offset_k(pg);
   else
    pgd = pgd_offset_gate(mm, pg);
   BUG_ON(pgd_none(*pgd));
   pmd = pmd_offset(pgd, pg);
   BUG_ON(pmd_none(*pmd));
   pte = pte_offset_map(pmd, pg);
   BUG_ON(pte_none(*pte));
   if (pages) {
    pages[i] = pte_page(*pte);
    get_page(pages[i]);
   }
   pte_unmap(pte);
   if (vmas)
    vmas[i] = gate_vma;
   i++;
   start += PAGE_SIZE;
   len--;
   continue;
  }

  if (!vma || (vma->vm_flags & VM_IO)
    || !(flags & vma->vm_flags))//如果vma是空的话,或者这个区域是用来I/O的映射用的,或者标志和原来的标志不一样时。
   return i ? : -EFAULT;

  if (is_vm_hugetlb_page(vma)) {//这个我们对于ARM来说也是不需要考虑的。
   i = follow_hugetlb_page(mm, vma, pages, vmas,
      &start, &len, i);
   continue;
  }
  spin_lock(&mm->page_table_lock);
  do {
   struct page *map;
   int lookup_write = write;
   while (!(map = follow_page(mm, start, lookup_write))) {//这里就是要判断申请的线性地址空间是否都有对应的物理内存页,我们都是通过查找二级映射表来查找的,最后我们会找到我们对应的struct page结构体的,但是如果没找到的话,我们就要做这个while循环为止。
    if (!lookup_write &&
        untouched_anonymous_page(mm,vma,start)) {
     map = ZERO_PAGE(start);//如果该区域是不可写的而且同时该区域是不再使用的匿名页时,我们就把线性地址永久映射到empty_zero_page对应的物理内存页。
     break;
    }
    spin_unlock(&mm->page_table_lock);
    switch (handle_mm_fault(mm,vma,start,write)) {//调用缺页处理函数handle_mm_fault(),判断它的返回值。
    case VM_FAULT_MINOR://如果是次缺页
     tsk->min_flt++;
     break;
    case VM_FAULT_MAJOR://如果是主缺页
     tsk->maj_flt++;
     break;
    case VM_FAULT_SIGBUS://如果是其它错误
     return i ? i : -EFAULT;
    case VM_FAULT_OOM://如果是内存不足
     return i ? i : -ENOMEM;
    default:
     BUG();
    }
    lookup_write = write && !force;
    spin_lock(&mm->page_table_lock);
   }
   if (pages) {//ARM处理器,这个函数不可能成立
    pages[i] = get_page_map(map);
    if (!pages[i]) {
     spin_unlock(&mm->page_table_lock);
     while (i--)
      page_cache_release(pages[i]);
     i = -EFAULT;
     goto out;
    }
    flush_dcache_page(pages[i]);
    if (!PageReserved(pages[i]))
     page_cache_get(pages[i]);
   }
   if (vmas)//这个也不可能成立
    vmas[i] = vma;
   i++;
   start += PAGE_SIZE;//指向下一个线性起始地址
   len--;
  } while(len && start < vma->vm_end);
  spin_unlock(&mm->page_table_lock);
 } while(len);//如果一个区域还是映射不完的话,我们就要用下一个区域来映射。
out:
 return i;//这里是统计我们成功分配了多少个物理内存页与线性地址构成了映射。
}
handle_mm_fault()是缺页处理函数,作用就是分配物理页。我们来看下它的代码吧
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
 unsigned long address, int write_access)
{
 pgd_t *pgd;
 pmd_t *pmd;

 __set_current_state(TASK_RUNNING);//设置当前进程处于正在运行状态
 pgd = pgd_offset(mm, address);//我们通过这个线性地址求得对应于一级链表的某个页表项,pgd就是指向这项的指针。

 inc_page_state(pgfault);//当前处理器的页状态结构的缺页计数器加1.per_cpu_page_states.pgfault+1

 if (is_vm_hugetlb_page(vma))//不考虑
  return VM_FAULT_SIGBUS;
 spin_lock(&mm->page_table_lock);
 pmd = pmd_alloc(mm, pgd, address);//pmd=pgd

 if (pmd) {//这里肯定要执行的
  pte_t * pte = pte_alloc_map(mm, pmd, address);//我们首先判断一级描述符是否为0,如果为空的话我们先调用函数pte_alloc_one()函数为struct page申请1页内存空间,接着我们再次判断一级页表是否为空,如果为空就是正常,我们继续往下看,我们会看到mm->nr_ptes++,其实nr_ptes表示当前进程的二级页表所占页数是多少。我们接着调用函数inc_page_state()表示当前处理器的页状态结构中的页表页数,即per_cpu_page_states.nr_page_table_pages加1.pmd_populate()这个函数就是设置address所在2m内存双段的以及描述符,最后返回二级描述符的地址,其实就是address对应二级描述符表的条目地址。
  if (pte)//如果找到对应的条目后,我们调用以下函数,进行物理映射。
   return handle_pte_fault(mm, vma, address, write_access, pte, pmd);
 }
 spin_unlock(&mm->page_table_lock);
 return VM_FAULT_OOM;//返回这个,表示不可以为页表分配内存。
}
接下来,我们建立好页表后,我们就要进行物理映射了,也就是在这个条目里存放着对应物理页的地址之类的东西。我们来看看这个函数吧。

static inline int handle_pte_fault(struct mm_struct *mm,
 struct vm_area_struct * vma, unsigned long address,
 int write_access, pte_t *pte, pmd_t *pmd)
{
 pte_t entry;

 entry = *pte;
 if (!pte_present(entry)) {//如果二级描述符没有设置L_PTE_PRESENT标志的话,说明这个页表项没效,就是说address还没有被映射的。
  if (pte_none(entry))//如果为真,说明该页从未被进程访问过。
   return do_no_page(mm, vma, address, write_access, pte, pmd);//从新申请一页物理内存,,将adress所指线性地址映射到该页上。
  if (pte_file(entry))//如果设置了这个标志,说明该页属于非线性磁盘文件的映射。
   return do_file_page(mm, vma, address, write_access, pte, pmd);//由于描述符不为零,但是没有设置L_PTE_PRESENT标志,那么该页就是已经被换出了,我们要把它换回来。
  return do_swap_page(mm, vma, address, pte, pmd, entry, write_access);
 }

//如果描述符设置了L_PTE_PRESENT的话,说明线性地址已经映射到物理内存了。

 if (write_access) {//如果线性区域可写的话
  if (!pte_write(entry))//如果没有在二级描述符里设置L_PTE_WRITE标志的话,说明对应的物理页还是不可写的。
   return do_wp_page(mm, vma, address, pte, pmd, entry);//进行COW(写时复制)操作

  entry = pte_mkdirty(entry);
 }
 entry = pte_mkyoung(entry);
 ptep_set_access_flags(vma, address, pte, entry, write_access);//设置pte所指位置的二级描述符条目(调用函数sat_pte()),刷新address所指线性区域的TLB。
 update_mmu_cache(vma, address, entry);//处理器MMU与CACHE可能导致的存储一致性问题。
 pte_unmap(pte);
 spin_unlock(&mm->page_table_lock);
 return VM_FAULT_MINOR;//这里返回的是次缺页标志,其实就是说明没有阻碍到当前进程的运行。而我们会在后面提到的VM_FAULT_MAJOR标志就是阻碍了当前进程,迫使了进程睡眠了。我们叫这种为主缺页。
}



 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值