18 请求调页

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

上一篇博文引出了“请求调页”技术,术语“请求调页”指的是一种动态内存分配技术,它把页框的分配推迟到不能再推迟为止,也就是说,一直推迟到进程要访问的页不在物理RAM中时为止,由此引起一个缺页异常。

 

请求调页技术背后的动机是:进程开始运行的时候并不访问其线性地址空间中的全部地址。

 

事实上,有一部分地址也许永远不被进程使用。此外,程序的局部性原理保证了在程序执行的每个阶段,真正引用的进程页只有一小部分,因此临时用不着的页所在的页框可以由其他进程来使用。因此,对于全局分配(一开始就给进程分配所需要的全部页框,直到程序结束才释放这些页框)来说,请求调页是首选的,因为它增加了系统中的空闲页框的平均数,从而更好地利用空闲内存。从另一个观点来看,在RAM总数保持不变的情况下,请求调页从总体上能使系统有更大的吞吐量。

 

为这一切优点付出的代价是系统额外的开销:由请求调页所引发的每个“缺页”异常必须由内核处理,这将浪费CPU的时钟周期。幸运的是,局部性原理保证了一旦进程开始在一组页上运行,在接下来相当长的一段时间内它会一直停留在这些页上而不去访问其他的页,这样我们就可以认为“缺页”异常是一种稀有事件。

 

稀有归稀有,下面,我们就来仔细研究一下缺页异常的最后一步,也就是推迟得不能再推迟得那一步,请求调页。

 

接着上一篇博文handle_mm_fault中调用handle_pte_fault函数:
handle_pte_fault(mm, vma, address, pte, pmd, write_access);


参数mm是发生缺页的进程的mm_struct数据结构,address是发生缺页异常的那个线性地址,vma是address所在的线性区,pte和pmd是对应的页表和页中间目录,write_access是写标记,在handle_mm_fault表示该页是否可写,是则为1,不是则为0。

//mm/Memory.c
static inline int handle_pte_fault(struct mm_struct *mm,
  struct vm_area_struct *vma, unsigned long address,
  pte_t *pte, pmd_t *pmd, int write_access)
{
 pte_t entry;
 pte_t old_entry;
 spinlock_t *ptl;

 old_entry = entry = *pte;
 if (!pte_present(entry)) {
  if (pte_none(entry)) {
   if (vma->vm_ops) {
    if (vma->vm_ops->nopage)
     return do_no_page(mm, vma, address,
         pte, pmd,
         write_access);
    if (unlikely(vma->vm_ops->nopfn))
     return do_no_pfn(mm, vma, address, pte,
        pmd, write_access);
   }
   return do_anonymous_page(mm, vma, address,
       pte, pmd, write_access);
  }
  if (pte_file(entry))
   return do_file_page(mm, vma, address,
     pte, pmd, write_access, entry);
  return do_swap_page(mm, vma, address,
     pte, pmd, write_access, entry);
 }

 ptl = pte_lockptr(mm, pmd);
 spin_lock(ptl);
 if (unlikely(!pte_same(*pte, entry)))
  goto unlock;
 if (write_access) {
  if (!pte_write(entry))
   return do_wp_page(mm, vma, address,
     pte, pmd, ptl, entry);
  entry = pte_mkdirty(entry);
 }
 entry = pte_mkyoung(entry);
 if (!pte_same(old_entry, entry)) {
  ptep_set_access_flags(vma, address, pte, entry, write_access);
  update_mmu_cache(vma, address, entry);
  lazy_mmu_prot_update(entry);
 } else {
  /*
   * This is needed only for protection faults but the arch code
   * is not yet telling us if this is a protection fault or not.
   * This still avoids useless tlb flushes for .text page faults
   * with threads.
   */
  if (write_access)
   flush_tlb_page(vma, address);
 }
unlock:
 pte_unmap_unlock(pte, ptl);
 return VM_FAULT_MINOR;
}


我们说发生缺页异常的时机是被访问的页可能不在主存中,其原因或者是进程从没访问过该页,或者是内核已经回收了相应的页框。在这两种情况下,缺页处理程序必须为进程分配新的页框。

 

不过,如何初始化这个页框取决于是哪一种页以及页以前是否被进程访问过。特殊情况下:
1.这个页从未被进程访问到且没有映射磁盘文件,或者页映射了磁盘文件。内核能够识别这些情况,它根据页表相应的表项被填充为0,也就是说,pte_none宏返回1。
2.页属于非线性磁盘文件的映射。内核能够识别这种情况,因为Present标志被清0而且Dirty标志被置1,也就是说,pte_file宏返回1。
3.进程已经访问过这个页,但是其内容被临时保存在磁盘上。内核能够识别这种情况,这是因为相应表项没被填充为0,但是Present和Dirty标志被清0。

 

因此,handle_pte_fault()函数通过检查address对应的页表项能够区分这三种情况:
 pte_t old_entry = entry = *pte;
 if (!pte_present(entry)) { /* 页表项pte的Present标志被请0,说明缺页 */
  if (pte_none(entry)) {  /* pte_none宏返回1说明:情况1 */
   if (vma->vm_ops) {
    if (vma->vm_ops->nopage) /* 还记得我们在“线性区数据结构”博文中提到的那些线性区处理方法吧 */
     return do_no_page(mm, vma, address,
         pte, pmd,
         write_access);
    if (unlikely(vma->vm_ops->nopfn))/* 首选nopage方法如果没有,就选nopfn方法 */
     return do_no_pfn(mm, vma, address, pte,
        pmd, write_access);
   }/* 如果没有vma->vm_ops方法,就只想匿名调页函数 */
   return do_anonymous_page(mm, vma, address,
       pte, pmd, write_access);
  }
  if (pte_file(entry)) /* pte_file宏返回1说明:情况2 */
   return do_file_page(mm, vma, address,
     pte, pmd, write_access, entry);
  /* 情况3 */
  return do_swap_page(mm, vma, address,
     pte, pmd, write_access, entry);
 }

 

我们将在内存映射和页框回收博文分别讨论第2和第3种情况。

 

下面详细说说第1种情况。我们先回忆一下关于页表项的东西,页表项由数据结构pte_t,在没有激活PAE的情况下它是:
typedef struct { unsigned long pte_low; } pte_t;

 

对,32位无符号整数,高20位当然就是页表中存在的页号,低12位则是相关的标志,如最低的一位是Present标志,博文中讲得很清楚了。那么:
#define pte_none(x)  (!(x).pte_low)

 

如果pte_none返回1,则说明pte_t完全就没有内容,则就是我们说的第一种情况,即这个页从未被进程访问到且没有映射磁盘文件,或者页映射了磁盘文件。糊涂了吧,别急,接着往下看。

 

当页从未被访问或页线性地映射磁盘文件时则调用do_no_page()函数。有两种方法装入所缺的页,这取决于这个页是否被映射到一个磁盘文件。该函数通过检查vma线性区描述符操作的vm_operations_struct结构中的nopage字段来确定这一点,如果页被映射到一个文件,nopage字段就指向一个函数,该函数把所缺的页从磁盘装人到RAM。因此,可能的情况是:

(1)vma->vm_ops->nopage字段不为NULL。在这种情况下,线性区映射了一个磁盘文件,nopage字段指向装入页的函数。这种情况将在“内存映射的请求调页”一博中进行阐述。这里只简单地提一下:
new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, &ret);

(2)或者vma->vm_ops字段为NULL,或者vma->vm_ops->nopage字段为NULL。在这种情况下,线性区没有映射磁盘文件,也就是说,它是一个匿名映射(anonymous mapping)。因此,do_no_page()调用do_anonymous_page()函数获得一个新的页框:
return do_anonymous_page(mm, vma, address, pte, pmd, write_access);

 

do_anonymous_page()函数(为了简化对这个函数的说明,我们略过处理反映射的语句,有关反映射的内容见“反向映射”博文。)分别处理写请求和读请求:

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
  unsigned long address, pte_t *page_table, pmd_t *pmd,
  int write_access)
{
 struct page *page;
 spinlock_t *ptl;
 pte_t entry;

 if (write_access) {
  /* Allocate our own private page. */
  pte_unmap(page_table);  /* 释放pte一种临时内核映射 */

  if (unlikely(anon_vma_prepare(vma)))
   goto oom;
  page = alloc_zeroed_user_highpage(vma, address);
  if (!page)
   goto oom;

  entry = mk_pte(page, vma->vm_page_prot);
  entry = maybe_mkwrite(pte_mkdirty(entry), vma);

  page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
  if (!pte_none(*page_table))
   goto release;
  inc_mm_counter(mm, anon_rss);
  lru_cache_add_active(page);
  page_add_new_anon_rmap(page, vma, address);
 } else {
  /* Map the ZERO_PAGE - vm_page_prot is readonly */
  page = ZERO_PAGE(address);
  page_cache_get(page);
  entry = mk_pte(page, vma->vm_page_prot);

  ptl = pte_lockptr(mm, pmd);
  spin_lock(ptl);
  if (!pte_none(*page_table))
   goto release;
  inc_mm_counter(mm, file_rss);
  page_add_file_rmap(page);
 }

 set_pte_at(mm, address, page_table, entry);

 /* No need to invalidate - it was non-present before */
 update_mmu_cache(vma, address, entry);
 lazy_mmu_prot_update(entry);
unlock:
 pte_unmap_unlock(page_table, ptl);
 trace_mm_anon_fault(mm, address, page);
 return VM_FAULT_MINOR;
release:
 page_cache_release(page);
 goto unlock;
oom:
 return VM_FAULT_OOM;
}

 

函数根据write_access指向不同的程序段,首选,如果write_access为1,则:

pte_unmap宏的第一次执行释放一种临时内核映射,这种映射了在调用handle_pte_fault()函数之前由pte_offset_map宏所建立页表项的高端内存物理地址(参见“高端内存映射”一博中的表)。pte_offset_map和pte_unmap这对宏获取和释放同一个临时内核映射。临时内核映射必须在调用alloc_zeroed_user_highpage,本质上也就是alloc_page()之前释放,因为这个函数可能会阻塞当前进程。

 

分配一个新的页面:
page = alloc_zeroed_user_highpage(vma, address);
#define alloc_zeroed_user_highpage(vma, vaddr) alloc_page_vma(GFP_HIGHUSER | __GFP_ZERO, vma, vaddr)
#define alloc_page_vma(gfp_mask, vma, addr) alloc_pages(gfp_mask, 0)

 

相应的页表项设置为页框的物理地址,除了根据vma->vm_page_prot设置相应页面的权限外,页表框被标记为既脏又可写的(pte_mkdirty(entry)。如果调试程序试图往被跟踪进程只读线性区中的页中写数据,内核就不设置Read/Write标志。函数maybe_mkwrite()处理这种特殊情况。):
entry = mk_pte(page, vma->vm_page_prot);
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
#define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))
#define pfn_pte(pfn, prot) __pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot))
#define __pte(x) ((pte_t) { (x) } )

 

函数递增内存描述符的rss字段以记录分配给进程的页框总数:
inc_mm_counter(mm, anon_rss);

 

lru_cache_add_active()函数把新页框插入与交换相关的数据结构中,我们在后面博文对它进行说明。

 

相反,当处理读访问时,即write_access为0,页的内容是无关紧要的,因为进程第一次对它访问。给进程一个填充为0的页要比给它一个由其他进程填充了信息的旧页更为安全。Linux在请求调页方面做得更深入一些。没有必要立即给进程分配一个填充为0的新页框,由于我们也可以给它一个现有的称为零页(zero page)的页,这样可以进一步推迟页框的分配。零页在内核初始化期间被静态分配,并存放在empty_zero_page变量中(长为4096字节的数组,并用0填充):
#define ZERO_PAGE(vaddr) (virt_to_page(empty_zero_page))
extern unsigned long empty_zero_page[1024];
#define virt_to_page(kaddr) pfn_to_page(__pa(kaddr) >> PAGE_SHIFT)
#define __pa(x)   ((unsigned long)(x)-PAGE_OFFSET)


再提醒一下,旧知识,给我记牢咯!:
#define PAGE_OFFSET  0xc0000000
#define PAGE_SHIFT 12
struct page *pfn_to_page(unsigned long pfn)
{
 return __pfn_to_page(pfn);
}
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET))

 

由于这个页被标记为不可写的,因此如果进程试图写这个页,则写时复制机制被激活。当且仅当在这个时候,进程才获得一个属于自己的页并对它进行写操作。究竟写时复制是个啥东东,且听下回分解。


原创粉丝点击