这学期开始学操作系统啦,一周五节课,是一位老教师,除了讲话慢了点还经常请同学们回答一些神奇问题外感觉还好,和上学期的寄网还是有差距的。暑假想预习来着的,不过经历了一些事情有点颓,现在回忆起来像是上个世纪的假期了。众所周知,大学=大不了自学,所以打开了MIT·S081。
课程配套实验基于教授们自己写的一个类UNIX教学操作系统——xv6,通过xv6上的实验你能够对操作系统中的一些概念以及操作理解更深,精品良心,废话不多说,希望这学期能好好做实验(\认真脸)。
第一个实验主要以熟悉xv6以及系统调用函数为主,实验文档戳这。
❖ 安装xv6
众所周知,环境两小时,实验5分钟。不过安装xv6似乎十分顺利并没有遭遇太多的不测,当然很大一部分原因是站在别人的血泪史上。如果你也是M1用户,可以参考MIT 6.S081/Fall 2020 搭建risc-v与xv6开发调试环境这篇博客。
前置环境主要是:
gcc / clang :用来编译riscv-gnu-toolchain 工具链
riscv-gnu-toolchain 工具链:用来编译调试xv6,需要下载源码进行编译
qemu:可以运行不同架构下操作系统的虚拟机,用C语言来模拟硬件的执行,由于xv6是运行在RISC-V架构上的,因此需要qemu来运行.
上述环境配置完成后,即可
> git clone git://g.csail.mit.edu/xv6-labs-2021
> cd xv6-labs-2021
> git checkout util
最后通过make qemu
来编译xv6并运行,接下来就可以在xv6的命令行中输入命令了。
❖ Coding
☑︎ sleep (easy)
第一个实验非常简单,实现用户指定时间的停顿,而xv6系统内置sleep函数,主要是熟悉码命令时的一些基本格式。首先每个命令都会传入两个参数:int argc
&char *argv[]
,argc是传入参数的个数,而argv是所有传入参数的字符串形式数组。那么这个问题就非常的清晰明了了,只需要把用户传入的时间转为数字,然后调用sleep()即可。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int arg, char *argv[])
{
if (arg <= 1)
{
fprintf(2, "usage: sleep n\n");
exit(1);
}
else if (arg > 2)
{
fprintf(2, "ERROR: too much arguments!\nusage: sleep n\n");
exit(1);
}
char *s = argv[1];
int n = 0;
while (*s >= '0' && *s <= '9')
n = 10 * n + *s++ - '0';
if (*s != 0)
{
fprintf(2, "ERROR: the second parameter must be a number!\n");
exit(0);
}
sleep(n);
exit(0);
}
⚠️argv[0]是命令名称,真正的参数从argv[1]开始。另外不要忘记错误处理与错误提示。
☑︎ pingpong (easy)
先用fork函数创建子进程,然后使用pipe函数进行进程间通讯,最后通过getpid函数获取进程id并输出“PID:received …”。
首先通过fork返回值是否为0判断是子进程or父进程(父进程返回子进程id),fork完之后两者的资源都是一样的而且都从fork函数继续运行。那么我们先用pipe创建一个管道,用一个int p[2]
来记录管道两端的文件描述符,p[1]
写p[2]
读。
那么先在父进程中write,然后子进程read完输出并write,父进程通过wait函数(等待第一个子进程结束,如果没有子进程直接继续)等待子进程结束再继续read并输出。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int arg, char *argv[])
{
int p[2];
pipe(p);
char buff[5];
if (fork() == 0)
{
if (read(p[0], buff, 5) != 5)
{
printf("Read Parent ERROR!\n");
exit(1);
}
close(p[0]);
printf("%d: received %s\n", getpid(), buff);
write(p[1], "pong", 5);
close(p[1]);
}
else
{
write(p[1], "ping", 5);
close(p[1]);
wait(0);
if (read(p[0], buff, 5) != 5)
{
printf("Read Child ERROR!\n");
exit(1);
}
printf("%d: received %s\n", getpid(), buff);
close(p[0]);
}
exit(0);
}
read函数返回读取的字节数,达到文件末尾返回0,出错返回-1,write类似。
⚠️读写完及时close,不要忘记字符串末尾的’\0’。
至于为什么父进程写的比子进程读的快,我想是因为子进程因为需要复制资源还需要耗费额外的时间,因此开始执行的时间会晚于父进程。
$ pingpong
4: received ping
3: received pong
$
☑︎ primes (moderate/hard)
不是一个命令,但有助于你理解pipe的工作流程。输入primes命令后输出2-35之间的素数。虽然个数有限而且很少可以直接枚举,但希望你利用pipe制作一个埃氏素数筛。大致的流程如下图所示:
它的思想很简单,首先用最小素数2筛去为其倍数的数字,然后再筛去为第二小素数3的倍数,依次类推。不难发现,如果该数组是以2开始的一串连续数字的话,那么每次筛完后的最小数字一定是素数,否则必然还存在比其小的素数未被筛选,这是矛盾的。因此我们只要每次输出最小数字然后筛去该数字的倍数即可。
很显然这里需要pipe实现传递的功能,那么我们可以每次先从上一个pipe中读出剩下的数组,然后创建一个新的pipe,将该轮筛选后剩余的数字写入新pipe中,接着fork一个新进程,有点类似函数递归调用,我下面用循环代替了递归。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main()
{
int buff[34], p[2], size = 34;
for (int i = 0; i < 34; ++i)
buff[i] = i + 2;
pipe(p);
write(p[1], buff, size * sizeof(int));
close(p[1]);
while(size)
{
if (fork() == 0)
{
read(p[0], buff, size * sizeof(int));
close(p[0]);
pipe(p);
printf("prime %d\n", buff[0]);
int cnt = 0, prime = buff[0];
for (int i = 0; i < size; ++i)
if (buff[i] % prime != 0)
buff[cnt++] = buff[i];
size = cnt;
write(p[1], buff, cnt * sizeof(int));
close(p[1]);
}
else
{
wait(0);
exit(0);
}
}
exit(0);
}
之所以父进程需要wait是因为如果不wait第一个进程在执行完后就直接exit(0)了,这样就会导致系统以为你执行完了事实上并没有的局面:
$ primes
p$r ime 2
prime 3
prime 5
prime 7
prime 11
prime 13
prime 17
prime 19
prime 23
prime 29
prime 31
你会发现在输出第一条信息的过程中系统以为进程结束,于是输出【$ 】,其实只是第一个进程结束了,但它需要对创建的所有进程负责。
⚠️创建新管道前记得把老管道的端口close,否则就丢失了。
☑︎ find (moderate)
实现一个简单的查找文件命令find,命令格式为【find rootfile filename】,共计两个参数,那么这里我们需要学会如何访问文件。
通过阅读user/ls.c,不难得知每个路径都是一个文件,文件内记录该路径下的子文件信息,首先通过open(char *path, int flag)
函数打开对应路径path的文件,返回相应的文件描述符,flag是读写标志。然后用stat(int fd, struct stat *st)
将fd对应的文件夹/文件的信息写入结构体st,其中st.type
表示其(文件夹T_DIR/文件T_FILE)类型。
这是一方面,但更重要的是获取路径下子文件的信息,例如文件名。fd对应的文件内部记录了所有子文件的信息,可以通过read(fd, &de, sizeof(de))
来获取一个子文件信息,其中de是子文件信息结构体类型的一个实体。de包含inum和name,inum给不同文件夹和文件编号,name为文件名或文件夹名。
那么通过stat(path+'/'+de.name, &st)
就能获取子文件的相关信息,再由st。tyoe
判断是否为文件夹进行递归搜索。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
void search(char *path, char *name)
{
struct dirent de;
struct stat st;
char buf[512], *p;
int fd;
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;
}
switch (st.type)
{
case T_FILE:
printf("find: %s is a file instead of a path.\n", path);
break;
case T_DIR:
if (strlen(path) + 1 + DIRSIZ + 1 > sizeof(buf))
{
printf("find: path too long!\n");
break;
}
strcpy(buf, path);
p = buf + strlen(path);
*p++ = '/';
while (read(fd, &de, sizeof(de)) == sizeof(de))
{
// dno't forget inum==0!
if (!strcmp(de.name, ".") || !strcmp(de.name, "..") || de.inum == 0)
continue;
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
if (stat(buf, &st) < 0)
{
printf("find: cannot stat %s", buf);
continue;
}
switch (st.type)
{
case T_DIR:
search(buf, name);
break;
case T_FILE:
if (!strcmp(de.name, name))
printf("%s\n", buf, path);
break;
// default:
// break;
}
}
break;
}
close(fd);
}
int main(int argc, char *argv[])
{
if (argc < 3)
{
fprintf(2, "usage: find rootpath filename.\n");
exit(1);
}
search(argv[1], argv[2]);
exit(0);
}
简单地说,de是子文件信息结构体,st是本身信息结构体。DIRSIZ是文件名固定长度,方便处理。memmove( void* dest, const void* src, size_t count )
从src拷贝count个字节到dest,如果是字符串记得补0。buf来存储路径以及子目录的路径字符串。
⚠️子文件信息里面会包含当前目录【.】、上级目录【..】还有一个inum==0 && de.name==""
的节点(大概查阅了一下,de.inum==0表示这是一块已经初始化并且可以用来创建文件或者文件夹的位置),注意绕道避免无限递归,无限递归将导致栈溢出,运行时会出现usertrap()
。
$ echo > b
$ mkdir a
$ echo > a/b
$ find . b
./b
./a/b
$
☑︎ xargs (moderate)
xargs命令是一个可以扩展输入参数的命令,通过cmd1 | xargs cmd2
可以将cmd1命令的输出添加到cmd2的输入参数中,如果cmd1输出有多行则分别添加执行,效果与UNIX中的xargs -n 1
等同:
$ echo "1\n2" | xargs -n 1 echo line
line 1
line 2
$ echo hello too | xargs echo bye
bye hello too
首先我们需要把xargs后面命令原本就跟着的参数拷贝过去,我这里是直接把字符串指针拷贝过去(相当于浅拷贝),并没有malloc一块新空间。
对于追加的参数,我们从标准输入(fd为0)读入。然后就一个一个字符地读,读到’\n’或’ ‘就malloc一块新的空间将这个参数复制过来并将头指针存入char *para[MAXARG]
,如果读到了’\n’或者文件结尾就执行一次exec函数(⚠️exec函数不会返回调用函数,如果返回就说明执行失败exit(1)
)。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "kernel/param.h"
#include "user/user.h"
#define MAXLEN 512
int main(int argc, char *argv[])
{
if (argc < 2)
{
fprintf(2, "usage: xargs command.\n");
exit(1);
}
char buf[MAXLEN], *para[MAXARG];
int flag = 1;
// shallow copy xisting parameters
for (int i = 1; i < argc; ++i)
para[i - 1] = argv[i];
// flag==0 means EOF
while (flag)
{
int cnt = argc - 1, len = 0;
// read a line from standard input
while ((flag = read(0, buf + len, 1)))
{
if (buf[len] == '\n' || buf[len] == ' ')
{
if (len)
{
para[cnt] = (char *)malloc(len + 1);
memmove(para[cnt], buf, len);
para[cnt][len] = 0;
len = 0;
if (++cnt > MAXARG)
{
printf("xargs: too many arguments!\n");
exit(1);
}
}
if (buf[len] == '\n')
break;
}
else if (len + 1 > MAXLEN)
{
printf("xargs: argument too long!\n");
exit(1);
}
else
++len;
}
if (!fork())
{
exec(para[0], para);
exit(1);
}
else
wait(0);
}
exit(0);
}
⚠️命令参数个数有限制(MAXARG),另外需要关注一个参数长度不能超过自己定义的buf(MAXLEN)。
❖ Grading & Debug
cd xv6-labs-2021
./grade-lab-util command_name
e.g.
> ./grade-lab-util sleep
make: `kernel/kernel' is up to date.
== Test sleep, no arguments == sleep, no arguments: OK (1.3s)
== Test sleep, returns == sleep, returns: OK (0.9s)
== Test sleep, makes syscall == sleep, makes syscall: OK (1.0s)
所有Test都显示OK即可。
Debug需要用gdb进行远程调试,首先在一个命令框中(在xv6-labs-2021目录下)make qemu-gdb
,然后新建一个命令框窗口(还是在xv6-labs-2021目录下)riscv64-unknown-elf-gdb user/_xargs
(【_】+命令名称 )。如果连接失败在gdb界面输入target remote localhost:25501
(端口号见qemu界面最后一行tcp::xxxxx)。不希望每次输入可以写入~/.gdbinit。b设置断点,c运行至断点,n单步执行,p arg查看arg值。