oklinux arm pagefault 流程简单分析

ARM 184浏览

  oklinux arm pagefault 流程简单分析 (转载请注名出处)
  **************************************************************************    
       首先还是看下native linux 关于 pagefault 的执行流程,在native linux 中(下面
  简称nlinux) ,发生和pagefault 相关的异常一般是data abort ,prefetch abort ,
  网上关于arm linux 异常的分析的文章很多,这里先简单分析下,到user空间取参数情况的
  pagefault 流程, 这是因为 syscall从user 空间取参数总是按1page 去取,如果取到不存在的
  数据,就发生data abort异常, 因为get user 是内核态,所以异常发生前在svc mode,
  那么到entry-armv.S 就执行到__dabt_svc, 然后svc_entry 中 将需要的现场压栈保护,
  然后执行CPU_DABORT_HANDLER,  获得 address of abort, FSR 来做为 do_DataAbort
  执行的参数, 进入do_DataAbort , 其中根据fsr 判断 ,目前我们的情况是,page
  translation fault(就是线性地址无效), 然后查表执行do_page_fault=>
  __do_page_fault,查找vma ,没有该虚拟内存区,然后返回到do_page_fault,
  因为是kernel mode ,所以__do_kernel_fault,在fixup_exception后修正
  栈上arm pc 到执行发生异常的下一条指令, 返回__dabt_svc ,继续执行,然后
  __dabt_svc 最后一条指令是 ldmia sp, {r0 - pc}^ ,将arm pc 值
  load 到pc ,就完成了异常处理,回到syscall 中继续执行下去.
      COW的异常也是从data abort 发生,然后 page translation fault,但是属于good area,
  通过handle_mm_fault =>handle_pte_fault => if (!pte_present(entry)) {   不满足
  的那个分支执行,分配新页面,然后一路返回到继续执行发生异常的指令
      另外prefetch abort 情况发生在,当执行一条预取时发生异常,这可能是可执行代码
  被换页换出,不在物理内存中, 根据内核代码总是在内存的原则,一般内核线程访问非连续
  区,才可能发生异常,这属于另外一种情况,这里简单分析用户程序执行发生prefetch 异常
  的情况, 发生异常后执行到entry-armv.S  __pabt_usr: ,和上面基本相似,usr_entry
  保护现场后,取PC15 的IFA(instruction fault addr) ,做为参数传入do_PrefetchAbort
  =>do_translation_fault,因为在user 空间,所以fault addr   ,那么走good_area分支,分配一页,然后调用file system 的read page 读入1页,如果分配有错误
   根据handle_mm_fault 返回的fault 值,判断,并且如果是应用程序,走到__do_user_fault,
   给app 发信号 ,如果一切顺利,回到__pabt_usr:  => ret_to_user (entry-common.S),
   如果没有调度需要,就继续执行发生异常的用户进程,恢复现场(出栈),然后movs pc,lr
   ,将arm pc 的值load pc ,然后恢复 cpsr ,跳到异常发生指令从新执行
      上面基本是nlinux page fault 异常的基本流程分析, 关于栈变化的过程等细节,
   网上有很多文章,
  
     
       现在来看下oklinux page fault  的处理过程:
   在oklinux 中发生pagefault 的异常,入口也是data abort ,prefetch abort,但是处理
   arm 异常的代码不在linux kernel ,而在okl4  kernel 的Traps.spp 中:
    arm_prefetch_abort_exception  和 arm_data_abort_exception
   
      根据3.0的spec 中描述: p107 (A-13.2 Page Fault Protocol) 
   当发生pagefault时,会向pager发送 pagefualt ipc,其中tag is lable ,
   msg 0 is addr (pagefault) ,msg 1 is ip
   至于从traps.S中收到异常到发送ipc的流程对于arm v4,v5 相当复杂,因为要支持FASS,
   这个就是ce 5.0的slot概念: slot 0 做为share 部分,所有任务在该段执行,
   其他地址空间有其他任务solt共享,
   根据fass 在arm9 oklinux 上的表现,偏离话题一下:如果使用arm9 cpu 跑
   ce 6.0  的性能可能不如 ce5.0, (进程切换速度是os的主要性能)
   该部分流程细节没有完全看懂,并且目前主要是关注oklinux 部分,就先略过
   不过大致过程为space->handle_pagefault ==>current->send_pagefault_ipc
      现在回到oklinux 中,在oklinux 中,因为linux 和 user 进程都是跑在l4 kernel
  的用户空间,所以收到pf ipc 时无法和nlinux 中一样,完成user 到kernel 的切换,
  那么办法就是轮询,所以oklinux  init 时在init_post执行init 进程时,
  在最终的__execve最后挂个syscall_loop, 这个syscall_loop不但处理 syscall,
  也处理pagefault, 在case L4_PAGEFAULT:
   l4_do_page_fault(addr, L4_Label(curinfo->tag) & 0xf, &curinfo->regs);
  其中addr 是pagefault 的地址 ,param 2,是access权限,(见tag的定义),这个类似
  与nlinunx中的fsr,param 3 其实是一组msg,在oklinux 中pt_regs被定义成如下:
  struct pt_regs {
 L4_Msg_t msg;
 int strace_flag;
 /* mode: 0 - in user, 1 - in kernel, 2 - isr */
 unsigned int mode;
  };
  对于msg 字段, 根据pagefault protocal, msg 0存放fault addr, msg 1放发生异常
 时的pc
 进入l4_do_page_fault,也nlinux一样,先查找vma,然后分成good area, bad area
  然后good area 中根据access 判断分支到bad_area,或者进入handle_mm_fault=>
  handle_pte_fault ,在后面的流程中基本和nlinux 相同, 但是oklinux 中的页表
  部分,并非mmu 使用,有人提到过shadow 的说法,就是维护该页表只是给oklinux
  kernel 管理用,但并非是进程切换时使用的,但对于页的一些实际操作还是要通过
  l4 api 去实现,而不能通过修改page 属性来完成,比如ptep_set_wrprotect:
  nlinunx中只需要设置page flag ,就完成了正真的mmu 写保护,但是在oklinux 中
  shadow 部分照做,但是还需要call tlb_modify 来重新map l4 page 到新的属性
    还有一些实际cache 和tlb 的操作,也不能通过nlinux 实践操作cp15完成,一样
  要call l4 api,如果flush_cache_page, update_mmu_cache, 这些都通过封装
  l4 api 实现,在archl4mmtlb.c 中
    还有一些页表操作部分,因为shadow的关系,不需要做,比如nlinux
  __pmd_populate 中需要flush_pmd_entry,clean cache中pmd 地址处数据,
  让更新起作用,现在oklinux 中该页表部分和硬件没关系,所以不用做
  具体变化在includeasm-l4pgalloc.h中
     再回到l4_do_page_fault 中,在接下来的bad_area部分,有部分是nlinux do
  pagefault 中没有的:
  if (user_mode(regs) &&  ((address & PAGE_MASK) == 0xffff0000)) {
  这部分判断包含的代码,实际是处理nlinux 中 User helpers部分中的一部分
  功能,其中最主要的就是NPTL 的pthread_self 的调用,因为实际pthread_self
  是通过 mvn r0, 0xf000,SUB     PC, R0, #0x1F,可以看出,在用户态访问
  kernel 0xFFFF0FE0 ,肯定引起异常,放在nlinux ,那么就到entry-armv.S
  中交由__kuser_get_tls 处理,而在oklinux,只能由l4_do_page_fault
  一起处理了,所以下面加了一大块,目的就要把取tls的功能实现,oklinux
  中关于该部分的实现相当的黑客,那就是直接改指令实现->到utcb 的
  Reserved[0],offset 56,去取出thread id
简单分析下:
    
 /* user_tid => user process L4-thread Id ,*/
L4_Copy_regs_to_mrs(task_thread_info(current)->user_tid);
/* 取出当前l4 thread的context,就是arm regs*/
L4_StoreMRs(0, 16, &msg.msg[0]);
lr = msg.msg[14];     /*  取出lr =>  (当前PC-4得出的地址) */
                      /*该lr 就是发生异常前的最后lr 地址*/
fpc = lr - 4;        /* 修正下lr =pc-4*/
L4_CacheFlushAll();  /* 刷下icache*/
instr = get_instr(fpc);
/* 如果是bl 指令,如果有兴趣可以dump 下glibc中libpthread
pthread_self
可以看到在movn r0,0xff00前会有条bl __aeabi_read_tp
 */
if ((instr & 0x0f000000) == 0x0b000000) { 
 offs = instr << 8;
 offs = offs >> 6; /* ASR */
      
/* 计算出bl 跳转到的指令*/
 fpc = (fpc + 8) + offs;         
/*  根据task mm pgd 找到该地址pte 然后计算vaddr
    shadow 页表也是有作用的,否则到那里去计算vaddr
*/
 instr = get_instr(fpc);         
 if (instr == -1UL)
  goto bad_area_nosemaphore;
 if ((instr & 0xffffffff) == 0xe3e00a0f) {
         /* 在进1步判断 是否mvn r0, 0xf000 */
                /* 从新写指令,只能写4条!!! */
  /* mov r0, #0xff000000 */
  r = set_instr(fpc, 0xe3a004ff);                  
  if (r == -1UL)
  goto bad_area_nosemaphore;            
  fpc += 4;
  /* ldr r0, [r0, #0xff0] */
  r = set_instr(fpc, 0xe5900ff0);              
/* 0xff000000 +  0xff0 => 0xff000ff0*/
/* #define USER_UTCB_REF 0xff000ff0 ,固定位置for armv5,user tcb */
 if (r == -1UL)
  goto bad_area_nosemaphore;
  fpc += 4;
  /* ldr r0, [r0, #56] */
                /* Reserved for future kernel use. 56 .. 63  */   
  r = set_instr(fpc, 0xe5900038);              
  if (r == -1UL)
  goto bad_area_nosemaphore;
  fpc += 4;
  /* mov pc, lr */
  r = set_instr(fpc, 0xe1a0f00e);
  if (r == -1UL)
   goto bad_area_nosemaphore;
/* 指令cache  也要刷下,否则ram 改了,cache 没变*/
  L4_CacheFlushAll();     
/* 执行下面这段就可以跳到修改后的地方执行:
L4_Copy_mrs_to_regs 中有下面动作:
dest->set_user_ip((addr_t)mr[15]); 
*/
 msg.msg[0] = current_thread_info()->tp_value;
 msg.msg[15] = lr;
 L4_LoadMRs(0, 16, &msg.msg[0]);
 L4_Copy_mrs_to_regs(
     task_thread_info(current)->user_tid);
 L4_MsgPutWord(¤t_regs()->msg, 1,
    lr);
     
  再接下来,和信号有关,等到研究完oklinux 信号时再写把
先占个位置:
/*
 * This decides where the kernel will map the __wombat_user_sig_handler
 * page to for signal and thread startup/modification.
 * This must not conflict with the utcb and kip area setup in
 * arch/l4/kernel/setup.c
 * NPTL support (thread pointer, cmpxchg) is also added to this page.
 */
#define TASK_SIG_BASE (0x98000000UL)
 
  前面讲了oklinux kernel部分是shadow,那么分配了一页后,如何让l4kernel
  知道该页,在真的mmu 上的页表变化,其实就是call l4 api map 该页
  比如:do_anonymous_page 中
update_mmu_cache(vma, address, page_table, entry); (注意被oklinux改过了)
  进去看下就知道了,根据oklinux 提供的pagetable 找到 fpage,然后call l4 api
  okl4_map_page 进行真的mmu 页表设置
  这样一来,两者就联系起来了
 
  好就要结束了,结束前关于,以前oklinxu中断帖子中:
 /////////////////////////////////////
  获得当前thread info , 然后resg mode ++ 就表示进入oklinux kernel mode ,进入kernel mode 是假的,就是标记下mode =1
  
  
   L4_MsgStore(tag, &msg);          /* get ipc msg */
   num = L4_MsgWord(&msg, 0);
   if (sender.raw == L4_nilthread.raw)  // 做个判断,只有sender 是一个nil thread 才是合法的 ,为什么???
           irq = mask_to_irq(&num);     // 从 utcb 的platform_reserved[0] 去获得hardware irq num
  
   接着call  handle_irq    =>__do_IRQ  根据irq call 到driver 的 ISR handle
   mode -- 返回到user mode   (这样标记user mode ,kernel mode 有什么 实际意义???, 因为search 了下,只有对mode 赋值,没有判断)
/////////////////////////////////////////
这段修正下mode++, mode-- 是有用的,没有直接判断,是用user_mode(regs) 判断
在这里user_mode(regs) 起了大作用了,上面有写的不正确或者有细节疑问
以后有空继续自我迭代  :-<