【MIT6.S81】-Lab1
课程链接
MIT6.S081:https://pdos.csail.mit.edu/6.828/2020/overview.html
在做到lab3时卡壳,准备重新看一下前面的内容再继续实验,感觉对前面的内容理解的还是不透彻,所以在文档中加入一些6.S081的相关内容
本文档部分内容参考网上的文章和视频
Chapter 1部分的总结如下,参考xv6 book英文原文,如有错误,敬请指正。
Chapter 1: Operating system interfaces
1.1 process and memory
**int fork(): **创建一个新的进程,在父进程中返回子进程的PID, 在子进程中返回0
**int exit(int status): **终止现有的进程,释放资源,将结束进程的状态报告给wait()
**int wait(int *status): **返回刚刚结束的子进程的PID,将子进程的退出状态复制到传入wait()的地址,若调用wait的进程无子进程,则返回-1,若无子进程终止,则继续等待
**int exec(char *file, char *argv[]): **加载文件并传参运行
char *argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;
exec("/bin/echo", argv);
printf("exec error");
xv6shell利用以上调用来代替用户运行程序
xv6shell的代码如下
当shell开始运行时,main函数首先通过getcmd来获取用户输入,接着调用fork,创建一个子进程,在子进程中调用runcmd函数,runcmd函数再通过exec来运行用户输入的命令,在exec中会调用exit来退出子进程返回到父进程,对应的是父进程的wait函数。
1.2 I/O and file descriptors
文件描述符是一个小整数,表示进程可以读取或写入的内核管理对象。
文件描述符将不同的文件抽象成字节流。
xv6 内核使用文件描述符作为每个进程表的索引,以便每个进程都有一个从零开始的文件描述符的私有空间。通常情况下,0代表读,1代表写,2代表错误。
新分配的文件描述符始终是当前进程中编号最小的未使用描述符。
下图中,子进程会关闭0,再将0用于input.txt,cat在使用0来执行
为什么fork和exec要分别定义为不同的系统调用?
将fork和exec分开定义可以再不干扰主shell I/O设置的情况下重定向子进程的I/O
两个文件描述符共享offset的情况:这两个文件描述符是必须从fork或者dup调用中的原始文件描述符中产生的
1.3 pipe
A pipe is a small kernel buffer exposed to processes as a pair of file descriptors, one for reading and one for writing.
管道是一个小的内核缓冲区,作为一对文件描述符暴露给进程,一个用于读取,一个用于写入。
wc中对pipe的简单应用:
如果pipe的读端无可读数据,pipe等待数据写入或等待写入端的所有文件描述符关闭,在后一种情况下,read 将返回 0,就像已到达数据文件末尾一样,所以及时关闭pipe的写端是很有必要的
xv6shell实现管道如下图代码所示(user/sh.c:100),例如grep fork sh.c | wc -1
和wc中类似,子进程创建pipe连接”|“的左右两端
1.4 file system
这部分内容比较简单,xv6 book中已详细说明,此处不再赘述。
Lab: Xv6 and Unix utilities
Lab guidance
调试技巧
以下是调试解决方案的一些提示:
- 确保您了解 C 和指针。 Kernighan 和 Ritchie 所著的《C 编程语言(第二版)》一书对 C 进行了简洁的描述。这里有一些有用的指针练习。 除非您已经精通 C 语言,否则请勿跳过或浏览上面的指针练习。 如果你不真正理解 C 中的指针,你将在实验室中遭受难以言喻的痛苦和痛苦,然后最终以艰难的方式理解它们。 相信我们; 你不想知道什么是“艰难的道路”。
- 如果您的练习部分有效,请通过提交代码来检查进度。 如果您稍后破坏了某些内容,则可以回滚到检查点并以较小的步骤前进。 要了解有关 Git 的更多信息,请查看 Git 用户手册,或者您可能会发现此面向 CS 的 Git 概述http://eagain.net/articles/git-for-computer-scientists/很有用。
- 如果测试失败,请确保您了解代码未通过测试的原因。 插入打印语句,直到您了解发生了什么。
- 您可能会发现您的 print 语句可能会产生很多您想要搜索的输出; 一种方法是在script内运行 make qemu (在您的计算机上运行 man script),它将所有控制台输出记录到一个文件中,然后您可以搜索该文件。 不要忘记退出script。
- 在许多情况下,打印语句就足够了,但有时能够单步执行某些汇编代码或检查堆栈上的变量会很有帮助。 要将 gdb 与 xv6 一起使用,请在一个窗口中运行 make qemu-gdb,在另一个窗口中运行 gdb(或 riscv64-linux-gnu-gdb),设置一个断点,然后是“c”(continue)和 xv6 将运行直到到达断点。 (有关有用的 GDB 提示,请参阅使用 GNU 调试器。
- 如果您想查看编译器为内核生成的程序集是什么,或者想了解特定内核地址处的指令是什么,请查看文件 kernel.asm,该文件是 Makefile 在编译内核时生成的。 (Makefile 还为所有用户程序生成 .asm。)
- 如果内核panics,它将打印一条错误消息,列出崩溃时程序计数器的值; 您可以搜索 kernel.asm 以找出程序计数器崩溃时所在的函数,或者您可以运行 addr2line -e kernel/kernel pc-value (运行 man addr2line 了解详细信息)。 如果你想获得回溯,请使用 gdb8 重新启动:在一个窗口中运行“make qemu-gdb”,在另一个窗口中运行 gdb(或 riscv64-linux-gnu-gdb),在panic*中设置断点(“b panic”),然后 by 后跟“c”(continue)。 当内核到达断点时,输入“bt”以获取回溯。
- 如果您的内核挂起(例如,由于死锁)或无法进一步执行(例如,由于执行内核指令时出现页面错误),您可以使用 gdb 找出挂起的位置。 在一个窗口中运行make qemu-gdb,在另一个窗口中运行 gdb ( riscv64-linux-gnu-gdb),然后运行“c”(continue)。 当内核出现挂起时,在 qemu-gdb 窗口中按 Ctrl-C 并输入“bt”以获取回溯。
- qemu 有一个“监视器”,可以让您查询模拟机器的状态。 您可以通过输入control-a c(“c”代表控制台)来获取它。 一个特别有用的监视命令是 info mem,用于打印页表。 您可能需要使用 cpu 命令来选择要查看哪个核心信息 mem,或者您可以使用 make CPUS=1 qemu 来启动 *qemu8,以导致只有一个核心。
环境配置
VMware:https://www.vmware.com/cn.html
Ubuntu20.04:https://ubuntu.com/download/desktop/thank-you?version=20.04.2.0&architecture=amd64
尽量使用20.04版本进行环境的搭建,不然容易出现错误
安装后还需要进行更换软件源等初步准备工作,这里不在赘述
安装xv6
打开终端,输入
sudo apt install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu libglib2.0-dev libpixman-1-dev gcc-riscv64-unknown-elf
下载xv6源码
git clone git://g.csail.mit.edu/xv6-labs-2020
cd xv6-riscv
make qemu
gdb调试
打开一个终端窗口,输入 make-gdb
再打开一个终端窗口,输入
echo "add-auto-load-safe-path YOUR_PATH/xv6-riscv/.gdbinit " >> ~/.gdbinit
输入gdb-multiarch
接下来就可以进行调试了
退出qemu:按下ctrl - a 放开后按x(之前一直搞错,要放开ctrl - a以后在按x)
Sleep
为 xv6 实现 UNIX 程序 sleep; sleep应该暂停用户指定的ticks。 时钟周期是 xv6 内核定义的时间概念,即定时器芯片两次中断之间的时间。 您的解决方案应该位于文件user/sleep.c中
原文如下
Implement the UNIX program
sleep
for xv6; yoursleep
should pause for a user-specified number of ticks. A tick is a notion of time defined by the xv6 kernel, namely the time between two interrupts from the timer chip. Your solution should be in the fileuser/sleep.c
.
提示:
- 在开始编码之前,请阅读 xv6 书的第一章。
- 查看 user/ 中的一些其他程序(例如 user/echo.c、user/grep.c 和 user/rm.c),了解如何获取传递给程序的命令行参数。
- 如果用户忘记传递参数,sleep 应该打印一条错误消息。
- 命令行参数作为字符串传递;您可以使用 atoi 将其转换为整数(请参阅 user/ulib.c)。
- 使用系统调用sleep。
- 请参阅 kernel/sysproc.c 了解实现 sleep 系统调用的 xv6 内核代码(查找 sys_sleep),参见 user/user.h 了解可从用户程序调用 sleep 的 C 定义,以及 user/usys.S 了解汇编代码从用户代码跳转到内核的sleep。
- 确保 main 调用 exit() 以退出程序。
- 将你的睡眠程序添加到Makefile中的UPROGS中;完成此操作后,make qemu 将编译您的程序,您将能够从 xv6 shell 运行它。
- 查看 Kernighan 和 Ritchie 的书《C 编程语言(第二版)》(K&R)来了解 C。
原文如下
Some hints:
- Before you start coding, read Chapter 1 of the xv6 book.
- Look at some of the other programs in
user/
(e.g.,user/echo.c
,user/grep.c
, anduser/rm.c
) to see how you can obtain the command-line arguments passed to a program.- If the user forgets to pass an argument, sleep should print an error message.
- The command-line argument is passed as a string; you can convert it to an integer using
atoi
(see user/ulib.c).- Use the system call
sleep
.- See
kernel/sysproc.c
for the xv6 kernel code that implements thesleep
system call (look forsys_sleep
),user/user.h
for the C definition ofsleep
callable from a user program, anduser/usys.S
for the assembler code that jumps from user code into the kernel forsleep
.- Make sure
main
callsexit()
in order to exit your program.- Add your
sleep
program toUPROGS
in Makefile; once you’ve done that,make qemu
will compile your program and you’ll be able to run it from the xv6 shell.- Look at Kernighan and Ritchie’s book The C programming language (second edition) (K&R) to learn about C.
传入main函数的argc表示参数的个数,argv[]表示传入的参数,argv[0]通常为,命令名
完整代码:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[])
{
if(argc != 2){ //传入的参数个数不为2,即不是sleep 3格式的
fprintf(2,"Usage: sleep...");
exit(1); //exit传入参数1表示错误结束
}
sleep(atoi(argv[1]));
exit(0); //exit传入参数0表示成功结束
}
此外,还要在Makefile的UPROGS中加入sleep函数
$U/_sleep\
Pingpong
编写一个程序,使用 UNIX 系统调用通过一对管道(每个方向一个)在两个进程之间“pingpong”一个字节。 父进程应向子进程发送一个字节; 子进程应打印“:已收到 ping”,其中 是其进程 ID,将管道上的字节写入到父进程,然后退出; 父进程应该从子进程读取字节,打印“:收到 pong”,然后退出。 您的解决方案应该位于文件 user/pingpong.c 中。
原文如下
Write a program that uses UNIX system calls to ‘‘ping-pong’’ a byte between two processes over a pair of pipes, one for each direction. The parent should send a byte to the child; the child should print “: received ping”, where is its process ID, write the byte on the pipe to the parent, and exit; the parent should read the byte from the child, print “: received pong”, and exit. Your solution should be in the file
user/pingpong.c
.
提示:
- 使用pipe创建一个管道
- 使用fork创建一个子进程
- 使用read来读管道,write来写管道
- 使用 getpid 查找调用进程的进程 ID
- 将程序添加到 Makefile 中的 UPROGS 中
- xv6 上的用户程序具有一组有限的可用库函数。您可以在 user/user.h 中看到该列表;源代码(系统调用除外)位于 user/ulib.c、user/printf.c 和 user/umalloc.c 中
原文如下
Some hints:
- Use
pipe
to create a pipe.- Use
fork
to create a child.- Use
read
to read from the pipe, andwrite
to write to the pipe.- Use
getpid
to find the process ID of the calling process.- Add the program to
UPROGS
in Makefile.- User programs on xv6 have a limited set of library functions available to them. You can see the list in
user/user.h
; the source (other than for system calls) is inuser/ulib.c
,user/printf.c
, anduser/umalloc.c
.
完整代码
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
int p1[2];
int p2[2];
char buf[5];
pipe(p1); //父进程到子进程
pipe(p2); //子进程到父进程
if(argc != 1)
{
fprintf(2, "Usage: pingpong");
exit(1);
}
int pid = fork();
if(pid == 0)//子进程
{
close(p1[1]); //关闭管道的写端(父进程到子进程)
if(read(p1[0],buf,sizeof(buf))>0)//子进程开始读
{
fprintf(1,"%d:received ping",getpid());
write(1, buf, sizeof(buf));
}
else
{
fprintf(2,"%d received failed!",getpid());
}
close(p2[0]); //关闭管道的读端(子进程到父进程)
write(p2[1],"pong\n",5); //子进程开始写
exit(0);
}
else
{//父进程
close(p1[0]); //关闭管道的读端(父进程到子进程)
write(p1[1],"ping\n",5); //父进程开始写
wait(0);
close(p2[1]); //关闭管道的写端(子进程到父进程)
if(read(p2[0],buf,sizeof(buf))>0) //父进程开始读
{
fprintf(2,"%d:received ping",getpid());
write(1, buf, sizeof(buf));
}
else
{
fprintf(2,"%d received failed!",getpid());
}
}
exit(0);
}
Primes
使用管道编写素数筛的并发版本。这个想法源自 Unix 管道的发明者 Doug McIlroy。本页中间的图片和周围的文字解释了如何操作。您的解决方案应该位于文件 user/primes.c 中。
原文如下
Write a concurrent version of prime sieve using pipes. This idea is due to Doug McIlroy, inventor of Unix pipes. The picture halfway down this page and the surrounding text explain how to do it. Your solution should be in the file
user/primes.c
.
您的目标是使用pipe和fork来设置管道。第一个进程将数字 2 到 35 输入管道。对于每个质数,您将安排创建一个进程,通过管道从其左邻居读取数据,并通过另一管道向其右邻居写入数据。由于xv6的文件描述符和进程数量有限,第一个进程可以停在35
原文如下
Your goal is to use
pipe
andfork
to set up the pipeline. The first process feeds the numbers 2 through 35 into the pipeline. For each prime number, you will arrange to create one process that reads from its left neighbor over a pipe and writes to its right neighbor over another pipe. Since xv6 has limited number of file descriptors and processes, the first process can stop at 35.
提示:
- 请小心关闭进程不需要的文件描述符,否则您的程序将在第一个进程达到 35 之前耗尽资源来运行 xv6。
- 一旦第一个进程达到 35,它应该等待整个管道终止,包括所有子进程、孙进程等。 因此,主 primes 进程仅应在所有输出打印完毕以及所有其他 primes 进程退出后退出。
- 提示:当管道的写入端关闭时,read返回零。
- 最简单的方法是直接将 32 位(4 字节)整数写入管道,而不是使用格式化的 ASCII I/O
- 您应该仅在需要时在管道中创建进程
- 将程序添加到 Makefile 中的 UPROGS 中。
原文如下
Some hints:
- Be careful to close file descriptors that a process doesn’t need, because otherwise your program will run xv6 out of resources before the first process reaches 35.
- Once the first process reaches 35, it should wait until the entire pipeline terminates, including all children, grandchildren, &c. Thus the main primes process should only exit after all the output has been printed, and after all the other primes processes have exited.
- Hint:
read
returns zero when the write-side of a pipe is closed.- It’s simplest to directly write 32-bit (4-byte)
int
s to the pipes, rather than using formatted ASCII I/O.- You should create the processes in the pipeline only as they are needed.
- Add the program to
UPROGS
in Makefile.
素数筛:
#include "kernel/types.h"
#include "user/user.h"
void process(int p[])
{
//递归创建进程的函数
close(p[1]);
int prime;
if (read(p[0], &prime, 4) > 0) { //从该进程的父进程中读入第一个数
fprintf(1, "prime %d\n", prime);//输出读到的这个数
int p2[2];
pipe(p2);//创建该进程与其子进程的管道
if (fork() > 0) {
close(p2[0]);
int i;
while(read(p[0], &i, 4) > 0) {//从该进程的父进程中读入剩下的数
if (i % prime != 0) {//将第一个数后的值,除了第一个数的倍数,通过pipe全部传给下一个子进程
write(p2[1], &i, 4);
}
}
close(p2[1]);
wait(0);
} else {
close(p[0]);
process(p2);//子进程继续创建子进程
}
}
}
int
main(int argc, char* argv[])
{
int p[2];
pipe(p);
int pid = fork();
//最初的父进程
if (pid > 0) {
close(p[0]);
fprintf(1, "prime 2\n"); //由于是第一个进程,也就是最早的父进程,所以先输出第一个数2
for (int i = 3; i <= 35; ++i) {//将2后的值,除了2的倍数,通过pipe全部传给下一个子进程
if (i % 2 != 0) {
write(p[1], &i, 4); //在pipe的写端写入4字节(int)的数据
}
}
close(p[1]);
wait(0);
} else {
process(p); //如果是子进程则调用函数
}
exit(0);
}
find
编写一个简单版本的 UNIX 查找程序:查找目录树中具有特定名称的所有文件。您的解决方案应该位于文件 user/find.c 中。
Write a simple version of the UNIX find program: find all the files in a directory tree with a specific name. Your solution should be in the file
user/find.c
.
提示:
- 查看 user/ls.c 以了解如何读取目录
- 使用递归允许 find 深入到子目录
- 不要递归成“.” 和 ”…”
- 文件系统的更改在 qemu 运行期间持续存在; 要获得干净的文件系统,请运行 make clean,然后运行 make qemu
- 您需要使用 C 字符串。 看一下 K&R,例如第 5.5 节
- 请注意,== 并不像 Python 中那样比较字符串。 请改用 strcmp()
- 将程序添加到 Makefile 中的 UPROGS 中
Some hints:
- Look at user/ls.c to see how to read directories.
- Use recursion to allow find to descend into sub-directories.
- Don’t recurse into “.” and “…”.
- Changes to the file system persist across runs of qemu; to get a clean file system run make clean and then make qemu.
- You’ll need to use C strings. Have a look at K&R (the C book), for example Section 5.5.
- Note that == does not compare strings like in Python. Use strcmp() instead.
- Add the program to
UPROGS
in Makefile.
#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
unit64 size; //size of file in bytes
};
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
char* fmtname(char *path) { //将文件名格式化
static char buf[DIRSIZ + 1];
char *p;
for (p = path + strlen(path); p >= path && *p != '/';p--);
p++;
if (strlen(p) >= DIRSIZ) return p;
memmove(buf, p, strlen(p));
memset(buf+strlen(p), ' ', DIRSIZ-strlen(p));
buf[strlen(p)] = 0;
return buf;
}
void find(char *path, char *fileName) {
char buf[512], *p;
int fd;
struct stat st;
struct dirent de;
if ((fd = open(path, 0)) < 0 ) {
fprintf(2, "find: cannot open %s\n", path);
return;
}
if (fstat(fd, &st) < 0) {
fprintf(2, "find: cannot stat %s\n", path);
close(fd);
return;
}
//printf("%s %s\n",path, fmtname(path));
switch(st.type) {
case T_FILE:
//printf("%s\n", fmtname(path));
if (strcmp(fmtname(path), fileName) == 0) {
printf("%s\n", path);
}
break;
case T_DIR:
//printf("%s %s\n", path, fmtname(path));
if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf) {
printf("find: path too long\n");
break;
}
strcpy(buf, path);
p = buf + strlen(buf);
*p++ = '/';
while (read(fd, &de, sizeof(de)) == sizeof(de)) {
if (de.inum == 0 || strcmp(de.name, ".") == 0 || strcmp(de.name, "..") == 0) {
continue;
}
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
find(buf, fileName);
}
break;
}
close(fd);
}
int main(int argc, char *argv[]) {
if (argc < 3) {
printf("error\n");
exit(1);
}
find(argv[1], argv[2]);
exit(0);
}
xargs
编写一个简单版本的 UNIX xargs 程序:从标准输入读取行并为每行运行一个命令,将该行作为参数提供给命令。您的解决方案应该位于文件 user/xargs.c 中
Write a simple version of the UNIX xargs program: read lines from the standard input and run a command for each line, supplying the line as arguments to the command. Your solution should be in the file
user/xargs.c
.
提示:
- 使用 fork 和 exec 在每一行输入上调用命令。 在父级中使用 wait 来等待子级完成命令
- 要读取单行输入,请一次读取一个字符,直到出现换行符 (‘\n’)。kernel/param.h 声明了 MAXARG,如果您需要声明 argv 数组,这可能很有用。
- 将程序添加到 Makefile 中的 UPROGS 中。
- 文件系统的更改在 qemu 运行期间持续存在; 要获得干净的文件系统,请运行 make clean,然后运行 make qemu。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/param.h"
int main(int argc, char *argv[])
{
char *args[MAXARG];
int i;
//将xargs的参数保存在args中
for(int i=0;i < argc;i++)
args[i] = argv[i];
char buf[256];
for( ; ; ){
int j = 0;
//从标准输入读入一行
while((read(0, buf + j, sizeof(char)) ! = 0) && buf[j] != '\n')
++j;
if(j == 0) break; //全部读完
buf[j] = 0;//添加结尾
//将标准输入传进的一行参数加到args后
args[i] = buf;
args[i + 1] = 0;//添加字符串结尾
//执行命令
if(fork() == 0){
//args[0]是xargs,args[1]是要执行的命令,剩余的是参数
exec(args[1], args + 1);
printf("exec error\n");
} else{
wait((void*)0);
}
}
exit(0);
}
args[i] = argv[i];
char buf[256];
for( ; ; ){
int j = 0;
//从标准输入读入一行
while((read(0, buf + j, sizeof(char)) ! = 0) && buf[j] != '\n')
++j;
if(j == 0) break; //全部读完
buf[j] = 0;//添加结尾
//将标准输入传进的一行参数加到args后
args[i] = buf;
args[i + 1] = 0;//添加字符串结尾
//执行命令
if(fork() == 0){
//args[0]是xargs,args[1]是要执行的命令,剩余的是参数
exec(args[1], args + 1);
printf("exec error\n");
} else{
wait((void*)0);
}
}
exit(0);
}