MIT6.S081 xv6book chapter2
第二章以操作系统三个要求:复用、隔离和交互展开讲述了内核设计、进程设计,还描述了xv6的启动流程。
看完这一章还是很笼统抽象,一些细节还是需要等到后续披露,但这时候你大致把握到操作系统的整体设计了。
隔离的设计——用户态与内核态;复用的设计——用户进程;交互设计——进程通信。
物理资源的抽象
禁止应用程序直接访问敏感的硬件资源,而是将资源抽象为服务。
比如 open, read, write, and close系统调用,文件系统的抽象让应用程序只需提供path name就能访问资源的便利,而底层硬盘的读写全都有操作系统进行。
Unix 透明地在进程之间切换硬件 CPU,根据需要保存和恢复寄存器状态,因此应用程序不必担心共享。
用户态与内核态
强隔离型要求应用程序和操作系统有一个硬边界,我们不希望一个失败的应用程序影响其他应用程序,甚至是操作系统。
那么,CPU在硬件级别上提供了强隔离型的支持:三种模态
- machine mode 通常是用于系统启动,配置;
- supervisor mode 用于执行特权命令,内核运行在kernel space;
- user mode 应用程序运行在user space。
如果一个应用程序想要调用一个内核函数(比如说系统调用),那么必须切换模式到内核态(比如riscv 提供的ecall指令),有内核执行系统调用。
用户态与内核态就是一个隔离,用户进程不能直接执行特权指令,这也就避免了危险发生。
内核设计
A key design question is what part of the operating system should run in supervisor mode.
操作系统的哪一部分应该常驻在内核中呢?根据设计的不同,内核设计也分为 monolithic kernel和micro kernel。
一体化内核中,操作系统全部运行在内核中,因此只要出现一个错误,操作系统整个完蛋。而微内核设计只将必要的代码运行在内核中。
微内核设计与一体化内核的不同:微内核充当消息转发者,比如shell想要读写某个文件,微内核将这个消息发送给file server。
在微内核中,内核接口由一些低级函数组成,用于启动应用程序、发送消息、访问设备硬件等。这种组织方式使内核相对简单,因为大多数操作系统都驻留在用户级服务器中。
不管微内核还是一体化内核,重要的是它们都实现一些key ideas:
They implement system calls, they use page tables, they handle interrupts, they support processes, they use locks for concurrency control, they implement a file system, etc.
这些key idea是值得我们去学习借鉴的。
进程总览
进程是实现隔离性的一个单元。进程抽象可防止一个进程破坏或监视另一个进程的内存、CPU、文件描述符等。它还可以防止进程破坏内核本身,因此进程无法破坏内核的隔离机制。
为了实现隔离性,进程为用户程序提供了一个幻觉,在程序看来,它好像掌握了整台机器。用户程序独占了机器一整片的地址空间,其他用户程序无法读写。用户程序还觉得自己独占了CPU来执行自己的指令。
地址空间
地址空间的幻觉由页表实现(第三章将详细讲述)。简单来说,进程看到的是虚拟地址空间,通过页表映射到真实的物理空间。xv6 为每个进程维护了不同的页表,这样就能够合理地定义进程的地址空间了。
进程的地址空间从零开始,一直到最大虚拟地址。
地址空间的布局:Instructions come first, followed by global variables, then the stack, and finally a “heap” area (for malloc) that the process can expand as needed. At the top of the address space xv6 reserves a page for a trampoline and a page mapping the process’s trapframe.
Xv6 uses these two pages to transition into the kernel and back; the trampoline page contains the code to transition in and out of the kernel and mapping the trapframe is necessary to save/restore the state of the user process
xv6 使用结构体 struct proc
来维护一个进程的状态,其中最为重要的状态是进程的页表,内核栈,当前运行状态。
线程
每个进程都有一个执行线程(或简称线程),用于执行进程的指令。线程可以挂起,以后再恢复。
为了在进程之间透明地切换,内核会挂起当前正在运行的线程并恢复另一个进程的线程。
线程的大部分状态(局部变量、函数调用返回地址)都存储在线程的堆栈中。
xv6大概是一个进程只有一个线程,因此两者的区别不大。
两个栈
Each process has two stacks: a user stack and a kernel stack (p->kstack). When the process is executing user instructions, only its user stack is in use, and its kernel stack is empty. When the process enters the kernel (for a system call or interrupt), the kernel code executes on the process’s kernel stack; while a process is in the kernel, its user stack still contains saved data, but isn’t actively used.
每个进程都有两个栈,用户栈和内核栈。程序在运行时只有用户栈在使用,内核栈为空。当程序进入内核时,内核栈被使用,用户栈可以被使用。
总结
In summary, a process bundles two design ideas: an address space to give a process the illusion of its own memory, and, a thread, to give the process the illusion of its own CPU. In xv6, a process consists of one address space and one thread. In real operating systems a process may have more than one thread to take advantage of multiple CPUs.
总之,进程捆绑了两个设计思想:一个是地址空间,用于为进程提供其自身内存的错觉,另一个是线程,用于为进程提供其自身 CPU 的错觉。
在 xv6 中,进程由一个地址空间和一个线程组成。在实际操作系统中,一个进程可能具有多个线程来利用多个 CPU。
编译运行kernel
首先,Makefile(XV6目录下的文件)会读取一个C文件,例如proc.c;之后调用gcc编译器,生成一个文件叫做proc.s,这是RISC-V 汇编语言文件;之后再走到汇编解释器,生成proc.o,这是汇编语言的二进制格式。
Makefile会为所有内核文件做相同的操作,比如说pipe.c,会按照同样的套路,先经过gcc编译成pipe.s,再通过汇编解释器生成pipe.o。
之后,系统加载器(Loader)会收集所有的.o文件,将它们链接在一起,并生成内核文件。这里生成的内核文件就是我们将会在QEMU中运行的文件。
我们来看传给QEMU的几个参数:
- -kernel:这里传递的是内核文件(kernel目录下的kernel文件),这是将在QEMU中运行的程序文件;
- -m:这里传递的是RISC-V虚拟机将会使用的内存数量;
- -smp:这里传递的是虚拟机可以使用的CPU核数;
- -drive:传递的是虚拟机使用的磁盘驱动,这里传入的是fs.img文件。
xv6,启动!
To make xv6 more concrete, we’ll outline how the kernel starts and runs the first process.
不必拘泥于细节,掌握启动流程。
MIT6.S081 xv6book chapter2
https://xyz.desirer233.fun/2024/01/07/MIT6.S081/book/chapter2/