其他分享
首页 > 其他分享> > MIT6.S081 ---- Preparation: Read chapter 4

MIT6.S081 ---- Preparation: Read chapter 4

作者:互联网

Preparation: Read Chapter 4

有三种事件会造成CPU放弃正常的指令执行,强制将控制权交给一段特殊的代码处理这个事件:

本书使用trap作为上述情况的通用术语。通常,执行在trap期间的任何代码稍后都需要恢复,并且不需要知道发生了什么。我们经常需要traps透明,这对设备中断特别重要。通常的流程是:

xv6在内核中处理所有的traps,traps不会交给用户代码。在内核中处理traps对于系统调用很正常;对于中断是有意义的,因为隔离性要求只允许内核使用物理设备,而且内核是一种可以在多进程间共享设备的方便机制;对于异常也是有意义的,因为xv6通过kill所有有问题的程序响应用户空间的所有异常。

xv6 trap处理过程分为四步:

RISC-V trap machinery

每个RISC-V CPU有一组控制寄存器:内核能写这些寄存器,通知CPU如何处理traps;内核能读这些寄存器,查找已经发生的trap。risc.h(kernel/risc.h:1)包含xv6使用的定义。这里有关于最重要的寄存器的概述:

以上寄存器和supervisor-mode下的traps处理有关,它们不可能在user-mode下被读写。在machine-mode下有一组相似的控制寄存器,xv6只在定时中断的特殊情况下使用它们。

每个CPU在一个多核芯片上有自己的一组寄存器,在任何时间,都可能有多个CPU在处理中断。

当需要强制进入trap时,RISC-V硬件为所有的trap类型(除时钟中断)做如下处理:

内核软件必须完成(CPU不负责这些):

为了加快trap思考上面这些步骤是否能省略是很有价值的。尽管在一些情况下可以使用更简单的顺序工作,但是通常省略许多步骤会很危险。例如,假设CPU没有切换PC,来自用户空间的trap可能在运行用户指令的时候切换到supervisor-mode。这些用户指令可能打破用户/内核的隔离性。例如通过修改satp寄存器指向一个允许访问所有物理内存的页表。
因此,CPU必须切换到一个内核指定的指令地址,称为stvec

来自用户空间的traps

xv6处理traps方式不同,这取决于它执行在内核,还是执行在用户代码。本节讲后者,4.5节讲前者。

当用户程序在用户空间执行时,调用了系统调用(ecall指令),或者做了一些非法操作,或者来了一个设备中断,这时引起了一个trap。来自用户空间的trap的顶级(调用链)path是:

xv6的trap handling的设计的主要限制是当trap到来时RISC-V硬件不切换页表。这意味着stvec的trap handler地址必须在用户页表中有一个有效的映射,因为当trap handing代码开始执行时,那个页表必须有效。再者,xv6的trap handling代码需要切换到内核页表,为了能在切换后继续执行,内核页表必须也有一个对于stvec指向的handler的映射。

xv6使用trampoline页满足这些要求。trampoline页包含uservec以及stvec指向的xv6 trap handling代码。trampoline页被映射在每个进程的页表中,地址为TRAMPOLINE,在虚拟地址空间的末尾,在程序自己使用的内存的上面。trampoline页也被映射在内核页的TRAMPOLINE地址。因为trampoline页被映射在用户页表,有PTE_Uflag,traps在supervisor-mode下在这里开始执行。因为trampoline页被映射在内核地址空间的相同地址,在切换到内核页表之后,trampoline能继续执行。

对于uservectrap handler的代码,在trampoline.S(kernel/trampoline.S:16)。当uservec开始执行时,所有32个寄存器包含有被中断的用户代码所拥有的值。这32个值需要被保存在内存中,当trap返回用户空间时需要恢复。存储到内存需要使用寄存器来保存地址,但是当前没有可用的通用寄存器。所幸RISC-V以sscratch寄存器的形式提供了帮助。uservec起始的csrrw指令交换a0sscratch寄存器的内容。现在用户代码的a0被保存在sscratchuservec有一个寄存器a0可以使用,a0的值内核之前放在了sscratch中。

uservec的下一个任务是保存32个用户寄存器。在进入用户空间之前,内核设置sscratch指向一个进程的trapframe结构(这里有一部分空间用来保存32个用户寄存器)(kernel/proc.h:44)。因为satp指向用户页表,uservec需要trapframe映射在用户地址空间。当创建每个进程的时候,xv6为进程的trapframe分配一页,总是将它映射在虚拟地址TRAMFRAME处,在TRAMPOLINE的下面。尽管内核能通过内核页表找到它的物理地址去使用trapframe,进程的p->trapframe也指向trampframe。

交换a0sscratch之后,a0有指向当前进程trapframe的指针。uservec保存所有用户寄存器,包括用户的a0,从sscratch中读取。

trapframe包含:

usertrap的任务是形成trap的原因并返回。(kernel/trap.c:37):

返回用户空间的第一步是调用usertrapret(kernel/trap.c:90),这个函数设置RISC-V控制寄存器为以后来自用户空间的trap做准备:

usertrapret:调用userretTRAPFRAME保存到a0,将进程的用户页表的指针保存在a1(kernel/trampoline.S:88)。
userret

Code: Calling system calls

Chapter2讲了initcode.S调用exec系统调用(user/initcode.S:11)。本节讲用户调用如何进入exec系统调用在内核中的实现。

initcode.Sexec的参数放在寄存器a0a1中,系统调用编号放在a7中。system call numbers匹配syscalls数组(syscalls是一个函数指针表)。ecall指令trap进内核,执行uservecusertrapsyscall

syscall(kernel/syscall.c:133)从trapframe中保存的a7中恢复系统调用编号,使用这个编号去索引syscalls。对于第一个系统调用,a7含有SYS_exec(kernel/syscall.h:8),引出系统调用实现sys_exec函数的调用。

sys_exec返回时,syscall将返回值存到p->trapframe-a0中,这也是exec()函数的返回值,因为RISC-V的C调用约定将返回值放在a0中。系统调用返回负数通常表明errors。0或者正数表明success。如果系统调用号无效,syscall打印一个error并且返回-1。

Code: System call arguments

系统调用在内核中的实现需要找到被用户代码传递的参数。因为用户代码调用系统调用封装的函数,参数按照RISC-V C calling convention放在寄存器里。内核trap代码保存用户寄存器到当前进程的trap frame,内核能在这里找到寄存器的值。内核函数argintargaddrargfd从trap frame中恢复系统调用的参数作为一个整数、指针、文件描述符。它们都是调用argraw恢复被保存的用户寄存器(kernel/syscall.c:35)。

一些系统调用传递指针作为参数,内核必须使用这些指针去读写用户内存。如:exec系统调用传给内核一组指向用户空间字符串参数的指针。这些指针带来两个挑战:

内核实现了可以安全地对用户提供的地址进行数据传输的函数。fetchstr是一个例子(kernel/syscall.c:25)。文件系统调用exec使用fetchstr从用户空间恢复字符串文件名参数。fetchstr调用copyinstr

copyinstr(kernel/vm.c:398)最多从用户页表pagetable的虚拟地址srcva复制max字节到dst。因为pagetable不是当前页表,copyinstr使用walkaddrwalkaddr调用walk)在pagetable中查找srcva,产生物理地址pa0(kernel/vm.c:405)。内核映射每个物理地址到相应的内核虚拟地址,所以copyinstr能直接从pa0复制字符串字节到dstwalkaddr(kernel/vm.c:104)检查用户提供的虚拟地址是否在用户地址空间内,所以应用程序不可能欺骗内核读取其他内存。一个类似的函数,copyout将数据从内核复制到用户提供的地址。

Traps from kernel space

xv6根据正在执行的是内核代码还是用户代码,对CPU trap寄存器的配置略有不同(内核这个情况主要是处理中断和异常的)。当内核正在一个CPU上执行时,内核将stvec指向汇编代码kernelvec(kernel/kernelvec.S:10)。因为xv6在内核中,kernelvec能依赖:已被设置为内核页表的satp,指向有效内核栈的栈指针。kernelvec将所有32个寄存器压入栈中,便于后来恢复它们使中断的内核代码可以不受干扰的执行。

kernelvec在被中断的内核线程的栈上保存寄存器,这是有意义的,因为寄存器的值属于该线程。如果trap导致切换到另一个线程,这点很重要,在这种情况下,trap将从新线程的栈上返回,将中断线程的保存的寄存器安全的保留在它的栈上。

保存寄存器后,kernelvec跳转到kerneltrap(kernel/trap.c:134)。kerneltrap为两种trap类型做了准备:设备中断和异常。调用devintr(kernel/trap.c:177)检查并处理中断 trap。如果trap不是一个设备中断,则必定是异常,如果发生在xv6内核,总是一个致命的error,内核调用panic并停止执行。

如果由于时钟中断调用kerneltrap,并且进程的内核线程正在运行(与调度线程相反),kerneltrap调用yield给其他线程一个运行的机会。在某个时刻,这些线程中的一个将yield,让我们的线程和它的kerneltrap再次恢复。第7章介绍yield

kerneltrap执行完毕,它将返回被trap中断的代码。因为yield可能破坏了sepcsstatus中的previous mode,所以kerneltrap在启动时需要保存它们。它恢复这些控制寄存器,返回到kernelvec(kernel/kernelvec.S:48)。
kernelvec从栈中弹出保存的寄存器,执行sret,复制sepc到PC,恢复中断的内核代码。

有意义的思考:如果kerneltrap因为时钟中断调用yield,trap返回如何发生。

当CPU从用户空间进入内核空间时,xv6设置CPU的stveckernelvec(见usertrap(kernel/trap.c:29));有个时间窗口:内核开始执行但stvec仍然设置为uservec,这期间没有设备中断至关重要。幸运的是当开始trap时,RISC-V总是关中断的,而xv6在设置stvec之前不会开中断。

标签:chapter,调用,MIT6,用户,Preparation,trap,寄存器,页表,内核
来源: https://www.cnblogs.com/seaupnice/p/15809701.html