MIT6.S081 - Lecture1: Introduction and Examples
课程简介
课程目标
- 理解操作系统的设计和实现
- 通过 XV6 操作系统动手实验,可以扩展或改进操作系统
操作系统的目标
- Abstraction:对硬件进行抽象
- Multiplex:在多个应用程序之间共用硬件资源
- Isolation:隔离性,程序出现故障时,不同程序之间不能相互干扰
- Sharing:实现共享,如数据交互或协同完成任务
- Security:想分享的时候可以分享,不想分享的时候可以不分享,可以称为 Access Control System
- Performance:操作系统至少需要不阻止应用程序获得高性能,甚至需要帮助应用程序获取高性能
- Range of oses:对于大部分操作系统,需要支持大量不同类型的应用程序
操作系统结构
系统调用函数
read/write 系统调用
read
和write
函数在执行时,不会关心读写的数据格式,操作系统中(内核)copy 程序会按照 8bit 的字节流处理数据read
和write
函数中第一个参数代表文件位置,shell 会确保默认情况下,当一个程序启动时,文件描述符 0 连接到 console 的输入,文件描述符 1 连接到了 console 的输出。
open 系统调用
- 字节流就是一段连续的数据按照字节的长度读取
open("output.txt", O_WRONLY | O_CREATE);
第二个参数用来告诉 open 系统调用在内核中的实现:我们将要创建并写入一个文件- 文件描述符本质上对应了内核中的一个表单数据。内核维护了每个运行进程的状态,内核会为每一个运行进程保存一个表单,表单的 key 是文件描述符。这个表单让内核知道,每个文件描述符对应的实际内容是什么。这里比较关键的点是,每个进程都有自己独立的文件描述符空间,所以如果运行了两个不同的程序,对应两个不同的进程,如果它们都打开一个文件,它们或许可以得到相同数字的文件描述符,但是因为内核为每个进程都维护了一个独立的文件描述符空间,这里相同数字的文件描述符可能会对应到不同的文件。
fork 系统调用
-
fork
可以创建新的进程,该进程(子进程)与原进程(父进程)的指令、数据、堆栈、文件描述符表单完全相同 -
fork
有两个返回值,但是有独立的地址空间(都认为内存从 0 开始增长)- 在父进程中,返回子进程的 PID
- 在子进程中,返回 0
- 如果创建失败,返回-1
wait 系统调用
-
wait
表示等待一个子进程退出- 如果调用者没有子进程,则
wait
立即返回-1 - 如果父进程不关注子进程的退出时状态,可以传一个
0
地址(或者NULL
指针)给wait
pid = wait((int *)0); // 只要一个子进程退出,wait 就结束,返回该退出的子进程的 pid 号
- 如果父进程关注子进程的退出时状态,可以使用如下方式,
status
将保存子进程结束时的状态(子进程退出时exit
里的参数会被保存到status
中)
int status; wait(&status);
- 如果调用者没有子进程,则
exit 系统调用
exit
系统调用退出程序,并释放资源。exit
需要一个整数状态参数,0 表示成功,1 表示失败
exec 系统调用
exec
系统调用使用新内存映像来替换内存的进程exec
会保留当前的文件描述符- 通常
exec
不会返回,只有出错时才会返回
_char _*argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0; // 标记了数组的结尾,也可以用NULL指针
exec("/bin/echo", argv); // 相当于echo hello
printf("exec error\n"); // exec出错时返回,否则不会运行到这里了
- 一般来说
fork
下的子进程中调用exec
函数来替换子进程,但这样会造成资源浪费,因此内核通过使用虚拟内存技术来进行优化
pipe 系统调用
管道的概念
- 以文件描述符对的形式提供给进程,一个表示读端,一个表示写端
- 本质是一个伪文件,实质为内核缓冲区
- 管道实际为内核使用环形队列机制,借助内核缓冲区(4K)实现
管道的局限性
- 必须作用在有血缘关系的进程之间
- 数据不能进程自己写,自己读(需要两个进程,一个读,一个写)
- 采用半双工通信,数据只能单方向流动
- 数据不能反复读取
管道函数
int pipe(int fd[2])
:创建并打开管道
- 参数:
fd[0]
读端;fd[1]
写端 - 返回值:
0
成功;-1
失败
管道程序举例
int p[2];
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p); // 创建并打开管道
if (fork() == 0) // 子进程
{
close(0); // 释放文件描述符0(标准输入)
dup(p[0]); // 使得文件描述符0引用了管道读端p[0]
close(p[0]); // 关闭管道读端
close(p[1]); // 关闭管道写端
exec("/bin/wc", argv); // wc相当于从管道读数据(这里其实不太理解,管道不是关上了吗)
}
else // 父进程
{
close(p[0]); // 读写只能有一个,父进程将读端关闭
write(p[1], "hello world\n", 12); // 向管道写入数据
close(p[1]); // 写完将管道关闭
}
管道的读写行为
- 读管道:
-
管道中有数据,
read
返回实际读到的字节数 -
管道中无数据:
- 管道写端全部关闭,
read
返回 0 - 管道写端没有全部关闭,
read
阻塞等待
- 管道写端全部关闭,
- 写管道:
-
管道读端全部关闭,进程异常终止(SIGPIPE 信号导致的)
-
管道读端没有全部关闭:
- 管道已满,
write
阻塞 - 管道未满,
write
将数据写入,并返回实际写入的字节数
- 管道已满,
File system
文件系统
- xv6 文件系统包含了数据文件(拥有字节数组)和目录(拥有对数据文件和其他目录的命名引用)
- 以
/
开头的如/a/b
表示根目录/
中a
目录中名为b
的文件或目录(绝对路径),不以/
开头的是相对于当前目录的路径(相对路径)
chdir 系统调用
通过 chdir
系统调用可以改变进程的当前目录,如下两个 open
打开了同一个文件
chdir("/a");
chdir("b");
open("c", O_RDONLY);
open("/a/b/c", O_RDONLY);
创建文件和目录
mkdir
可以创建一个新的目录,open
使用 O_CREATE
可以创建新的数据文件,mknod
可以创建一个新的设备文件
mkdir("/dir");
fd = open("/dir/file", O_CREATE | O_WRONLY);
close(fd);
mknod("/console", 1, 1); // 主设备号,次设备号,唯一标识一个内核设备
当一个进程打开设备文件后,内核会将系统的读写调用转移到内核设备实现上,而不是将它们传递给文件系统
文件链接
inode
是操作系统用来储存除了文件名和实际数据的数据结构,它是用来连接实际数据和文件名的;每个inode
保存着文件的元数据,包括文件的类型(文件 or 目录 or 设备)、长度、内容在磁盘的位置以及文件的链接数量fstat
系统调用从文件描述符引用的 inode 中检索信息
// int fstat(int fd, struct stat*),stat为接收inode信息的结构体,如fstat(fd, &st)
#define T_DIR 1 // Directory
#define T_FILE 2 // File
#define T_DEVICE 3 // Device
struct stat {
int dev; // File system's disk device
uint ino; // Inode number
short type; // Type of file
short nlink; // Number of links to file
uint64 size; // Size of file in bytes
};
link
系统调用创建了一个引用同一个inode
的文件
open("a", O_CREATE | O_WRONLY);
link("a", "b"); // 创建文件b,且a和b指向同一个inode
unlink
系统调用会从文件系统中删除一个文件名,只有当文件链接数为零且没有文件描述符引用它时,文件inode
和存放其内容的磁盘空间才会被释放
unlink("a"); // 删除a,此时只有b引用inode
// 创建临时文件的一种惯用方式,它创建一个无名称的inode,在进程关闭fd或退出时删除文件
fd = open("/tmp/x", O_CREATE | O_WRONLY);
unlink("/tmp/x");
Linux 命令
xargs 命令
- 作用:将标准输入转为命令行参数;
xargs
的作用在于,大多数命令(比如rm
、mkdir
、ls
)与管道一起使用时,都需要xargs
将标准输入转为命令行参数。
$ echo "hello world" | xargs echo
hello world
上面的代码将管道左侧的标准输入,转为命令行参数 hello world
,传给第二个 echo
命令
$ echo "one two three" | xargs mkdir
上面的代码等同于 mkdir one two three
。如果不加 xargs
就会报错,提示 mkdir
缺少操作参数
- 命令格式如下:
$ xargs [-options] [command]
真正执行的命令,紧跟在 xargs
后面,接受 xargs
传来的参数