Lab1: Xv6 and Unix utilities

这学期开始学操作系统啦,一周五节课,是一位老教师,除了讲话慢了点还经常请同学们回答一些神奇问题外感觉还好,和上学期的寄网还是有差距的。暑假想预习来着的,不过经历了一些事情有点颓,现在回忆起来像是上个世纪的假期了。众所周知,大学=大不了自学,所以打开了MIT·S081

课程配套实验基于教授们自己写的一个类UNIX教学操作系统——xv6,通过xv6上的实验你能够对操作系统中的一些概念以及操作理解更深,精品良心,废话不多说,希望这学期能好好做实验(\认真脸)。

第一个实验主要以熟悉xv6以及系统调用函数为主,实验文档戳这

❖ 安装xv6


众所周知,环境两小时,实验5分钟。不过安装xv6似乎十分顺利并没有遭遇太多的不测,当然很大一部分原因是站在别人的血泪史上。如果你也是M1用户,可以参考MIT 6.S081/Fall 2020 搭建risc-v与xv6开发调试环境这篇博客。

前置环境主要是:

  1. gcc / clang :用来编译riscv-gnu-toolchain 工具链

  2. riscv-gnu-toolchain 工具链:用来编译调试xv6,需要下载源码进行编译

  3. 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制作一个埃氏素数筛。大致的流程如下图所示:

alt

它的思想很简单,首先用最小素数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值。

alt
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值