Chapter 1 Operating system interfaces
操作系统负责管理和抽象底层硬件,为上层应用提供接口。
设计一个好的接口是困难的:一方面,我们希望接口是简单且有限的,这样就能简单地正确实现;另一方面,我们希望为应用提供高级的特性。解决的思路是,设计仅依赖少数机制的接口,更好的满足普遍性要求。
xv6 提供了类似 Unix 的基础界面接口。xv6 包括传统形式的内核,即提供运行程序基础服务的一个特殊程序。其它程序(被称为进程)分别拥有内存来容纳指令、数据和栈。通常来说,一台计算机包括若干进程和一个内核。当进程需要调用系统服务时,进程使用系统调用 system call 。系统调用进入内核,由内核执行服务,然后退出。这样就使得进程交替地在用户态 user space 和内核态 kernel space 运行。xv6 提供了 Unix 系统调用的一个子集。
内核使用 CPU 提供的硬件保护机制来确保每个进程仅能访问自己分配到的内存
Processes and Memory
每个 xv6 进程都包括用户空间的内存和由内核私有的状态。内核为每一个进程分配了一个独有的 PID 号用于识别。
fork 系统调用可以用于创建子进程,子进程与父进程有着相同的内存内容,但实际使用的内存和寄存器是不相同的。 exit 系统调用停止程序运行并释放进程使用的资源。wait系统调用等待一个子进程结束,返回子进程的 PID 。exec 系统调用使用一个符合 ELF 格式的程序来替换当前内存空间中运行的程序。
xv6 shell 使用 fork 和 exec 这两个系统调用来代表用户运行进程。shell 读入用户输入,然后创建一个子进程,子进程使用 exec 调用用户指定的程序,父进程等待子进程结束运行。为避免浪费,kernel 在调用 fork 时使用了一些虚拟内存的技术,如写时复制 copy-on-write。
xv6 在几乎完全负责内存分配。fork 分配内存使得子进程能完整复制父进程的内存内容,exec 分配能容纳整个运行程序的内存,而运行时进程可以调用 sbrk 来手动申请更多的内存。
I/O and File descriptors
kernel 管理着文件描述符 file descriptor 用于进程的文件输入输出。具体的可以是文件、目录、设备或管道,文件描述符这一抽象让我们可以将这些东西统一当作文件来看待。
每个进程的文件描述符都是私有的,且从 0 开始编号。通常来说,进程从 0 standard input 输入,输出到 1 standard output ,同时将错误信息输出到 2 standard error。 shell 确保每一个进程在运行之前就已经设置好了这三个文件描述符。
read 和 write 这两个系统调用通过文件描述符来对文件进行读写,形式为 read(fd, buf, n),返回实际读取或者写入的字节数(若读取的字符数不足要求的数量,说明可能到达文件末尾或出现错误;若写入的字节数不足要求的数量,说明可能遇到了错误)。
文件描述符通过 close 系统调用关闭。这一系统调用释放文件描述符使用的资源,使后续 open, pipe, dup 等调用能够使用这一编号。新分配的文件描述符总是所有可分配编号中最小的。
在进行 fork 的时候,fork 会将父进程的所有内存内容复制到子进程,自然也就包括文件描述符,所以子进程有着与父进程完全一致的文件描述符。同样, exec 系统调用将用新的程序替换整个内存空间,但保留已经打开的文件描述符。这样的特性使得 shell 在实现文件重定向或是为子进程设置初始标准输入输出时尤为方便。
dup 系统调用复制一个已有的文件描述符,返回新建立的文件描述符编号(满足最小可用性质)。复制的文件描述符与原描述符共享同一个文件偏移量,对一个进行读写等价于对另一个进行同样操作。
Pipes
pipe 是一个由内核提供的缓冲区,它提供给进程一对文件描述符,分别用于输入和输出,以此提供一种进程间通信的方式。进程在管道一侧写入,在另一侧读出。使用 pipe(int a[2]) 来创建一个管道,默认 a[0] 用于读,a[1] 用于写。当管道中没有数据时,调用 read 会阻塞调用 read 的进程,直到管道中有写入数据或是持有写端文件描述符的所有进程均关闭了该文件描述符。
在 shell 中提供了简单的方式创建类似 pipe 的进程间通信,如 a | b | c 会用管道依次将前后进程的输出输入连接起来。使用这一技巧还能代替很多使用临时文件作为中转的方法。
File system
xv6 文件系统提供数据文件,将所有的文件夹组织成树的结构。
系统调用 chdir 用于切换当前所在文件目录。mkdir 创建新文件夹。mknod 创建新设备文件,对于这类设备文件,尽管仍然是用 read 和 write 来进行读写,但内核会使用对应设备的实现来代替原本的对文件系统的读写。
文件名与文件本身是区别开的。同样一个底层文件 inode 可以有多个名字 links,每个 link 都包括一个在目录树中的条目,这个条目包括文件名和对 inode 的引用。inode 同时还持有文件的元数据 metadata,包括其类型(文件 or 文件夹 or 设备)、长度、其内容在磁盘上的位置、指向这个 inode 的 link 数。
系统调用 fstat 可以得到文件描述符指向的 inode 的信息。系统调用 link 则是将一个新文件与一个旧文件所拥有的 inode 链接起来。每个 inode 都有独特的 inode number。对应的有 unlink 系统调用,当 inode 的 link 数为 0 时且没有文件描述符引用到它时,这个 inode 就会被删除。
大部分与文件有关的操作都是用户层面的程序,可以使用 shell 调用。一个例外是 cd,它是 shell 内置的,因为 cd 必须能切换 shell 的当前工作目录,若按照一般创建子进程运行的方法,只会修改子进程的工作目录。
Real world
xv6 系统参照了很多 POSIX 标准中的内容,但并没有完全实现 POSIX,所以它并不是 POSIX 标准 OS。