[MIT 6.1810]Xv6 Chapter 1
Operating system interfaces
操作系统的工作是:
- 在多个程序间共享一台电脑
- 提供一套比硬件更有用的服务
操作系统:
- 管理并抽象底层硬件
- 在多个程序间共享硬件
- 提供一种受控的程序交互方式
操作系统是通过接口来向程序提供服务的。
每个进程(运行中的程序),都有指令、数据、栈。
进程通过系统调用(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
需要两个参数:可执行文件的名称和一个字符串数组,包含传递给新进程的参数。大多数程序通常会忽略掉字符串数组的第一个参数,一般是程序的名字。
为什么 fork
和 exec
是分离的
操作系统利用这种分离机制实现IO重定向。同时,为了避免创建一个子进程然后马上调用 exec
调换掉内存,操作系统通过虚拟内存的一些技巧来优化,例如copy on write。
申请内存
xv6大多数时候隐式申请用户空间的内存,但是进程也可以通过调用 sbrk(n)
来申请更多的内存空间,该函数返回申请内存的起始地址。
1.2 I/O and File descriptors
文件描述符是一个整数,代表着一个内核管理的object,进程可以从这个object读,或向这个object写。
一个进程可以通过
- 打开文件、目录、设别
- 创建管道;
- 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 |
|
关键点是子进程先关闭了文件描述符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
通过 fork
和 dup
获得的文件描述符和都共享相同的偏移。
1.3 Pipes
管道是内核中的一个小缓冲区,通过一对文件描述符暴露给进程。这是进程间通信的一种方式。
pipe
系统调用
该系统调用创建一个新的管道,并将一对文件描述符放到参数指定的整数数组中。
如果没有可读数据,读一个管道可能会:
- 阻塞直到数据到来
- 阻塞直到所有对管道写端的文件描述符被关闭
第二种情况会返回0。
管道相较临时文件的优势:
- 管道会自动清理
- 管道可以处理任意长度的数据,而临时文件必须要求硬盘有足够的空间
- 管道允许管道的不同阶段并行执行
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 |
|
当进程调用close或者退出后,这个inode会被自动清理。