Xv6 system call and the first process
xv6 系统调用与第一个进程
该文章作为本人学习 MIT 6.S081 课程与 xv6 book 的学习笔记,如有错误或疑问,欢迎联系我
什么是系统调用
对于用户程序而言,是无法与计算机硬件等进行交互的,必须通过系统调用通知内核与硬件交互,例如读取文件,只有通过read系统调用通知内核才能读取对应的文件。通过系统调用,可以把内核和用户完全分隔开。
在xv6中有许多系统调用,甚至xv6 lab中让你来添加系统调用。xv6实现的系统调用如下所示。
接下来就是如何进行系统调用,如何从用户程序切换到内核程序,我们从xv6第一个进程的启动开始讲起。
在操作系统中,有两个非常重要的系统调用:fork和exec,fork用于创建一个新的进程,exec用于执行可执行程序,一般来说,操作系统都是创建第一个进程然后在第一个进程中fork出新进程,然后在新进程中exec程序来工作的。
内核和用户进程的内存空间
其中比较重要的是kernel和user的内存中的最高处是trampoline(跳板)页面,其中保存着用于用户态跳转到内核态的代码。
其次就是在user内存中在trampoline下面的trapframe(陷阱帧)页面。
对于内核态trampoline代码的分配请参考我上一篇 笔记 。对于用户态trampoline代码,是在proc_pagetable函数(每次初始化并给进程分配页表的时候调用)中实现,其中还分配了trapframe页
1 |
|
在trapframe页面中保存了一个如下的数据结构:
1 | struct trapframe { |
其中保存都是一些在跳转到内核的时候需要保存的寄存器现场,例如a0、a1等等。其中比较重要的是前几个字段,kernel_satp保存内核页表的地址,kernel_sp保存进程的内核栈地址,kernel_trap保存usertrap的地址,epc保存在跳转内核之前用户的pc值。
第一个进程
首先我们看向main.c中的main函数,在初始化一些内容之后进入userinit函数开启第一个进程。
1 | void main() { |
看到userinit函数,首先调用allocaproc分配页表、pid以及trapframe和trampoline页面,在allocproc函数中还有一个比较特殊的地方,它将当前进程上下文中的ra字段定义为forkret函数的地址。
进程上下文中的ra字段在后续scheduler函数切换上下文的时候会加载到ra寄存器,而ra寄存器的作用是在函数return的时候跳转到ra寄存器中的地址,上述过程将ra字段定义为forkret函数的地址,那么在下次切换上下文就会将ra字段加载到ra寄存器,return之后就会到forkret函数,具体看我下一篇笔记。
接下来调用uvmfirst函数,在用户页表最低处分配一个页,并且将initcode放进去。
1 |
|
对于initcode,它是如下一段十六进制数,其实是initcode.S的十六进制表示,在汇编代码initcode.S中可以看到调用了ecall指令,这就是risc-v中的系统调用指令,a0寄存器保存了第一个参数/init\0(第一个进程的名字),a7寄存器保存了SYS_exec(对应exec函数调用),所以这一段就是执行了exec(init, argv)系统调用。
1 | // a user program that calls exec("/init") |
1 | // initcode.S |
然后我们回到main.c中的main函数,调用完userinit之后会调用scheduler函数,他会调度我们的init进程执行。
到目前为止,我们第一个进程的页表已经建立,trampoline和trapframe页面我们页分配好了,并且写入了trampoline代码和trapframe数据,还将initcode的汇编代码映射到了用户页表,只等scheduler调度init进程执行initcode代码。
系统调用
当进行系统调用时,我们先了解下面几个寄存器:
- stvec: 保存trap handler的代码地址(系统调用是trap的一种),当我们陷入到内核的时候会跳转到这个地方
- sepc: 当我们陷入内核的时候,我们把用户当前的pc复制到这里,在sret指令回到用户态的时候将sepc的值复制回pc
- scause: 保存陷入内核的原因,如果是系统调用,这个寄存器的值是8.
- sscratch: 内核在此处放置一个值,在陷阱处理程序的最开始时派上用场。
- sstatus: sstatus 中的 SIE 位控制是否启用设备中断。如果内核清除 SIE,RISC-V 将推迟设备中断,直到内核设置 SIE。 SPP 位指示陷阱是来自用户模式还是管理员模式,并控制 sret 返回的模式。
当我们真正执行到上面的ecall指令时,我们的硬件会执行下面的操作
- 清除sstatus中的SIE位关闭中断
- 将当前用户程序的pc复制到sepc
- 将当前模式(用户或supervisor)保存在sstatus寄存器的SPP位中
- 设置scause值为8
- 设置当前模式为supervisor模式(因为后续要跳转到内核执行内核代码)
- 将stvec寄存器复制到pc
- 开始执行新pc的地址
那我们第一次进行系统调用的时候,stvec中的值是什么呢?
To be continue….