Lec01 Introduction and Examples

学生问:系统调用跳到内核与标准的函数调用跳到另一个函数相比,区别是什么? Robert教授:Kernel的代码总是有特殊的权限。当机器启动Kernel时,Kernel会有特殊的权限能直接访问各种各样的硬件,例如磁盘。而普通的用户程序是没有办法直接访问这些硬件的。所以,当你执行一个普通的函数调用时,你所调用的函数并没有对于硬件的特殊权限。然而,如果你触发系统调用到内核中,内核中的具体实现会具有这些特殊的权限,这样就能修改敏感的和被保护的硬件资源,比如访问硬件磁盘。我们之后会介绍更多有关的细节。

如果一个应用程序通过open系统调用得到了一个文件描述符fd。之后这个应用程序调用了fork系统调用。fork的语义是创建一个当前进程的拷贝进程。而对于一个真正的拷贝进程,父进程中的文件描述符也必须存在且可用。所以在这里,一个通过open获得的文件描述符,与fork以这种有趣的方式进行交互。当然,你需要想明白,子进程是否能够访问到在fork之前创建的文件描述符fd。在我们要研究的操作系统中答案是,Yes,需要能够访问。

文件描述符 0 连接到console的输入,文件描述符 1 连接到了console的输出。

文件描述符本质上对应了内核中的一个表单数据。内核维护了每个运行进程的状态,内核会为每一个运行进程保存一个表单,表单的key是文件描述符。这个表单让内核知道,每个文件描述符对应的实际内容是什么。这里比较关键的点是,每个进程都有自己独立的文件描述符空间,所以如果运行了两个不同的程序,对应两个不同的进程,如果它们都打开一个文件,它们或许可以得到相同数字的文件描述符,但是因为内核为每个进程都维护了一个独立的文件描述符空间,这里相同数字的文件描述符可能会对应到不同的文件。

学生提问:有一个系统调用和编译器的问题。编译器如何处理系统调用?生成的汇编语言是不是会调用一些由操作系统定义的代码段? Robert教授:有一个特殊的RISC-V指令,程序可以调用这个指令,并将控制权交给内核。所以,实际上当你运行C语言并执行例如open或者write的系统调用时,从技术上来说,open是一个C函数,但是这个函数内的指令实际上是机器指令,也就是说我们调用的open函数并不是一个C语言函数,它是由汇编语言实现,组成这个系统调用的汇编语言实际上在RISC-V中被称为ecall。这个特殊的指令将控制权转给内核。之后内核检查进程的内存和寄存器,并确定相应的参数。

学生提问:fork产生的子进程是不是总是与父进程是一样的?它们有可能不一样吗? Robert教授:在XV6中,除了fork的返回值,两个进程是一样的。两个进程的指令是一样的,数据是一样的,栈是一样的,同时,两个进程又有各自独立的地址空间,它们都认为自己的内存从0开始增长,但这里是不同的内存。 在一个更加复杂的操作系统,有一些细节我们现在并不关心,这些细节偶尔会导致父子进程不一致,但是在XV6中,父子进程除了fork的返回值,其他都是一样的。除了内存是一样的以外,文件描述符的表单也从父进程拷贝到子进程。所以如果父进程打开了一个文件,子进程可以看到同一个文件描述符,尽管子进程看到的是一个文件描述符的表单的拷贝。除了拷贝内存以外,fork还会拷贝文件描述符表单这一点还挺重要的,我们接下来会看到。

学生提问:如果父进程有多个子进程,wait是不是会在第一个子进程完成时就退出?这样的话,还有一些与父进程交错运行的子进程,是不是需要有多个wait来确保所有的子进程都完成? Robert教授:是的,如果一个进程调用fork两次,如果它想要等两个子进程都退出,它需要调用wait两次。每个wait会在一个子进程退出时立即返回。当wait返回时,你实际上没有必要知道哪个子进程退出了,但是wait返回了子进程的进程号,所以在wait返回之后,你就可以知道是哪个子进程退出了。

最后更新于