【6S.081】学习笔记01:初识xv6以及配置xv6环境
由于6S.081本身就是操作系统类课程,故在学习操作系统的过程中,融合了该门课程的学习。所以在操作系统篇中直接涉猎,这里不再做笔记,仅仅针对xv6的文档、lab以及代码实现进行笔记记录。
注:内容分为文档阅读笔记和实验笔记。
文档阅读笔记1:第一章 操作系统接口
1.讲解操作系统的构建理念
操作系统的工作是
(1)将计算机的资源在多个程序间共享,并且给程序提供一系列比硬件本身更有用的服务。
(2)管理并抽象底层硬件,举例来说,一个文字处理软件(比如 word)不用去关心自己使用的是何种硬盘。
(3)多路复用硬件,使得多个程序可以(至少看起来是)同时运行的。
(4)最后,给程序间提供一种受控的交互方式,使得程序之间可以共享数据、共同工作。
2.介绍xv6
- xv6提供Unix操作系统中的基本接口,同时模仿其内部设计
- 使用了传统的Kernel(内核)概念,一个向其他运行中程序提供服务的特殊程序
- xv6内核提供了Unix传统系统调用的一部分,即以下部分,注意,请务必熟记,以后会经常用到:
系统调用 | 描述 |
---|---|
fork() | 创建进程 |
exit() | 结束当前进程 |
wait() | 等待子进程结束 |
kill(pid) | 结束 pid 所指进程 |
getpid() | 获得当前进程 pid |
sleep(n) | 睡眠 n 秒 |
exec(filename, *argv) | 加载并执行一个文件 |
sbrk(n) | 为进程内存空间增加 n 字节 |
open(filename, flags) | 打开文件,flags 指定读/写模式 |
read(fd, buf, n) | 从文件中读 n 个字节到 buf |
write(fd, buf, n) | 从 buf 中写 n 个字节到文件 |
close(fd) | 关闭打开的 fd |
dup(fd) | 复制 fd |
pipe( p) | 创建管道, 并把读和写的 fd 返回到p |
chdir(dirname) | 改变当前目录 |
mkdir(dirname) | 创建新的目录 |
mknod(name, major, minor) | 创建设备文件 |
fstat(fd) | 返回文件信息 |
link(f1, f2) | 给 f1 创建一个新名字(f2) |
unlink(filename) | 删除文件 |
- xv6适用shell,本质上是一个Unix Bourne shell 的简单实现。介绍shell概念:shell是一个普通的用户界面,接受用户输入的命令并且执行它们。它不是内核的一部分,充分说明了系统调用接口的强大。
3.xv6的进程
一个xv6进程由两部分组成,一部分是用户内存空间(指令,数据,栈),另一部分是仅对内核可见的进程状态。
xv6具有分时特性:可以在可用CPU之间不断切换,决定哪一个等待中进程被执行;不被执行的那个进程的CPU寄存器将会被xv6保存,当要被执行时恢复这些寄存器的值。
内核将每个进程与一个**pid(进程标识符)**关联起来。
在了解父子进程前,先了解几个函数的概念:
fork
:创建进程
wait
:等待进程结束
exec
:读取内存镜像,执行可执行文件
exit
:退出进程
父进程与子进程:
-
在某个进程(被称为父进程)中使用
fork
创建的新进程被称为子进程。 -
fork
函数在父进程中返回子进程的pid,在子进程中返回0. -
int pid; pid = fork(); if(pid > 0){//如果pid大于0,则是父进程返回了子进程的pid printf("parent: child=%d\n", pid); pid = wait(); printf("child %d is done\n", pid); } else if(pid == 0){//如果pid等于0,则是子进程返回了0 printf("child: exiting\n"); exit(); } else {//否则出错 printf("fork error\n"); }
-
父进程与子进程拥有不同的内存空间和寄存器,改变一个进程中的变量不会影响另一个进程。
xv6通常隐式地分配用户内存空间。fork
在子进程需要装入父进程的内存拷贝时分配空间,exec
在需要装入可执行文件时分配空间。一个进程在需要额外内存时可以通过调用 sbrk(n)
来增加 n 字节的数据内存。 sbrk
返回新的内存的地址。
4.I/O与内存描述符
如果我们要了解xv6系统的核心要领,那么I/O接口和文件描述符是不得不去理解的。
文件描述符
我们首先来看文件描述符,它是一个小整数(small integer),它表示进程可以读取或写入的由内核管理的对象。
我们在xv6的源代码中可以找到以下数据:
这些都是与文件描述符有关的代码。
文件描述符是一个由内核管理的非负整数,本质是进程访问I/O资源的句柄 。
进程通过文件描述符操作文件、管道、设备等资;而内核通过文件描述符表为每个进程维护独立的I/O上下文。
文件描述符标准约定:
- 0:stdin——标准输入
- 1:stdout——标准输出
- 2:stderr——标准错误
以上规定在user/sh.c
中可以找到
read
、write
、open
、close
read(fd,buf,n)
:
解释:第一个参数fd是文件描述符,第二个参数buf是要复制到的位置,第三个参数n是fd最多可以读取多少字节。
作用:从当前偏移量读取数据,读取完成后偏移量增加n字节,返回读取的字节数
write(fd,buf,n)
:
解释:将buf中的n字节写入文件描述符,并返回写入的字节数。
作用:返回实际写入的字节数(仅错误时小于n
)
open("input.txt", O_RDONLY
:
解释:以某种方式打开文件,如下:
宏定义 | 功能说明 |
---|---|
O_RDONLY | 只读 |
O_WRONLY | 只写 |
O_RDWR | 可读可写 |
O_CREATE | 如果文件不存在则创建文件 |
O_TRUNC | 将文件截断为零长度 |
close(int fd)
:
解释:关闭文件描述符,使其可以被未来使用的open、pipe或dup系统调用重用。
I/O重定向的实现机制
xv6通过fork
与exec
的分离设计实现灵活的重定向:
在这两个调用之间,shell有机会对子进程进行I/O重定向,而不会干扰主shell的I/O设置。
文件偏移量的共享与独立性
dup
:
dup
的核心是通过复制文件描述符,实现资源的共享和重定向
dup
系统调用复制一个现有的文件描述符,返回一个引用自同一个底层I/O对象的新文件描述符。两个文件描述符共享一个偏移量,就像fork复制的文件描述符一样。
如果两个文件描述符是通过一系列fork
和dup
调用从同一个原始文件描述符派生出来的,那么它们共享一个偏移量。否则,文件描述符不会共享偏移量,即使它们来自于对同一文件的打开调用。
文件描述符与内核的关系
-
权限隔离:用户态进程通过系统调用(如
open
、read
)访问内核管理的文件对象,内核在管理模式(Supervisor Mode)验证权限并执行特权操作(如修改偏移量) -
资源抽象:文件描述符背后可能对应磁盘文件(通过文件系统层)、管道(通过日志层)或设备(如控制台驱动),但用户进程无需感知底层差异
5.管道
管道定义
我们看管道(pipes)的定义:
管道是作为一对文件描述符公开给进程的小型内核缓冲区,一个用于读取,一个用于写入。将数据写入管道的一端使得这些数据可以从管道的另一端读取。管道为进程提供了一种通信方式。
仔细看这句话:将数据写入管道的一端使得这些数据可以从管道的另一端读取。
嗯,管道跟队列很像。
我们来看一个程序,来分析它的功能:
int p[2];
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p);
if (fork() == 0) {
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
exec("/bin/wc", argv);
} else {
close(p[0]);
write(p[1], "hello world\n", 12);
close(p[1]);
}
-
创建管道:
pipe(p)
创建一个管道,并将两个文件描述符存储在数组p
中。p[0]
是管道的读端,p[1]
是管道的写端。int p[2]; pipe(p);
-
设置
argv
:char *argv[2]; argv[0] = "wc"; argv[1] = 0;
设置
argv
数组,用于传递给exec
函数。 -
创建子进程:
fork()
创建一个子进程。如果fork()
返回 0,则表示当前是子进程。 -
子进程的操作:
close(0); dup(p[0]); close(p[0]); close(p[1]); exec("/bin/wc", argv);
close(0);
关闭标准输入(文件描述符 0)。dup(p[0]);
将管道的读端复制到文件描述符 0(标准输入)。close(p[0]);
关闭管道的读端。close(p[1]);
关闭管道的写端。exec("/bin/wc", argv);
使用exec
执行 [wc](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html) 程序,wc
程序将从标准输入读取数据。
-
父进程的操作:
close(p[0]); write(p[1], "hello world\n", 12); close(p[1]);
close(p[0]);
关闭管道的读端。write(p[1], "hello world\n", 12);
向管道的写端写入数据"hello world\n"
。close(p[1]);
关闭管道的写端。
管道(pipe)的作用和特点
作用:
- 管道用于在父子进程之间传递数据。它提供了一种简单的进程间通信(IPC)机制。
特点:
- 单向通信:
- 管道是单向的,即数据只能从写端流向读端。如果需要双向通信,需要创建两个管道。
- 文件描述符:
- 管道使用文件描述符来标识其读端和写端。
pipe(p)
创建的管道会返回两个文件描述符,p[0]
是读端,p[1]
是写端。
- 管道使用文件描述符来标识其读端和写端。
- 阻塞行为:
- 如果管道的读端没有被打开,写操作会导致进程阻塞,直到有进程打开读端。
- 如果管道的写端没有被打开,读操作会导致进程阻塞,直到有进程打开写端。
- 自动同步:
- 管道提供了自动同步机制,确保数据按顺序传递。
- 有限缓冲区:
- 管道有一个有限的缓冲区。如果缓冲区满了,写操作会阻塞,直到有数据被读取。
通过上述代码示例,可以看到管道在父子进程间传递数据的作用。父进程通过管道写入数据,子进程从管道读取数据,并将其作为标准输入传递给 wc
程序,从而实现了进程间的数据传递和通信。
另外,实际上我们看到管道不过是一种传输形式,但是它相比临时文件又有些什么优势呢?
- 首先,管道会自动清理自己;在文件重定向时,shell使用完
/tmp/xyz
后必须小心删除 - 其次,管道可以任意传递长的数据流,而文件重定向需要磁盘上足够的空闲空间来存储所有的数据。
- 第三,管道允许并行执行管道阶段,而文件方法要求第一个程序在第二个程序启动之前完成。
- 第四,如果实现进程间通讯,管道的阻塞式读写比文件的非阻塞语义更高效。
6.文件系统
xv6中,有一个完备且齐全的文件系统,也包含很多与文件有关的操作。
Xv6 文件系统管理 数据文件(存储字节数据)和 目录(存储文件/目录引用),形成 树形结构,从 /
(根目录)开始。
路径解析方式如下:
- 绝对路径(如
/a/b/c
):从根目录开始 - 相对路径(如
b/c
):基于进程的 当前工作目录,可用chdir()
更改
1. 重要的文件和目录操作
操作 | 说明 |
---|---|
mkdir("/dir") | 创建新目录 |
`open("/dir/file", O_CREATE) | 创建新文件 |
mknod("/console", 1, 1) | 创建设备文件,指定主设备号和次设备号 |
chdir("/a"); chdir("b") | 改变当前目录到 /a/b |
open("c", O_RDONLY) | 访问 c (基于当前目录) |
open("/a/b/c", O_RDONLY) | 直接访问 /a/b/c |
2.Inode 与文件元数据
-
文件名和文件本身是分离的,文件的核心是 inode(索引结点)
-
Inode 结构体
struct stat(在stat.h中)
包含:dev
:所属磁盘设备ino
:inode 编号(唯一标识文件)type
:文件类型(目录/文件/设备)nlink
:硬链接数size
:文件大小
3.link与unlink
硬链接(link)
link("a", "b")
让b
也指向a
的 inodefstat()
结果显示相同的ino
,nlink
计数增加- 读取
a
或b
等效
删除文件(unlink)
unlink("a")
仅删除a
这个名字,文件本体不变(如果nlink > 0
)- 只有
nlink=0
且 没有进程打开文件时,inode 才释放 unlink("/tmp/xyz")
可创建 无名称临时文件,在进程关闭fd
或退出时自动清理
7.总结
xv6的主要目标是简单明了,同时提供一个简单的类unix系统调用接口。
文件系统和文件描述符是强大的抽象,然而,正是这样的抽象,使得文件描述符与文件、管道、shell语法等结合使用才造就了xv6甚至Unix。
本书研究了xv6如何实现其类Unix接口,但这些思想和概念不仅仅适用于Unix。任何操作系统都必须在底层硬件上复用进程,彼此隔离进程,并提供受控制的进程间通讯机制。在学习了xv6之后,你应该去看看更复杂的操作系统,以及这些系统中与xv6相同的底层基本概念。
以上部分就是关于xv6文档的第一章的阅读。接下来是对于lab的分析。