【6S.081】学习笔记01:初识xv6以及配置xv6环境

0 阅读12分钟

【6S.081】学习笔记01:初识xv6以及配置xv6环境

由于6S.081本身就是操作系统类课程,故在学习操作系统的过程中,融合了该门课程的学习。所以在操作系统篇中直接涉猎,这里不再做笔记,仅仅针对xv6的文档、lab以及代码实现进行笔记记录。

注:内容分为文档阅读笔记实验笔记

文档阅读笔记1:第一章 操作系统接口

1.讲解操作系统的构建理念

操作系统的工作是

(1)将计算机的资源在多个程序间共享,并且给程序提供一系列比硬件本身更有用的服务。

(2)管理并抽象底层硬件,举例来说,一个文字处理软件(比如 word)不用去关心自己使用的是何种硬盘。

(3)多路复用硬件,使得多个程序可以(至少看起来是)同时运行的。

(4)最后,给程序间提供一种受控的交互方式,使得程序之间可以共享数据、共同工作。

image-20250303130957875

2.介绍xv6

  • xv6提供Unix操作系统中的基本接口,同时模仿其内部设计
  • 使用了传统的Kernel(内核)概念,一个向其他运行中程序提供服务的特殊程序image-20250303131428682
  • 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的源代码中可以找到以下数据:

image-20250306170027410

image-20250306170045967

这些都是与文件描述符有关的代码。

文件描述符是一个由内核管理的非负整数本质是进程访问I/O资源的句柄

进程通过文件描述符操作文件、管道、设备等资;而内核通过文件描述符表为每个进程维护独立的I/O上下文。

文件描述符标准约定:

  • 0:stdin——标准输入
  • 1:stdout——标准输出
  • 2:stderr——标准错误

以上规定在user/sh.c中可以找到

image-20250306172347451

readwriteopenclose

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通过forkexec的分离设计实现灵活的重定向:

在这两个调用之间,shell有机会对子进程进行I/O重定向,而不会干扰主shell的I/O设置。

文件偏移量的共享与独立性

dup

dup 的核心是通过复制文件描述符,实现资源的共享和重定向

dup系统调用复制一个现有的文件描述符,返回一个引用自同一个底层I/O对象的新文件描述符。两个文件描述符共享一个偏移量,就像fork复制的文件描述符一样。

如果两个文件描述符是通过一系列forkdup调用从同一个原始文件描述符派生出来的,那么它们共享一个偏移量。否则,文件描述符不会共享偏移量,即使它们来自于对同一文件的打开调用。

文件描述符与内核的关系

  • 权限隔离:用户态进程通过系统调用(如openread)访问内核管理的文件对象,内核在管理模式(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]);
}
  1. 创建管道

    pipe(p) 创建一个管道,并将两个文件描述符存储在数组 p 中。p[0] 是管道的读端,p[1] 是管道的写端。

    int p[2];
    pipe(p);
    
  2. 设置 argv

    char *argv[2];
    argv[0] = "wc";
    argv[1] = 0;
    

    设置 argv 数组,用于传递给 exec 函数。

  3. 创建子进程

    fork() 创建一个子进程。如果 fork() 返回 0,则表示当前是子进程。

  4. 子进程的操作

    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 程序将从标准输入读取数据。
  5. 父进程的操作

    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)机制。

特点

  1. 单向通信
    • 管道是单向的,即数据只能从写端流向读端。如果需要双向通信,需要创建两个管道。
  2. 文件描述符
    • 管道使用文件描述符来标识其读端和写端。pipe(p) 创建的管道会返回两个文件描述符,p[0] 是读端,p[1] 是写端。
  3. 阻塞行为
    • 如果管道的读端没有被打开,写操作会导致进程阻塞,直到有进程打开读端。
    • 如果管道的写端没有被打开,读操作会导致进程阻塞,直到有进程打开写端。
  4. 自动同步
    • 管道提供了自动同步机制,确保数据按顺序传递。
  5. 有限缓冲区
    • 管道有一个有限的缓冲区。如果缓冲区满了,写操作会阻塞,直到有数据被读取。

通过上述代码示例,可以看到管道在父子进程间传递数据的作用。父进程通过管道写入数据,子进程从管道读取数据,并将其作为标准输入传递给 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:文件大小

image-20250306195955667

3.link与unlink

硬链接(link)

  • link("a", "b")b 也指向 ainode
  • fstat() 结果显示相同的 inonlink 计数增加
  • 读取 ab 等效

删除文件(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的分析。

请跳转:【6S081】Lab1详解:Xv6 and Unix utilities- 掘金