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 实现,在arch\l4\mm\tlb.c 中
还有一些页表操作部分,因为shadow的关系,不需要做,比如nlinux
__pmd_populate 中需要flush_pmd_entry,clean cache中pmd 地址处数据,
让更新起作用,现在oklinux 中该页表部分和硬件没关系,所以不用做
具体变化在include\asm-l4\pgalloc.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) 起了大作用了,上面有写的不正确或者有细节疑问
以后有空继续自我迭代 :-<