[MIT 6.1810]Xv6 Chapter 1

Operating system interfaces

操作系统的工作是:

  1. 在多个程序间共享一台电脑
  2. 提供一套比硬件更有用的服务

操作系统:

  1. 管理并抽象底层硬件
  2. 在多个程序间共享硬件
  3. 提供一种受控的程序交互方式

操作系统是通过接口来向程序提供服务的。

每个进程(运行中的程序),都有指令、数据、栈。

进程通过系统调用(system call)来使用操作系统提供的服务。

1.1 Processes and memory

进程由用户空间的内存(指令、数据、栈)和一些状态组成。xv6是分时操作系统,每个进程都有一个唯一的PID来标识。

fork 系统调用

fork系统调用完全复制调用者的内存给新进程,然后在原进程和新进程中返回,原进程中返回值为新进程的PID,新进程中的返回值为0。

需要注意的是,父子进程之间的内存空间是相互独立的,即每个进程都有一份自己的拷贝。

wait 系统调用

wait系统调用返回当前进程的一个退出的子进程的PID,并且将子进程的退出状态拷贝到指定内存中。如果没有退出的子进程,wait阻塞等待。如果当前进程没有子进程,wait立即返回-1。如果不关心子进程的退出状态,可以传入一个0地址。

exec系统调用

exec系统调用负责用存储在文件系统中的可执行文件加载新的内存镜像,替换调用进程的内存。这个可执行文件必须遵循特定格式,定义了文件的各个部分,例如哪些部分包含指令和数据,以及从哪个指令开始执行。在 xv6 中,使用的是 ELF(可执行与可链接格式),详见第三章。

通常,这个可执行文件是通过编译程序源代码生成的。当 exec 成功调用时,控制权不会返回到原程序,而是从 ELF 头部指定的入口点开始执行新加载的指令。exec 需要两个参数:可执行文件的名称和一个字符串数组,包含传递给新进程的参数。大多数程序通常会忽略掉字符串数组的第一个参数,一般是程序的名字。

为什么 forkexec 是分离的

操作系统利用这种分离机制实现IO重定向。同时,为了避免创建一个子进程然后马上调用 exec 调换掉内存,操作系统通过虚拟内存的一些技巧来优化,例如copy on write。

申请内存

xv6大多数时候隐式申请用户空间的内存,但是进程也可以通过调用 sbrk(n) 来申请更多的内存空间,该函数返回申请内存的起始地址。

1.2 I/O and File descriptors

文件描述符是一个整数,代表着一个内核管理的object,进程可以从这个object读,或向这个object写。

一个进程可以通过

  1. 打开文件、目录、设别
  2. 创建管道;
  3. duplicating一个文件描述符

来获得文件描述符。

文件描述符这种抽象使得我们可以不用关注文件的种类,无论是文件、管道、设备,我们都将他们视为字节流来处理。

内部,xv6内核将文件描述符用作每个进程的打开文件表的索引。通常,一个进程从文件描述符0读取输入(标准输入),向文件描述符1输出(标准输出),将错误信息输出到文件描述符2(标准错误)。

read(fd, buf, n) 系统调用

该系统调用从fd指向的object读取至多n个字节,拷贝到buf内,并返回读取的字节数。

每个文件描述符都有一个关联的偏移量,read 会更新这个偏移量,指向下一个未被读取的字节。

当没有更多的字节可读时,read 会返回0。

write(fd, buf, n) 系统调用

该系统调用向fd写n个字节,返回实际写入的字节数。只有当错误发生的时候,实际写入的字节数才不等于n。

close(fd) 系统调用

该系统调用释放一个文件描述符,使之可以为之后的文件使用。

一个新的文件描述符总是当前进程中最小的未使用的文件描述符。

fork 和文件描述符

fork 会将父进程的打开文件表复制给子进程,子进程和父进程的打开文件是一样的。

exec 会替换调用进程的内存,但是会保留打开文件表。

这种行为使得shell可以简单的实现IO重定向。

cat < input.txt 的简单实现

1
2
3
4
5
6
7
8
9
10
char *argv[2];
argv[0] = "cat";
argv[1] = 0;
if (fork() == 0)
{
// child
close(0);
open("input.txt", O_RDONLY);
exec("/bin/cat", argv);
}
C

关键点是子进程先关闭了文件描述符0,然后再打开input.txt,这是操作系统会将文件描述符0分配给这个文件。

父进程的文件描述符并不会被改变,因为子进程有一张自己的打开文件表。

需要注意的是,虽然fork会复制打开文件表,但是每个文件描述符关联的偏移在父子进程间是共享的。

open 系统调用

该系统调用打开一个文件,并返回相应的文件描述符。

第二个参数可以是(kerenl/fcntl.h):

  • O_RDONLY
  • O_WRONLY
  • O_RDWR
  • O_CREATE
  • O_TRUNC

dup(fd) 系统调用

该系统调用复制一个文件描述符,返回一个指向相同object的文件描述符。这两个文件描述符共享偏移,类似fork。

Two file descriptors share an offset if they were derived from the same original file descriptor by a sequence of fork and dup calls. Otherwise file descriptors do not share offsets, even if they resulted from open calls for the same file

通过 forkdup 获得的文件描述符和都共享相同的偏移。

1.3 Pipes

管道是内核中的一个小缓冲区,通过一对文件描述符暴露给进程。这是进程间通信的一种方式。

pipe 系统调用

该系统调用创建一个新的管道,并将一对文件描述符放到参数指定的整数数组中。

如果没有可读数据,读一个管道可能会:

  • 阻塞直到数据到来
  • 阻塞直到所有对管道写端的文件描述符被关闭

第二种情况会返回0。

管道相较临时文件的优势:

  1. 管道会自动清理
  2. 管道可以处理任意长度的数据,而临时文件必须要求硬盘有足够的空间
  3. 管道允许管道的不同阶段并行执行

1.4 File system

xv6文件系统提供数据文件,包含字节流;提供目录,包含指向数据文件和其他目录文件的引用。

目录以树的形式组织,以/开始的路径以根目录为根,不以/开始的路径以当前进程当前的工作目录为根(可以通过chdir系统调用来改变)。

mkdir 系统调用创建一个新的目录,open 系统调用通过指定 O_CREATE 选项来创建一个新的数据文件, mknode 系统调用创建一个新的设备文件。该系统调用通过两个参数来唯一标识一个内核设备(主、副设备号)。

文件名和文件本身是不同的。同一个文件由一个唯一的inode来标识;同一个文件可以有多个文件名,即链接。

每个链接由一个目录中的目录项组成,这个目录项包含一个文件名以及一个指向文件inode的指针。

每个inode保存着文件的元数据,包括种类,长度,磁盘位置,以及链接数等。

fstat 系统调用

该系统调用从inode中获取文件的元数据,并将数据填入 stat 结构体中。(kernel/stat.h)

link 系统调用

该系统调用创建一个链接。读写该链接和读写原文件操作的是同一个文件。

unlink 系统调用

该系统调用删除一个链接。只有当指向同一个inode的所有链接都被删除的时候,才会真正释放inode和文件内容。

一种约定俗称的创建一个临时inode方法:

1
2
fd = open("/tmp/xyz", O_CREATE | O_RDWR);
unlink("/tmp/xyz");
C

当进程调用close或者退出后,这个inode会被自动清理。


[MIT 6.1810]Xv6 Chapter 1
https://erlsrnby04.github.io/2024/09/24/MIT-6-1810-Xv6-Chapter-1/
作者
ErlsrnBy04
发布于
2024年9月24日
许可协议