我首先会简单介绍一下trap代码的执行流程,但是这节课大部分时间都会通过gdb来跟踪代码是如何通过trap进入到内核空间,这里会涉及到很多的细节。为了帮助你提前了解接下来的内容,我们会跟踪如何在Shell中调用write系统调用。从Shell的角度来说,这就是个Shell代码中的C函数调用,但是实际上,write通过执行ECALL指令来执行系统调用。ECALL指令会切换到具有supervisor mode的内核中。在这个过程中,内核中执行的第一个指令是一个由汇编语言写的函数,叫做uservec。这个函数是内核代码trampoline.s文件的一部分。所以执行的第一个代码就是这个uservec汇编函数。
之后,在这个汇编函数中,代码执行跳转到了由C语言实现的函数usertrap中,这个函数在trap.c中。
现在代码运行在C中,所以代码更加容易理解。在usertrap这个C函数中,我们执行了一个叫做syscall的函数。
这个函数会在一个表单中,根据传入的代表系统调用的数字进行查找,并在内核中执行具体实现了系统调用功能的函数。对于我们来说,这个函数就是sys_write。
sys_write会将要显示数据输出到console上,当它完成了之后,它会返回给syscall函数。
因为我们现在相当于在ECALL之后中断了用户代码的执行,为了用户空间的代码恢复执行,需要做一系列的事情。在syscall函数中,会调用一个函数叫做usertrapret,它也位于trap.c中,这个函数完成了部分方便在C代码中实现的返回到用户空间的工作。
除此之外,最终还有一些工作只能在汇编语言中完成。这部分工作通过汇编语言实现,并且存在于trampoline.s文件中的userret函数中。
最终,在这个汇编函数中会调用机器指令返回到用户空间,并且恢复ECALL之后的用户程序的执行。
对于这里的概述大家有问题吗?没有的话我要切到gdb了。
学生提问:vm.c运行在什么mode下?
Robert教授:vm.c中的所有函数都是内核的一部分,所以运行在supervisor mode。
学生提问:为什么这些函数叫这些名字?
Robert教授:现在的函数命名比较乱,明年我会让它们变得更加合理一些。(助教说)我认为命名与寄存器的名字有关。
学生提问:难道vm.c里的函数不是要直接访问物理内存吗?
Robert教授:是的,这些函数能这么做的原因是,内核小心的在page table中设置好了各个PTE。这样当内核收到了一个读写虚拟内存地址的请求,会通过kernel page table将这个虚拟内存地址翻译成与之等价物理内存地址,再完成读写。所以,一旦使用了kernel page table,就可以非常方便的在内核中使用所有这些直接的映射关系。但是直到trap机制切换到内核之前,这些映射关系都不可用。直到trap机制将程序运行切换到内核空间之前,我们使用的仍然是没有这些方便映射关系的user page table。
学生提问:这个问题或许并不完全相关,read和write系统调用,相比内存的读写,他们的代价都高的多,因为它们需要切换模式,并来回捣腾。有没有可能当你执行打开一个文件的系统调用时, 直接得到一个page table映射,而不是返回一个文件描述符?这样只需要向对应于设备的特定的地址写数据,程序就能通过page table访问特定的设备。你可以设置好限制,就像文件描述符只允许修改特定文件一样,这样就不用像系统调用一样在用户空间和内核空间来回捣腾了。
Robert教授:这是个很好的想法。实际上很多操作系统都提供这种叫做内存映射文件(Memory-mapped file access)的机制,在这个机制里面通过page table,可以将用户空间的虚拟地址空间,对应到文件内容,这样你就可以通过内存地址直接读写文件。实际上,你们将在mmap 实验中完成这个机制。对于许多程序来说,这个机制的确会比直接调用read/write系统调用要快的多。