前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux进程的内存管理之缺页异常

Linux进程的内存管理之缺页异常

作者头像
刘盼
发布2021-04-13 14:25:08
2.5K1
发布2021-04-13 14:25:08
举报
文章被收录于专栏:人人都是极客人人都是极客

通过《Linux进程的内存管理之malloc和mmap》我们知道,这两个函数只是建立了进程的vma,但还没有建立虚拟地址和物理地址的映射关系。

当进程访问这些还没建立映射关系的虚拟地址时,处理器会自动触发缺页异常。

ARM64把异常分为同步异常和异步异常,通常异步异常指的是中断(可看《上帝视角看中断》),同步异常指的是异常。关于ARM异常处理的文章可参考《ARMv8异常处理简介》。

ARM64处理

当处理器有异常发生时,处理器会先跳转到ARM64的异常向量表中:

代码语言:javascript
复制
ENTRY(vectors)
 kernel_ventry 1, sync_invalid   // Synchronous EL1t
 kernel_ventry 1, irq_invalid   // IRQ EL1t
 kernel_ventry 1, fiq_invalid   // FIQ EL1t
 kernel_ventry 1, error_invalid  // Error EL1t

 kernel_ventry 1, sync    // Synchronous EL1h
 kernel_ventry 1, irq    // IRQ EL1h
 kernel_ventry 1, fiq_invalid   // FIQ EL1h
 kernel_ventry 1, error_invalid  // Error EL1h

 kernel_ventry 0, sync    // Synchronous 64-bit EL0
 kernel_ventry 0, irq    // IRQ 64-bit EL0
 kernel_ventry 0, fiq_invalid   // FIQ 64-bit EL0
 kernel_ventry 0, error_invalid  // Error 64-bit EL0

#ifdef CONFIG_COMPAT
 kernel_ventry 0, sync_compat, 32  // Synchronous 32-bit EL0
 kernel_ventry 0, irq_compat, 32  // IRQ 32-bit EL0
 kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
 kernel_ventry 0, error_invalid_compat, 32 // Error 32-bit EL0
#else
 kernel_ventry 0, sync_invalid, 32  // Synchronous 32-bit EL0
 kernel_ventry 0, irq_invalid, 32  // IRQ 32-bit EL0
 kernel_ventry 0, fiq_invalid, 32  // FIQ 32-bit EL0
 kernel_ventry 0, error_invalid, 32  // Error 32-bit EL0
#endif
END(vectors)

以el1下的异常为例,当跳转到el1_sync函数时,读取ESR的值以判断异常类型。根据类型跳转到不同的处理函数里,如果是data abort的话跳转到el1_da函数里,instruction abort的话跳转到el1_ia函数里:

代码语言:javascript
复制
el1_sync:
 kernel_entry 1
 mrs x1, esr_el1   // read the syndrome register
 lsr x24, x1, #ESR_ELx_EC_SHIFT // exception class
 cmp x24, #ESR_ELx_EC_DABT_CUR // data abort in EL1
 b.eq el1_da
 cmp x24, #ESR_ELx_EC_IABT_CUR // instruction abort in EL1
 b.eq el1_ia
 cmp x24, #ESR_ELx_EC_SYS64  // configurable trap
 b.eq el1_undef
 cmp x24, #ESR_ELx_EC_SP_ALIGN // stack alignment exception
 b.eq el1_sp_pc
 cmp x24, #ESR_ELx_EC_PC_ALIGN // pc alignment exception
 b.eq el1_sp_pc
 cmp x24, #ESR_ELx_EC_UNKNOWN // unknown exception in EL1
 b.eq el1_undef
 cmp x24, #ESR_ELx_EC_BREAKPT_CUR // debug exception in EL1
 b.ge el1_dbg
 b el1_inv

流程图如下:

代码语言:javascript
复制
asmlinkage void __exception do_mem_abort(unsigned long addr, unsigned int esr,
      struct pt_regs *regs)
{
 const struct fault_info *inf = esr_to_fault_info(esr);
 struct siginfo info;

 if (!inf->fn(addr, esr, regs))
  return;

 pr_alert("Unhandled fault: %s (0x%08x) at 0x%016lx\n",
   inf->name, esr, addr);

 mem_abort_decode(esr);

 info.si_signo = inf->sig;
 info.si_errno = 0;
 info.si_code  = inf->code;
 info.si_addr  = (void __user *)addr;
 arm64_notify_die("", regs, &info, esr);
}

该函数主要是根据传进来的esr获取fault_info信息,从而去调用函数inf->fn.

代码语言:javascript
复制
static const struct fault_info fault_info[] = {
 { do_bad,  SIGBUS,  0,  "ttbr address size fault" },
 { do_bad,  SIGBUS,  0,  "level 1 address size fault" },
 { do_bad,  SIGBUS,  0,  "level 2 address size fault" },
 { do_bad,  SIGBUS,  0,  "level 3 address size fault" },
 { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 0 translation fault" },
 { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 1 translation fault" },
 { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 2 translation fault" },
 { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 3 translation fault" },
 { do_bad,  SIGBUS,  0,  "unknown 8"   },
 { do_page_fault, SIGSEGV, SEGV_ACCERR, "level 1 access flag fault" },
 { do_page_fault, SIGSEGV, SEGV_ACCERR, "level 2 access flag fault" },
 { do_page_fault, SIGSEGV, SEGV_ACCERR, "level 3 access flag fault" },
  ......
}
  • do_translation_fault: 出现0/1/2/3级页表转换错误时调用
  • do_page_fault:出现1/2/3级页表访问权限时调用
  • do_bad:其它错误

以do_translation_fault为例:

代码语言:javascript
复制
static int __kprobes do_translation_fault(unsigned long addr,
       unsigned int esr,
       struct pt_regs *regs)
{
 if (addr < TASK_SIZE)
  return do_page_fault(addr, esr, regs); //用户空间

 do_bad_area(addr, esr, regs); //内核空间或非法空间
 return 0;
}

do_page_fault

主要调用__do_page_fault

代码语言:javascript
复制
static int __do_page_fault(struct mm_struct *mm, unsigned long addr,
      unsigned int mm_flags, unsigned long vm_flags,
      struct task_struct *tsk)
{
 struct vm_area_struct *vma;
 int fault;

 vma = find_vma(mm, addr);
 fault = VM_FAULT_BADMAP; //没有找到vma区域,说明addr还没有在进程的地址空间中
 if (unlikely(!vma))
  goto out;
 if (unlikely(vma->vm_start > addr))
  goto check_stack;

 /*
  * Ok, we have a good vm_area for this memory access, so we can handle
  * it.
  */
good_area://一个好的vma
 /*
  * Check that the permissions on the VMA allow for the fault which
  * occurred.
  */
 if (!(vma->vm_flags & vm_flags)) {//权限检查
  fault = VM_FAULT_BADACCESS; 
  goto out;
 }

 //重新建立物理页面到VMA的映射关系
 return handle_mm_fault(vma, addr & PAGE_MASK, mm_flags);

check_stack:
 if (vma->vm_flags & VM_GROWSDOWN && !expand_stack(vma, addr))
  goto good_area;
out:
 return fault;
}

从__do_page_fault函数能看出来,当触发异常的虚拟地址属于某个vma,并且拥有触发页错误异常的权限时,会调用到handle_mm_fault函数来建立vma和物理地址的映射,而handle_mm_fault函数的主要逻辑是通过__handle_mm_fault来实现的。

__handle_mm_fault

代码语言:javascript
复制
static int __handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
  unsigned int flags)
{
  ......
 //查找页全局目录,获取地址对应的表项
 pgd = pgd_offset(mm, address);
 //查找页四级目录表项,没有则创建
 p4d = p4d_alloc(mm, pgd, address);
 if (!p4d)
  return VM_FAULT_OOM;

 //查找页上级目录表项,没有则创建
 vmf.pud = pud_alloc(mm, p4d, address);
 ......
 //查找页中级目录表项,没有则创建
 vmf.pmd = pmd_alloc(mm, vmf.pud, address);
  ......
 //处理pte页表
 return handle_pte_fault(&vmf);
}
代码语言:javascript
复制
static int handle_pte_fault(struct vm_fault *vmf)
{
  ......
 //页表项不存在
 if (!vmf->pte) {
  //判断是否是匿名页
  if (vma_is_anonymous(vmf->vma))
   //处理匿名页   
   return do_anonymous_page(vmf); //malloc/mmap分配了vma,但是没有进行映射处理,在首次访问时触发
  else
   //处理文件页,匿名共享页
   return do_fault(vmf); 
 }

 //页表项存在,但页不在内存中
 if (!pte_present(vmf->orig_pte))
  return do_swap_page(vmf);

 if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
  return do_numa_page(vmf); //NUMA自动平衡处理
  ......
 if (vmf->flags & FAULT_FLAG_WRITE) {
  if (!pte_write(entry))
   return do_wp_page(vmf); //页在内存中,但是没有写权限位,写时复制
  entry = pte_mkdirty(entry);
 }
  ......
}

do_anonymous_page

匿名页缺页异常,对于匿名映射,映射完成之后,只是获得了一块虚拟内存,并没有分配物理内存,当第一次访问的时候:

  1. 如果是读访问,会将虚拟页映射到0页,以减少不必要的内存分配
  2. 如果是写访问,用alloc_zeroed_user_highpage_movable分配新的物理页,并用0填充,然后映射到虚拟页上去
  3. 如果是先读后写访问,则会发生两次缺页异常:第一次是匿名页缺页异常的读的处理(虚拟页到0页的映射),第二次是写时复制缺页异常处理。

从上面的总结我们知道,第一次访问匿名页时有三种情况,其中第一种和第三种情况都会涉及到0页

代码语言:javascript
复制
/*
 * Empty_zero_page is a special page that is used for zero-initialized data
 * and COW.
 */
unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)] __page_aligned_bss;
EXPORT_SYMBOL(empty_zero_page);

可以看到定义了一个全局变量,大小为一页,页对齐到 bss 段,所有这段数据内核初始化的时候会被清零,所有称之为0页

下面我们结合代码看下匿名页如何针对上面三种情况操作:

1. 第一次读匿名页情况

代码语言:javascript
复制
 //是否为写内存导致的缺页异常
 if (!(vmf->flags & FAULT_FLAG_WRITE) &&
   !mm_forbids_zeropage(vma->vm_mm)) {
  //异常由读操作触发,并允许使用0页。设置页表项的值映射到0页。
  entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
      vma->vm_page_prot));
  vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
    vmf->address, &vmf->ptl);
  if (!pte_none(*vmf->pte))
   goto unlock;
  ret = check_stable_address_space(vma->vm_mm);
  if (ret)
   goto unlock;
  /* Deliver the page fault to userland, check inside PT lock */
  if (userfaultfd_missing(vma)) {
   pte_unmap_unlock(vmf->pte, vmf->ptl);
   return handle_userfault(vmf, VM_UFFD_MISSING);
  }
  goto setpte;
 }
  ......
setpte:
 //设置页表项
 set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);

 /* No need to invalidate - it was non-present before */
 //更新cpu的tlb页表缓存
 update_mmu_cache(vma, vmf->address, vmf->pte);

pte_mkspecial 是主要函数,设置页表项的值映射到0页。my_zero_pfn就是内核初始化设置的empty_zero_page这个0页得到页帧号。

2. 第一次写匿名页的情况

代码语言:javascript
复制
 //分配一个高端可迁移的被0填充的物理页
 page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
 if (!page)
  goto oom;
  ......
 //设置page的标志位,PG_uptodate
 __SetPageUptodate(page);

 //根据vma的权限位和page描述符,生成页表项
 entry = mk_pte(page, vma->vm_page_prot);
 if (vma->vm_flags & VM_WRITE)
  //如果有写权限,设置页表项值为脏且可写
  entry = pte_mkwrite(pte_mkdirty(entry));
  ......
 //增加匿名页面计数
 inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
 //建立物理页到虚拟页的反向映射,添加到rmap链表中
 page_add_new_anon_rmap(page, vma, vmf->address, false);
 mem_cgroup_commit_charge(page, memcg, false, false);
 //把物理页添加到lru链表中
 lru_cache_add_active_or_unevictable(page, vma);

通过alloc_zeroed_user_highpage_movable分配物理页面

3. 读之后写匿名页

见 do_wp_page

do_fault

文件页缺页异常

do_swap_page

上面已经讲过,pte对应的内容不为0(页表项存在),但是pte所对应的page不在内存中时,表示此时pte的内容所对应的页面在swap空间中,缺页异常时会通过do_swap_page()函数来分配页面。

在讲do_swap_page之前,我们先看下什么是swap cache?

由于内存和磁盘的读写性能差异较大,Linux会在内存充裕时将空闲内存当作swap cache,用来缓存磁盘数据,以提高I/O性能。相对的在内存紧张时Linux会将这些缓存回收,将脏页回写到磁盘中。

内核中使用swap_info_struct结构体来管理swap分区,一个swap_info_struct对应一个swap分区。如下图所示,swap分区内部会以page大小为单位划分出多个swap slot,同时通过swap_map对每个slot的使用情况进行记录,为0代表空闲,大于0则代表该slot被map的进程数量。

发生内存回收时,一个内存页会被回收到一个slot中,同时会修改pte内容为slot对应的swp_entry_t。swp_entry_t是一个64位的变量,其结构如下图所示,其中2-7位存放swap分区的type,8-57位存放slot在分区内的offset。内存换入时,通过pte的内容即可在磁盘上找到对应的slot。

正式进入do_swap_page

do_swap_page发生在swap in的时候,即查找磁盘上的slot,并将数据读回。

换入的过程如下:

  1. 查找swap cache中是否存在所查找的页面,如果存在,则根据swap cache引用的内存页,重新映射并更新页表;如果不存在,则分配新的内存页,并添加到swap cache的引用中,更新内存页内容完成后,更新页表。
  2. 换入操作结束后,对应swap area的页引用减1,当减少到0时,代表没有任何进程引用了该页,可以进行回收。
代码语言:javascript
复制
int do_swap_page(struct vm_fault *vmf)
{
  ......
 //根据pte找到swap entry, swap entry和pte有一个对应关系
 entry = pte_to_swp_entry(vmf->orig_pte);
  ......
 if (!page)
  //根据entry从swap缓存中查找页, 在swapcache里面寻找entry对应的page
  //Lookup a swap entry in the swap cache
  page = lookup_swap_cache(entry, vma_readahead ? vma : NULL,
      vmf->address);
 //没有找到页
 if (!page) {
  if (vma_readahead)
   page = do_swap_page_readahead(entry,
    GFP_HIGHUSER_MOVABLE, vmf, &swap_ra);
  else
   //如果swapcache里面找不到就在swap area里面找,分配新的内存页并从swap area中读入
   page = swapin_readahead(entry,
    GFP_HIGHUSER_MOVABLE, vma, vmf->address);
  ......
 //获取一个pte的entry,重新建立映射
 vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
   &vmf->ptl);
  ......
 //anonpage数加1,匿名页从swap空间交换出来,所以加1
 //swap page个数减1,由page和VMA属性创建一个新的pte
 inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
 dec_mm_counter_fast(vma->vm_mm, MM_SWAPENTS);
 pte = mk_pte(page, vma->vm_page_prot);
  ......
 flush_icache_page(vma, page);
 if (pte_swp_soft_dirty(vmf->orig_pte))
  pte = pte_mksoft_dirty(pte);
 //将新生成的PTE entry添加到硬件页表中
 set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);
 vmf->orig_pte = pte;
 //根据page是否为swapcache
 if (page == swapcache) {
  //如果是,将swap缓存页用作anon页,添加反向映射rmap中
  do_page_add_anon_rmap(page, vma, vmf->address, exclusive);
  mem_cgroup_commit_charge(page, memcg, true, false);
  //并添加到active链表中
  activate_page(page);
 //如果不是
 } else { /* ksm created a completely new copy */
  //使用新页面并复制swap缓存页,添加反向映射rmap中
  page_add_new_anon_rmap(page, vma, vmf->address, false);
  mem_cgroup_commit_charge(page, memcg, false, false);
  //并添加到lru链表中
  lru_cache_add_active_or_unevictable(page, vma);
 }

 //释放swap entry
 swap_free(entry);
  ......
 if (vmf->flags & FAULT_FLAG_WRITE) {
  //有写请求则写时复制
  ret |= do_wp_page(vmf);
  if (ret & VM_FAULT_ERROR)
   ret &= VM_FAULT_ERROR;
  goto out;
 }
  ......
  return ret;
}

do_wp_page

走到这里说明页面在内存中,只是PTE只有读权限,而又要写内存的时候就会触发do_wp_page。

do_wp_page函数用于处理写时复制(copy on write),其流程比较简单,主要是分配新的物理页,拷贝原来页的内容到新页,然后修改页表项内容指向新页并修改为可写(vma具备可写属性)。

代码语言:javascript
复制
static int do_wp_page(struct vm_fault *vmf)
 __releases(vmf->ptl)
{
 struct vm_area_struct *vma = vmf->vma;

 //从页表项中得到页帧号,再得到页描述符,发生异常时地址所在的page结构
 vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
 if (!vmf->page) {
  //没有page结构是使用页帧号的特殊映射
  /*
   * VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a
   * VM_PFNMAP VMA.
   *
   * We should not cow pages in a shared writeable mapping.
   * Just mark the pages writable and/or call ops->pfn_mkwrite.
   */
  if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
         (VM_WRITE|VM_SHARED))
   //处理共享可写映射
   return wp_pfn_shared(vmf);

  pte_unmap_unlock(vmf->pte, vmf->ptl);
  //处理私有可写映射
  return wp_page_copy(vmf);
 }

 /*
  * Take out anonymous pages first, anonymous shared vmas are
  * not dirty accountable.
  */
 if (PageAnon(vmf->page) && !PageKsm(vmf->page)) {
  int total_map_swapcount;
  if (!trylock_page(vmf->page)) {
   //添加原来页的引用计数,方式被释放
   get_page(vmf->page);
   //释放页表锁
   pte_unmap_unlock(vmf->pte, vmf->ptl);
   lock_page(vmf->page);
   vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
     vmf->address, &vmf->ptl);
   if (!pte_same(*vmf->pte, vmf->orig_pte)) {
    unlock_page(vmf->page);
    pte_unmap_unlock(vmf->pte, vmf->ptl);
    put_page(vmf->page);
    return 0;
   }
   put_page(vmf->page);
  }
  //单身匿名页面的处理
  if (reuse_swap_page(vmf->page, &total_map_swapcount)) {
   if (total_map_swapcount == 1) {
    /*
     * The page is all ours. Move it to
     * our anon_vma so the rmap code will
     * not search our parent or siblings.
     * Protected against the rmap code by
     * the page lock.
     */
    page_move_anon_rmap(vmf->page, vma);
   }
   unlock_page(vmf->page);
   wp_page_reuse(vmf);
   return VM_FAULT_WRITE;
  }
  unlock_page(vmf->page);
 } else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
     (VM_WRITE|VM_SHARED))) {
  //共享可写,不需要复制物理页,设置页表权限即可
  return wp_page_shared(vmf);
 }

 /*
  * Ok, we need to copy. Oh, well..
  */
 get_page(vmf->page);

 pte_unmap_unlock(vmf->pte, vmf->ptl);
 //私有可写,复制物理页,将虚拟页映射到物理页
 return wp_page_copy(vmf);
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-03-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 人人都是极客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ARM64处理
  • do_page_fault
  • __handle_mm_fault
  • do_anonymous_page
  • do_fault
  • do_swap_page
    • 在讲do_swap_page之前,我们先看下什么是swap cache?
      • 正式进入do_swap_page
      • do_wp_page
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档