一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
引
如果想要了解操作系统,浅显的通过知识点的堆积往往是不够的,我们或许可以通过代码去进行学习,但是如果选择一上来就通过Linux源码阅读的话,或许会有一些障碍。译者曾于2021春基于xv6完成了操作系统实验,个人认为阅读xv6是理解操作系统的一个好方法,所以译者尝试针对最新的xv6-riscv文档进行翻译,并且会尝试在文档中加入一些自己的理解或思考,希望可以加深自己对xv6/操作系统的理解。
让我们开始吧。
Chapter 1 操作系统接口
操作系统的职责是让多个程序分享一台电脑,并且给程序提供比单个硬件更多的服务能力;同时其管理和抽象低层次的硬件,比如一个字符处理程序并不需要他正在使用的是什么类型的硬盘;此外,其能够给多个程序分享硬件从而使这些程序可以看起来同时运行;最后,操作系统控制着程序之间的交互,从而使得程序们能够分享数据或者一起工作。
操作系统通过接口给用户程序提供服务。而设计好的接口是很困难的一件事情:一方面我们希望接口能够简单而且精准,因为这能够让接口更容易被正确的实现;另一方面,我们可能会尝试提供许多复杂的特性给应用程序(译者注:这种对接口的需求使得我们需要控制好接口的粒度)。解决这种窘境的一个小技巧就是设计仅依靠很少的机制的接口,而这些接口又可以被组合起来提供更普遍的功能(译者注:例如fork与exec的组合)。
本书通过一个小的操作系统xv6作为实际的例子来解释操作系统的概念。xv6提供了Unix中基础的接口,同时又模仿了Unix的内部设计。Unix提供了一系列精准的、组合起来可以提供让人惊讶的普遍性的接口。这些接口是如此的成功以至于像BSD、Linux等操作系统都有Unix风格的接口。理解xv6是理解其他这些操作系统的好的开始。
如图1.1所示,xv6采取了传统的内核(kernel) 形式,作为一个特殊的提供服务给运行中程序的程序存在。每一个运行的程序,我们将其称为进程(process) ,存储着指令、数据和一个运行栈。指令(instructions)实现这个程序的运算(译者注:例如a=a+1),而数据(data)则是运算的对象,而栈(stack)则组织了这个程序的过程调用。一个计算机可能会有很多个进程,但是却只能由一个内核。
当进程需要调用一个系统服务(译者注:例如文件操作)时,它会发起一个系统调用(system call) ,而这些系统调用就是操作系统提供的接口。这些系统调用进入内核,内核提供服务然后再返回:所以一个进程总是在用户空间(user space) 和内核空间(kernel space) 交替执行。
内核通过CPU提供的硬件保护机制来确保每一个在用户空间的进程都只能获取它自己的内存空间。内核拥有实现保护机制所需要的硬件特权,但是用户程序并没有硬件特权。当用户程序调用系统调用时,硬件提高特权级然后开始执行在内核中预先安排好的方法。
内核提供的系统调用的集合就是用户程序可以使用的接口。xv6提供了Unix内核通常提供的系统调用的一个子集,下标展示了xv6所有的系统调用。
系统调用 | 描述 |
---|---|
int fork() | 创建进程,返回子进程PID(process id) |
int exit(int status) | 结束当前进程,报告状态给wait(),无返回 |
int wait(int *status) | 等待子进程调用exit(),退出状态在*status中,返回子进程PID |
int kill(int pid) | 结束PID号进程,返回0,错误返回-1 |
int getpid() | 返回当前进程PID |
int sleep(int n) | 等待n个时钟 |
int exec(char *file, char *argv[]) | 载入文件并且执行,仅在错误时返回 |
char *sbrk(int n) | 增长n字节进程内存,返回新内存开始地址 |
int open(char *file, int flags) | 打开文件,flags指定文件模式,返回文件描述符 |
int write(int fd, char *buf, int n) | 从buf中写n字节到文件描述符,返回n |
int read(int fd, char *buf, int n) | 读n字节到buf中,返回读的数量,如果文件结束返回0 |
int close(int fd) | 释放一个开启的文件描述符 |
int dup(int fd) | 返回一个指向相同文件描述符的文件描述符 |
int pipe(int p[]) | 创建一个管道 |
int chdir(char *dir) | 改变当前目录 |
int mkdir(char *dir) | 创建新目录 |
int mknod(char *file, int, int) | 创建一个设备文件 |
int fstat(int fd, struct stat *st) | 将一个开启文件信息放到*st中 |
int stat(char *file, struct stat *st) | 将一个有名文件放到*st中 |
int link(char *file1, char *file2) | 给file1创建其他名(file2) |
int unlink(char *file) | 移除一个文件 |
本章的其他部分简述了xv6提供的服务:进程、内存、文件描述符、管道和一个文件系统,并且针对shell如何使用他们给出了代码和讨论,在shell上使用的系统调用展现了他们是如何被仔细的设计的。
shell是一个从用户读取命令并且执行他们的普通的程序(译者注:比如我们经常用的命令行工具)。shell是一个用户程序而不是内核的一部分展示了操作系统接口的强大:shell并没有什么特殊的,这也意味着shell是可以被替换的,因而线代的Unix操作系统有一系列的shell可供选择,这些shell都拥有他们自己的用户接口和脚本特征。xv6 shell本质上是一个Unix Bourne shell的简单实现。它的实现可以在这里被找到。
今天的主要内容是对xv6做了一个初步的概括,因为其高度概括性,所以很难有代码进行讲解,在后续的部分,将会尝试结合代码进行讲解。
感谢阅读。