Lab1:&Lab2

lab1 主要是为xv6添加一些util。

lab2 主要是为xv6添加一些系统调用

我觉得这两个lab能关注的主要这些点:

  • 内核编译

  • xv6启动过程

  • 如何从user进入kernel执行系统调用(比如调用sleep)

主要分析的代码有:

# 内核编译:
Makefile

# xv6 启动过程
main.c
proc.c/userinit() 
initcode.S
init.c

# 系统调用
syscall.c/syscall()

内核编译

这里主要解决一些xv6的内核编译细节。

xv6的代码主要有三个组成部分:

  • kernel: XV6 是一个宏内核结构,kernel目录下的所有文件会被编译成一个叫做kernel的二进制文件,然后这个二进制文件会被运行在kernle mode中。

  • user: 基本上是运行在user mode的程序。这也是为什么一个目录称为kernel,另一个目录称为user的原因。

  • mkfs: 它会创建一个空的文件镜像,我们会将这个镜像存在磁盘上,这样我们就可以直接使用一个空的文件系统。

首先,Makefile(XV6目录下的文件)会读取一个C文件,例如proc.c;之后调用gcc编译器,生成一个文件叫做proc.s,这是RISC-V 汇编语言文件;之后再走到汇编解释器,生成proc.o,这是汇编语言的二进制格式。

Makefile会为所有内核文件做相同的操作,比如说pipe.c,会按照同样的套路,先经过gcc编译成pipe.s,再通过汇编解释器生成pipe.o。

之后,系统加载器(Loader)会收集所有的.o文件,将它们链接在一起,并生成内核文件。

这里生成的内核文件就是我们将会在QEMU中运行的文件。

同时,Makefile还会创建kernel.asm,这里包含了内核的完整汇编语言,可以通过查看它来定位究竟是哪个指令导致了Bug。

make qemu 指令会执行:

几个指令:

-kernel:这里传递的是内核文件(kernel目录下的kernel文件),这是将在QEMU中运行的程序文件。

-m:这里传递的是RISC-V虚拟机将会使用的内存数量

-smp:这里传递的是虚拟机可以使用的CPU核数

-drive:传递的是虚拟机使用的磁盘驱动,这里传入的是fs.img文件

xv6 启动过程

XV6从entry.s开始启动,这个时候没有内存分页,没有隔离性,并且运行在M-mode(machine mode)。

XV6会尽可能快的跳转到kernel mode或者说是supervisor mode,然后到达 kernel/main.c

userinit() 函数比较有意思,运行了shell来与系统进行交互:

initCode.S 首先将init中的地址加载到a0(la a0, init),argv中的地址加载到a1(la a1, argv),exec系统调用对应的数字加载到a7(li a7, SYS_exec),最后调用ECALL。所以这里执行了3条指令,之后在第4条指令(ecall)将控制权交给了操作系统。

sys_exec中的第一件事情是从用户空间读取参数,它会读取path,也就是要执行程序的文件名。这里首先会为参数分配空间,然后从用户空间将参数拷贝到内核空间。我们打印path,可以看到传入的就是init程序。

init.c 会为用户空间设置好一些东西,比如配置好console,调用fork,并在fork出的子进程中执行shell。最终的效果就是Shell运行起来了。

系统调用

这里主要解决如何从user态进入kernel态并执行系统调用。

user/kernel mode是分隔用户空间和内核空间的边界,用户空间运行的程序运行在user mode,内核空间的程序运行在kernel mode。操作系统位于内核空间。

因为user/kernel mode 是隔离起来的,所以需要有一种方式能够让应用程序可以将控制权转移给内核(Entering Kernel)。

在RISC-V中,有一个专门的指令用来实现这个功能,叫做ECALL。ECALL接收一个数字参数,当一个用户程序想要将程序执行的控制权转移到内核,它只需要执行ECALL指令,并传入一个数字。这里的数字参数代表了应用程序想要调用的System Call

ECALL会跳转到内核中一个特定,由内核控制的位置。在xv6这,ecall跳转到 kernel/syscall.c syscall():

Last updated