Xv6 Operating system 01-interface

操作系统的职责是在多个程序(进程)之间共享计算机提供的物理资源,并提供一系列比直接操作硬件更有用的服务。具体地说,操作系统为计算机硬件提供一层抽象,使软件程序不需要关心硬件的具体实现。操作系统还必须通过某种时分复用的机制使得多个程序可以共享底层硬件。最后,操作系统也必须为不同程序进程提供某种通信机制。

操作系统中,运行的程序被抽象成进程,一个进程的内容即它所占用的内存及其他资源。其中,在内存中存有进程的指令,数据,堆栈等运行时上下文。操作系统接口即操作系统内核为其他进程提供的一系列服务的接口,称为系统调用 (system call) 。当进程需要调用系统服务时,其只能通过调用操作系统接口实现.

xv6 提供的系统调用有:

系统调用 描述
int fork() 创建一个进程,返回子进程的PID
int exit(int status) 终止当前进程,并将状态报告给wait()函数。无返回
int wait(int *status) 等待一个子进程退出; 将退出状态存入*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表示read/write;返回一个fd(文件描述符)
int write(int fd, char *buf, int n) 从buf 写n 个字节到文件描述符fd; 返回n
int read(int fd, char *buf, int n) 将n 个字节读入buf;返回读取的字节数;如果文件结束,返回0
int close(int fd) 释放打开的文件fd
int dup(int fd) 返回一个新的文件描述符,指向与fd 相同的文件
int pipe(int p[]) 创建一个管道,把read/write文件描述符放在p[0]和p[1]中
int chdir(char *dir) 改变当前的工作目录
int mkdir(char *dir) 创建一个新目录
int mknod(char *file, int, int) 创建一个设备文件
int fstat(int fd, struct stat *st) 将打开文件fd的信息放入*st
int stat(char *file, struct stat *st) 将指定名称的文件信息放入*st
int link(char *file1, char *file2) 为文件file1创建另一个名称(file2)
int unlink(char *file) 删除一个文件

进程和内存

xv6 的进程拥有用户态内存(指令,数据,堆栈)和由内核管理的进程状态。xv6 在进程之间时分复用处理器,即它透明地切换待执行的进程进入处理器执行。当进程被切换出处理器不再执行时,xv6 会为其保存 CPU 寄存器(保存上下文),并在该进程下次执行时将保存的 CPU 寄存器重新加载 (恢复上下文)。操作系统内核为每个进程分配一个标识符,即 PID, 作为该进程的索引。

fork 系统调用

  1. 一个进程可以使用 fork 系统调用创建一个新进程
  2. 新创建的进程具有和 caller process 相同的内存内容,称为子进程
  3. fork 函数创建子进程成功后立刻返回到 caller process ,返回值是子进程的 PID。在子进程中,fork 返回值是 0, 可以利用这一点为父进程和子进程设计不同的执行分支

exit 系统调用

  1. 一个进程可以使用 exit 系统调用终止自己的执行,并释放自己占用的所有资源
  2. exit 接受一个参数用于指示 caller process 的退出状态,按照惯例,状态 0 表示正常退出,状态 1 表示进程因错误退出。

wait 系统调用

  1. 一个进程可以使用 wait 系统调用等待其子进程执行完成,同时阻塞自己的执行
  2. 如果 caller process 的子进程之一退出,wait 返回退出子进程的 PID,如果 caller process 没有子进程, wait 立即返回 -1
  3. wait 接受一个指针作为参数,子进程的退出状态将被拷贝到该指针位置. 如果 caller process 不关心子进程的退出状态,可以传递空指针 (0)

exec 系统调用

  1. 一个进程可以使用 exec 系统调用将自己的进程内存替换为从文件系统中新加载的内存镜像,并保留原进程的文件描述符表。
  2. exec 函数执行成功后并不返回 caller process ,它会直接执行其内存中的指令(即从文件系统加载的指令). exec 只有在错误时 (加载文件或替换进程内存时的错误,而非新的程序执行时的错误) 才返回, 因此 caller process 中 exec 函数之后的程序应该只有错误处理过程。

IO 和文件描述符(file descriptors)

文件描述符是一个整数值,其代表一个指向内核管理的对象的指针。进程可以通过 open 文件、文件夹、或设备,创建 pipe, 或 dup 一个已有的文件描述符来获取文件描述符。

文件描述符旨在提供一种抽象的接口,它使得操作系统管理的 IO 设备在接口上没有差异,都是字节流的形式。

值得注意的是文件描述符的管理是逐 process 的,操作系统会为每个 process 维护一张文件描述符表。其中三个特殊的文件描述符是:

  • 0 代表标准输入 (standard input)
  • 1 代表标准输出 (standard output)
  • 2 代表标准错误 (standard error)

文件描述符的数值会递增地分配,即: 新获取的文件描述符将总是所有未占用文件描述符中数值最小的。

readwrite 系统调用

readwrite 从文件描述符指向的已打开的文件中读取或向其写入字节。

对于指向文件的文件描述符,其总是具备一个偏移量 offset,该 offset 指示该从该文件读取或向该文件写入的字节起点。

  • 对于 readread(fd, buf, n) 表示从文件描述符 fd 指向的文件中读取最多 n 个字节,并将读取的数据复制到指针 buf 所指向的内存中,同时返回本次读取到的字节长度。值得注意的是 read 会从文件描述符 fd 所具备的 offset 位置开始读取,并在读取后将 offset 的值递增到本次读取的终点,即:下次对 fd 的读取将从本次读取的终点之后开始。当 offset 到达文件的终点时,新的 read 调用将返回 0, 代表文件已读取完毕。
  • 对于 write, write(fd, buf, n) 表示向文件描述符 fd 指向的文件中写入 n 个来自指针 buf 指向的内存位置的字节。同样地,write 会返回本次写入的字节长度,当返回值小于 n 时,代表本次写入有错误发生。对于 offset 的处理也与 read 类似:写入总是从 offset 的位置开始,并在结束后将 offset 递增到本次写入的终点。

关于文件描述符的 offset:

如果一个文件描述符是从另一个已经存在的文件描述符得到的,即通过 fork 复制文件描述符表或 dup 复制文件描述符的方法,那么该文件描述符与原文件描述共享同一个 offset。否则,新的文件描述符(如通过 open 同一个文件)即使指向相同的 IO 设备,也不会共享 offset

open 系统调用

open 系统调用打开一个文件并为其分配文件描述符,open(filename, flag) 表示以 flag 模式打开文件 filename. 可选的 flag 有:

  1. O_RDONLY: 只读模式
  2. O_WRONLY: 只写模式
  3. O_RDWR: 读写模式
  4. O_CREATE: 文件不存在则创建
  5. O_TRUNC: 截断模式,打开时将文件内容清空

close 系统调用

close 系统调用释放一个文件描述符,使其在后续的分配中可用。

操作系统会保证新分配的文件描述符总是所有可用文件描述符中数值最小的(否则随着系统的运行,文件描述符的数值终将导致溢出)

dup 系统调用

dup 系统调用复制一个已有的文件描述符并返回,两个文件描述符将指向同一个 IO 设备,并共享一个 offset

管道 (pipe)

pipe 是一个小型内核缓冲区,其以==一对==文件描述符的形式暴露给用户进程:一个用于读取,另一个用于写入。读取端文件描述符可以读取向写入端文件描述符写入的数据,内核以这种形式向用户态提供了一种跨进程通信的方式。

pipe 以如下方式调用:

1
2
3
4
int pipefds[2];
pipe(pipefds);
// pipefds[0] is for reading
// pipefds[1] is for writing

值得注意的是 pipe 以==阻塞式语义==进行读写,即: 对读取端文件描述符的读取操作 (read) 会等待写入端的写入。

这一特性还可以重述为:对读取端文件操作符的读取会一直阻塞到写入端文件描述符有数据写入。另一种结束读取端阻塞的情况是: 所有指向 pipe 写入端的文件描述符都被关闭,此时对 pipe 读取端的 read 操作会返回 0,与对非 pipe IO 设备 read 时遇到 EOF 相似。

跨进程通信也可以通过向临时文件读写数据实现,但 pipe 相比这种方式至少有以下四点优势:

  1. pipe 会自动清理其缓冲区,临时文件则需要在通信结束后手动处理
  2. pipe 可以传递任意长度的数据,临时文件则受到文件系统的限制
  3. pipe 允许读写阶段的并行执行,临时文件方案则需要两个阶段逐个执行
  4. 当使用 pipe 实现进程间通信时,其阻塞式读写语义将比文件的非阻塞式语义更高效

对于 3, 4,一个理解是:

  1. pipe 的阻塞式读写使得读取端对写入端有感知,因此可以在写入端写入一段数据后立刻读取,而不用等待其全部写入后再读取,因此是并行的。而文件方案的读取端对文件的写入没有感知,因此必须等待写入端写完数据并显式通知读取端才可以读取,否则很可能读到缺少的或重复的数据。
  2. 还是因为 pipe 的阻塞读写语义,在构建跨进程通信程序时不需要在如何同步读写操作上花费功夫,因此更加高效

文件系统(File system)

文件系统中,文件名 (file name / file path) 只是对文件系统中文件本体的一个具名引用。文件系统中的文件本体叫做 inode.

一个 inode 可以有多个文件名,称为 links. 一个 link 即目录中的一个条目,包含一个文件名和一个指向 inode 的引用。

inode 描述了文件的元数据 metadata, 包括文件的类型 (文件/目录/设备),长度,文件在磁盘上的地址,该文件的 link 数目。

fstat 系统调用

fstat 系统调用通过一个文件描述符查询该文件描述符所指向的 inode 的信息,其结果以结构体 stat 的形式返回:

1
2
3
4
5
6
7
8
9
10
11
#define T_DIR    1
#define T_FILE 2
#define T_DEVICE 3

struct stat {
int dev; // disk device of the file system
uint ino; // inode number
short type; // type of file
short nlink; // number of links
uint64 size; // size of the file in bytes
}
  • chdir 系统调用可以改变当前进程所在的当前文件夹。当计算相对路径时,进程的当前文件夹被用作参照
  • open 系统调用当使用 O_CREATE flag 时可以创建新的文件
  • mkdir 系统调用用于创建新的文件夹
  • mknod 系统调用于创建新的其他设备文件。具体地说,mknod 为一个已有的设备创建一个文件,这个文件的路径指向该设备。

    该设备的主设备号和次设备号需要作为参数传入 mknod, 并于新创建的设备文件关联。这样一来,当用于进程访问该设备文件时,内核会将对该设备文件的 readwrite 系统调用转发给内核中的设备实现,而不是调用文件系统中的实现。

  • link 系统调用为一个已有文件创建一个新的 link
  • unlink 系统调用

    unlink 系统调用从文件系统删除一个 link

{% note primary %}
    在典型 unix 系统中,一个文件的 `inode` 以及文件在磁盘中的本体只有在文件系统中没有 `link` 指向该 `inode`, 并且所有进程中都不存在指向该文件的文件描述符时才会被释放和删除。
    {% endnote %}