6.828
Lec01 Introduction and Examples
文件描述符本质上对应了内核中的一个表单数据。内核维护了每个运行进程的状态,内核会为每一个运行进程保存一个表单,表单的key是文件描述符。这个表单让内核知道,每个文件描述符对应的实际内容是什么。这里比较关键的点是,每个进程都有自己独立的文件描述符空间,所以如果运行了两个不同的程序,对应两个不同的进程,如果它们都打开一个文件,它们或许可以得到相同数字的文件描述符,但是因为内核为每个进程都维护了一个独立的文件描述符空间,这里相同数字的文件描述符可能会对应到不同的文件。
fork会拷贝当前进程的内存,并创建一个新的进程,这里的内存包含了进程的指令和数据。之后,我们就有了两个拥有完全一样内存的进程。fork系统调用在两个进程中都会返回,在原始的进程中,fork系统调用会返回大于0的整数,这个是新创建进程的ID。而在新创建的进程中,fork系统调用会返回0。所以即使两个进程的内存是完全一样的,我们还是可以通过fork的返回值区分旧进程和新进程。Unix提供了一个wait系统调用,wait会等待之前创建的子进程退出。fork会在父进程中返回大于0的值。
exec系统调用,这个系统调用会从指定的文件中读取并加载指令,并替代当前调用进程的指令。从某种程度上来说,这样相当于丢弃了调用进程的内存,并开始执行新加载的指令。通常来说exec系统调用不会返回,因为exec会完全替换当前进程的内存,相当于当前进程不复存在了,所以exec系统调用已经没有地方能返回了。所以,exec系统调用从文件中读取指令,执行这些指令,然后就没有然后了。exec系统调用只会当出错时才会返回,因为某些错误会阻止操作系统为你运行文件中的指令,例如程序文件根本不存在,因为exec系统调用不能找到文件,exec会返回-1来表示:出错了,我找不到文件。所以通常来说exec系统调用不会返回,它只会在kernel不能运行相应的文件时返回。实际上,Shell会执行fork,之后fork出的子进程再调用exec系统调用,这是一个非常常见的Unix程序调用风格。对于那些想要运行程序,但是还希望能拿回控制权的场景,可以先执行fork系统调用,然后在子进程中调用exec。exec系统调用只会在出错的时候返回给调用进程。
wait会等待之前fork创建的子进程退出。当我在命令行执行一个指令时,我们一般会希望Shell等待指令执行完成。所以wait系统调用,使得父进程可以等待任何一个子进程返回。wait系统调用只能等待当前进程的子进程。所以wait的工作原理是,如果当前进程有任何子进程,并且其中一个已经退出了,那么wait会返回。但是如果当前进程没有任何子进程,比如在这个简单的例子中,如果子进程调用了wait,因为子进程自己没有子进程了,所以wait会立即返回-1,表明出现错误了,当前的进程并没有任何子进程。如果一个进程调用fork两次,如果它想要等两个子进程都退出,它需要调用wait两次。每个wait会在一个子进程退出时立即返回。当wait返回时,你实际上没有必要知道哪个子进程退出了,但是wait返回了子进程的进程号,所以在wait返回之后,你就可以知道是哪个子进程退出了。
两个文件描述符共享一个偏移,如果它们都是通过fork和dup从同一个原始文件描述符派生的。2>&1通知shell文件描述符2是描述符1的复制。
Lec03 OS Organization and System Calls
只需要使用 gcc -g 选项编译源文件,即可生成满足 GDB 要求的可执行文件。GCC 编译器支持 -O(等于同 -O1,优化生成的目标文件)和 -g 一起参与编译。GCC 编译过程对进行优化的程度可分为 5 个等级,分别为 O0~O4,O0 表示不优化(默认选项),从 O1 ~ O4 优化级别越来越高,O4 最高。
为了支持user/kernel mode,处理器会有两种操作模式,第一种是user mode,第二种是kernel mode。当运行在kernel mode时,CPU可以运行特定权限的指令(privileged instructions);当运行在user mode时,CPU只能运行普通权限的指令(unprivileged instructions)。特殊权限指令主要是一些直接操纵硬件的指令和设置保护的指令,例如设置page table寄存器、关闭时钟中断。在处理器上有各种各样的状态,操作系统会使用这些状态,但是只能通过特殊权限指令来变更这些状态。在处理器里面有一个flag。在处理器的一个bit,当它为1的时候是user mode,当它为0时是kernel mode。当处理器在解析指令时,如果指令是特殊权限指令,并且该bit被设置为1,处理器会拒绝执行这条指令,就像在运算时不能除以0一样。如果你在用户空间(user space)尝试执行一条特殊权限指令,用户程序会通过系统调用来切换到kernel mode。当用户程序执行系统调用,会通过ECALL触发一个软中断(software interrupt),软中断会查询操作系统预先设定的中断向量表,并执行中断向量表中包含的中断处理程序。中断处理程序在内核中,这样就完成了user mode到kernel mode的切换,并执行用户程序想要执行的特殊权限指令。每一个进程都会有自己独立的page table,这样的话,每一个进程只能访问出现在自己page table中的物理内存。操作系统会设置page table,使得每一个进程都有不重合的物理内存,这样一个进程就不能访问其他进程的物理内存,因为其他进程的物理内存都不在它的page table中。一个进程甚至都不能随意编造一个内存地址,然后通过这个内存地址来访问其他进程的物理内存。这样就给了我们内存的强隔离性。需要有一种方式能够让应用程序可以将控制权转移给内核(Entering Kernel)。在RISC-V中,有一个专门的指令用来实现这个功能,叫做ECALL。ECALL接收一个数字参数,当一个用户程序想要将程序执行的控制权转移到内核,它只需要执行ECALL指令,并传入一个数字。这里的数字参数代表了应用程序想要调用的System Call。