和森AI知识分享平台(kindlytree)
5:OS Traps and system calls
操作系统工程(Operating System Engineering)
陷阱与系统调用
5.1 Traps简介
RISC-V trap机制
5.3 RISC-V trap机制
为什么需要这些步骤 安全性 切换到stvec指向的地址确保陷阱处理程序始终在受控的内核代码中执行. 保存和恢复程序计数器(pc)到sepc寄存器:避免陷阱处理时丢失原始执行状态,使得程序能够正确恢复执行. 提高性能 这种设计让操作系统可以根据具体情况选择是否进行页表切换等操作 RISC-V 不会在发生陷阱时自动切换到内核页表或内核栈.
5.4 RISC-V陷阱处理流程
陷阱可能会在用户态执行的条件下发生
5.5 从用户空间触发的陷阱
陷阱可能会在用户态执行的条件下发生
uservec(trampoline.S中的以label uservec开始的代码段)的具体行为: 用户代码运行的32个寄存器需要保存到结构体trapframe中 先将寄存器a0存放到sscratch寄存器中(a0常作为函数调用时参数传递寄存器以及函数返回值传递寄存器) 然后将trapframe的地址加载到a0寄存器中 再基于a0的偏移(即trapframe的偏移)保存众多的通用寄存器 trapframe在用户的地址空间进行映射,satp仍指向用户页表,trapframe的内容 当前进程的内核栈指针 当前CPU的hartid usertrap函数的地址 内核页表的地址 uservec将satp寄存器指向内核页表后,跳转到usertrap执行
5.6 从用户空间触发的陷阱
陷阱可能会在用户态执行的条件下发生
usertrap(kernel/trap.c第37行)的具体行为: 将stvec设置为kernelvec,使得在内核态下发生trap能由kernelvec进行处理 保存用户程序计数器p->trapframe->epc = r_sepc(); 由于usertrap可以调用yield切换到其他进程,其他进程可能会返回到用户空间,在用户空间里修改了sepc寄存器 如果trap为一个系统调用,usertrap会调用syscall来处理 在系统调用情况下,sepc会在保存的用户程序计数器的基础之上再加上4,因为一开始sepc指向的ecall指令,但是用户代码在返回后的下一条指令处执行 如果为一个设备中断,则会调用devintr来处理 如果为一个异常,则内核会kill掉出错的进程 在执行完相关的trap逻辑后准备返回前,需要check一下进程是否kill掉了,或者如果为设备中断,则需要让出CPU执行(yield) 返回用户空间的第一步为调用usertrapret
5.7 从用户空间触发的陷阱
陷阱可能会在用户态执行的条件下发生
usertrapret (kernel/trap.c第90行)的具体行为: 设置stvec寄存器的值为uservec 将kernel相关的信息存回trapframe以便下次trap时uservec能够方便的获取 调用userret,将指向进程用户页表的指针作为参数(a0寄存器,代码在kernel/trampoline.S:101) 将用户进程页表切换到satp寄存器 用户页表会同时映射trampoline page和TRAPFRAME trampoline page在用户页表和内核页表为同样的虚拟地址,以允许userret在改变satp之后能够持续执行 userret将TRAPFRAME的地址load到寄存器a0,通过a将trapframe保存的用户寄存器进行恢复 再恢复保存的a0,执行sret返回到用户空间
5.8 从用户空间触发的陷阱
陷阱可能会在用户态执行的条件下发生
当用户程序执行了系统调用(ecall instruction) 做了一些非法的事情(something illegal),如除零错误 或者设备中断(device interrupts) 从用户空间触发trap的粗粒度执行路径 uservec(kernel/trampoline.S) usertrap(kernel/trap.c) usertrapret(kernel/trap.c) userret(kernel/trampoline.S) Trampoline page的作用 trampoline page包含有uservec,xv6的trap handling的代码stvec指向的内容 uservec trap handler的代码为trampoline.S(kernel/trampoline.S) 当uservec开始执行时,所有的32位寄存器都包含值着中断处用户代码的相关值 这32个值需要保存在内存的某个地方,因此后续内核可以将其恢复到用户空间 RISC-V提供了sscratch寄存器,在uservec开始处的csrw指令将a0存入sscratch寄存器,然后uservec就有a0寄存器操作对象 uservec的下一个任务为存储32位用户寄存器,内核为每一个进程分配了一个内存页来存储trapframe结构,该结构里可以存储32个用户寄存器(kernel/proc.h)
5.6 从用户空间触发的陷阱
Trampoline page的作用
trampoline page包含有uservec,xv6的trap handling的代码stvec指向的内容 uservec trap handler的代码为trampoline.S(kernel/trampoline.S) 当uservec开始执行时,所有的32位寄存器都包含值着中断处用户代码的相关值 这32个值需要保存在内存的某个地方,因此后续内核可以将其恢复到用户空间 RISC-V提供了sscratch寄存器,在uservec开始处的csrw指令将a0存入sscratch寄存器,然后uservec就有a0寄存器操作对象 uservec的下一个任务为存储32位用户寄存器,内核为每一个进程分配了一个内存页来存储trapframe结构,该结构里可以存储32个用户寄存器(kernel/proc.h) satp寄存器仍然指向用户页表,uservec需要trapframe映射到用户空间 xv6将每一个进程在虚拟地址TRAPFRAME的trapframe映射到进程的用户页表 TRAPFRAME紧接着TRAMPOLINE下方,进程的p->trapframe同时也指向了trapframe Uservec将TRAPFRAME的地址加载进a0,然后在那里保存所有用户寄存器,包括用户寄存器a0。然后从sscratch寄存器读取相关内容到当前寄存器 trapframe包含了当前进程内核的地址,当前CPU的hartid,usertrap函数的地址,内核页表的地址,uservec检索出这些值,然后将satp切换到内核页表,跳转到usertrap处开始执行
5.7 从用户空间触发的陷阱
Trampoline page的作用
trampoline page包含有uservec,xv6的trap handling的代码stvec指向的内容 uservec trap handler的代码为trampoline.S(kernel/trampoline.S) 当uservec开始执行时,所有的32位寄存器都包含值着中断处用户代码的相关值 这32个值需要保存在内存的某个地方,因此后续内核可以将其恢复到用户空间 RISC-V提供了sscratch寄存器,在uservec开始处的csrw指令将a0存入sscratch寄存器,然后uservec就有a0寄存器操作对象 uservec的下一个任务为存储32位用户寄存器,内核为每一个进程分配了一个内存页来存储trapframe结构,该结构里可以存储32个用户寄存器(kernel/proc.h) satp寄存器仍然指向用户页表,uservec需要trapframe映射到用户空间 xv6将每一个进程在虚拟地址TRAPFRAME的trapframe映射到进程的用户页表 TRAPFRAME紧接着TRAMPOLINE下方,进程的p->trapframe同时也指向了trapframe uservec将TRAPFRAME的地址加载进a0,然后在那里保存所有用户寄存器,包括用户寄存器a0。然后从sscratch寄存器读取相关内容到当前寄存器 trapframe包含了当前进程内核的地址,当前CPU的hartid,usertrap函数的地址,内核页表的地址,uservec检索出这些值,然后将satp切换到内核页表,跳转到usertrap处开始执行 usertrap的职责为确定trap的原因,进行处理然后返回,其首先改变stvec的值以至于trap发生在内核状态时能够被kernelvec而不是uservec处理
5.8 从用户空间触发的陷阱
Trampoline page的作用
trampoline page包含有uservec,xv6的trap handling的代码stvec指向的内容 usertrap的职责 确定trap的原因,进行处理然后返回,其首先改变stvec的值以至于trap发生在内核状态时能够被kernelvec而不是uservec处理 其保存sepc寄存器(保存用户程序计数器pc),因为usertrap可能可以调用yield去切换到另外的进程内核线程,当前进程可能会返回到用户空间,在进程里可以修改sepc 如果trap为系统调用,usertrap会调用syscall去处理 如果设备中断,devintr 如果异常,内容会kill错误的进程 系统调用会将在保存的用户的程序计数器的基础之上再加4,因为RISC-V在系统调用的情形下,会将程序指针指向ecall指令但是用户代码需要在接下来的下一条指令恢复执行 在退出的过程中,usertrap会检查进程是否被kill掉或者需要让出cpu(如果这个trap是一个定时器中断) 返回到用户空间的第一步为调用usertrapret,这个函数设置RISC-V控制寄存器以准备将来从用户空间产生新的trap 设置stvec为uservec 准备trapframe里uservec项的值 usertrapret将sepc设置为之前保存的用户程序计数器
5.9 从用户空间触发的陷阱
Trampoline page的作用
trampoline page包含有uservec,xv6的trap handling的代码stvec指向的内容 usertrap的职责 返回到用户空间的第一步为调用usertrapret,这个函数设置RISC-V控制寄存器以准备将来从用户空间产生新的trap 设置stvec为uservec 准备trapframe里uservec项的值 usertrapret将sepc设置为之前保存的用户程序计数器 最后,usertrapret在trampoline page(同时在用户内核页表映射)调用userret,因为在userret里的汇编代码将要切换页表 userret将satp切换到进程的用户页表 用户页表同时映射trampoline page核和TRAPFRAME
5.10 从用户空间触发的陷阱
触发系统调用(calling system calls)
initcode.S触发了exec系统调用,exec系统调用的过程路径 initcode.S为函数exec将参数放入寄存器a0和a1,并将系统调用的函数指针对应在系统调用函数指针数组的索引数字放入a7寄存器 ecall指令将切换到内核并触发uservec,usertrap,然后执行syscall syscall从保存在trapframe数据结构的a7寄存器检索具体的系统调用函数在函数数组中的索引号 第一个系统调用,a7包含的SYS_exec的值,会引起触发sys_exec的系统调用函数 当sys_exec返回时,syscall记录了其返回值p->trapframe->a0,这将引起原先的用户空间调用exec()来返回该值,由于在RISC-V上c调用的规范为将返回值放在a0上 系统调用通常返回负值表示错误,0或正值表示成功 如果系统调用的数字是非法给的,syscall会打印错误返回-1
5.11 触发系统调用
系统调用的参数(system call arguments)
在内核里实现的系统调用需要在用户代码里传入参数 这些参数会放在寄存器里 内核的trap code会保存用户寄存器到当前进程的trap frame里,然后内核代码可以去获取这些值 内核函数argint,agraddr以及argfd用来从trap frame的系统调用里检索系统调用的第n个参数,指针或文件描述符,它们均调用argraw来检索保存的合适的用户寄存器 一些系统调用会将指针做为参数,内核必须使用这些指针来读和写内存 exec系统调用,将传递给内核一个指针数组,指向用户空间中的字符串参数 这些指针面临两个挑战 第一,用户程序可能有bug或错误,可能会传递给内核一个非法的指针或者使得内核访问了一个用户内存 第二,xv6的内核页表的映射和用户页表的映射不同,内核不能使用普通的命令去读写用户提供的地址 内核实现了函数可以在用户提供的地址来传输数据 fetchstr就是一个示例(kernel/syscall.c) 文件系统调用如exec使用fetchstr来检索来自用户空间的字符串的文件名作为的参数 fetchstr调用copyinstr来做具体的事情
5.12 系统调用的参数
系统调用的参数(system call arguments)
在内核里实现的系统调用需要在用户代码里传入参数 内核实现了函数可以在用户提供的地址来传输数据 fetchstr就是一个示例(kernel/syscall.c) 文件系统调用如exec使用fetchstr来检索来自用户空间的字符串的文件名作为的参数 fetchstr调用copyinstr来做具体的事情 从用户页表pagetable的虚拟地址srcva中拷贝最多max个字节到dst中 由于pagetable不是当前的页表,copyinstr使用walkaddr(会进一步调用walk)在pagetable总查找srcva,获得物理地址pa0 内核页表将虚拟地址和物理地址直接对等映射,这样就允许copyinstr直接将字符串字节流数据从pa0拷贝到dst
5.12 系统调用的参数
来自内核的traps(traps from kernel space)
xv6在内核态下处理traps和从用户态下处理traps不同 当进入内核时,usertrap将stvec指向了在kernelvec处的汇编代码 kernelvec仅仅当xv6已经在内核时才执行 kernelvec能够依赖satp指向内核页表,以及栈指针指向有效的内核栈 Kernelvec将所有的32个寄存器push到站上,之后再内核中断的地方可以恢复这些寄存器 kernelvec将寄存器保存到中断的内核线程的栈上是可行的,因为寄存器的值属于那个thread 当保存寄存器之后,kernelvec会跳转到kerneltrap,kerneltrap可以处理两种类型的traps 设备中断 异常 调用devintr来检测,如果trap不是一个设备中断,其必须为异常,在内核发生异常时,讲师一个致命的错误,内核将调用panic来终止执行 如果kerneltrap由于一个时间中断来调用,进程的kernel线程在运行(而不是一个调度线程),kerneltraps调用yield来给一个其他线程运行的机会
5.13 来自内核的Traps
页错误异常(page fault exceptions)
Xv6对异常的反应比较繁琐 如果异常发生在用户空间,内核会kill掉出错的进程 如果一个异常发生在内核空间,kernel本身将会出现fatal error,真实使用的操作系统会比这些处理方式更加的有趣 举个实例,很多的内容使用页错误(page faults)来实现copy-on-write(COW)fork 为了解释copy-on-write fork, xv6的系统调用fork会创建一个子进程,创建初始时子进程的初始内存内容和父进程一致,xv6在实现fork时使用了uvmcopy,其为子进程分配物理内存然后将父进程的内存内容拷贝过去