MIT6.S081 Lab1:Xv6 and Unix utilities

830 阅读4分钟

Lab1:Xv6 and Unix utilities

这个Lab会让你熟悉xv6和它的系统调用。

Boot xv6(通过qemu模拟)

下载xv6源码(配环境时已经下载过,略过),并切换到 util分支(这个版本是为这个lab定制的)

注:xv6-labs-2020 相比于书中的xv6-riscv添加了一些文件,可以通过git log查看变化。

通过git diff命令可以查看自上次commit后,你的代码的变化

通过git diff origin/util可以查看和原始的代码相比的变化

xv6没有ps命令,但是可以通过ctrl-p来查看进程

通过ctrl-a x退出qemu

注意:xv6上的用户程序只有一组有限的库函数可用。它们在 user/user.h中,源码(除了系统调用外)在user/ulib.cuser/printf.cuser/umalloc.c

sleep(easy)

要求:

 为xv6实现UNIX程序 sleep
 sleep应该暂停用户指定的tick值
 tick是一个由xv6内核定义的时间概念,即定时器芯片上两个中断之间的时间
 解决方案应该放在 user/sleep.c中
 ​
 如果忘记传入参数,则应该报错
 ​

提示:

  • 查看 user/文件夹下的其他文件(比如user/echo.c和user/grep.c),看如何获取命令行的参数(通过main函数的argv获取
  • 传入的参数是string,可以通过 user/ulib.c中的atoi转换为整数
  • 使用系统调用sleep(kernel/sysproc.c中的sys_sleep函数)
  • 在main中调用exit()退出程序
  • 将写好的sleep程序加到Makefile中的UPROGS,这样make qemu将编译你的程序,并且你可以通过xv6 shell调用它

代码:

 // 由于三个头文件有依赖关系,所以必须以下面的顺序include
 #include"kernel/types.h"
 #include "kernel/stat.h"
 #include "user/user.h" 
 ​
 int main(int argc, char* argv[]) {
     // 此时说明用户没有传入参数,应该报错
     if (argc == 1) { 
         printf("no ticks!");
         exit(1);
     }
 ​
     if (argc > 2) {
         printf("too many parameters!");
         exit(1);
     }
     // 传入的参数是字符串,需要转换为整型
     sleep(atoi(argv[1]));
     exit(0);
 }

测试:

通过make grade可以运行所有作业的测试

如果想单独测试某个作业(比如这个sleep),可以通过命令./grade-lab-util sleep

 manu@manu-virtual-machine:~/xv6-labs-2020$ ./grade-lab-util sleep
 #... lots of outputs ...
 == Test sleep, no arguments == sleep, no arguments: OK (2.3s) 
 == Test sleep, returns == sleep, returns: OK (0.5s) 
 == Test sleep, makes syscall == sleep, makes syscall: OK (0.9s) 

pingpong(easy)

要求:

 使用UNIX系统调用,通过管道在两个进程之间'ping-pong'一个字节(每个方向一个)
 父进程向子进程发送一个字节,子进程打印"<pid>: received ping",<pid>是子进程的进程ID,同时通过管道将收到的字节再发给父进程,然后退出exit
 父进程读取从子进程收到的字节,打印"<pid>: received pong",然后退出
 解决方案应该放在 user/pingpong.c中

提示:

  • 通过getpid得到进程ID
  • 两个进程间要想双向通信的话,应该建立两个管道,因为管道是单向通信的

代码:

 #include "kernel/types.h"
 #include "kernel/stat.h"
 #include "user/user.h"
 ​
 int main(int argc, char* argv[]) {
     // 由于父子进程间要双向通信,所以应该建立两个管道
     int parent2child[2];
     int child2parent[2];
     pipe(parent2child);
     pipe(child2parent);
 ​
     char buf[1];
 ​
     int pid = fork();
     if (pid < 0) {
         exit(1);
     }
     else if (pid == 0) {
         close(parent2child[1]);
         close(child2parent[0]);
 ​
         read(parent2child[0], buf, 1);
         printf("%d: received ping\n", getpid());
         write(child2parent[1], buf, 1);
         
         close(parent2child[0]);
         close(child2parent[1]);
         exit(0);
     }
     else { 
         close(parent2child[0]);
         close(child2parent[1]);
 ​
         write(parent2child[1], "a", 1);
         read(child2parent[0], buf, 1);
         printf("%d: received pong\n", getpid());
 ​
         close(parent2child[1]);
         close(child2parent[0]);
         exit(0);
     }
 }

测试(通过)

image-20220412215550223

primes (moderate/hard)

要求:

 使用管道编写一个素数筛选器的并发版本。——这个想法源于Unix管道的发明者Doug McIlroy。
 参考:https://swtch.com/~rsc/thread/
 解决方案应该放在 user/primes.c中

提示:

  • 建立管道。第一个进程将数字2-35放入管道。对每个质数,创建一个进程,该进程通过管道从其左邻居读取数据,并通过另一个管道向其右邻居写入数据。由于xv6的文件描述符和进程数量有限,第一个进程可以在35时停止。
  • 要小心关闭进程不需要的文件描述符,否则程序会在第一个进程到达35之前耗尽资源。
  • 一旦第一个进程达到35,它应该等待直到整个管道结束,包括所有的子进程。因此,主质数进程应该只在打印完所有输出以及所有其他质数进程退出之后才退出。
  • 当管道的写端关闭时,read调用返回0(说明读到了文件末尾 EOF)
  • 最简单的方法是直接将32位(4字节)整型写入管道,而不是使用格式化的 ASCII I/O

image-20220409170003500

素数筛法

用来求 2-n 的所有素数的方法。

埃式筛

基本思想:把从2到n的一组正整数从小到大按顺序排列。从中依次删除2的倍数,3的倍数,5的倍数,直到 的倍数为止,剩余的即为2-n之间的所有素数。 (因为一个质数的倍数一定是合数)

时间复杂度

image-20220409171820430

优化:

由于筛选时,先删除2的所有倍数,再删除3的所有倍数,但在删除3的倍数时,还是从3的2倍开始筛,不过此时 3 * 2 已经被 2 * 3 时筛过了。同理,删除5的倍数时,5 * 2 已经被 2 * 5筛去,5 * 3 已经被 3 * 5 筛去,5 * 4 已经被 2 * 10 筛去,所以对于 i ,只需要从 i * i 开始筛即可。

并且,判断一个数是否是质数,只需要判断到根号N就可以了。因为如果一个因子大于,另外一个因子必定小于。

综上,优化后的埃式筛,是对一个数 i,从 i * i开始筛选,优化后的时间复杂度近似

欧拉筛

又称线性筛,时间复杂度。

埃氏筛是筛去每个质数的倍数,但难免,会有合数会被其不同的质因子多次重复筛去。这就造成了时间浪费。

比如:,120会被2筛去一次, 3筛去一次, 5筛去一次。多做了两次不必要的操作。

欧拉筛的核心思想:让每一个合数被其最小质因数筛到。blog.csdn.net/GD_ONE/arti…

所以,可以将所有质数存储到 数组 primes[]中,然后枚举到第i个数时,就筛去所有的primes[j] * i,这样就在每一次遍历中,正好筛除了所有已知素数的 i 倍。 如果 ,就结束内层循环。 代码:

 主进程:生成 n ∈ [2,35] -> 子进程1:筛掉所有 2 的倍数 -> 子进程2:筛掉所有 3 的倍数 -> 子进程3:筛掉所有 5 的倍数 -> .....
 每个进程输出当前数字集合中最小的数字x(该数字一定是素数,因为它没有被任何比它小的数筛掉,即[2,x-1]中没有它的因子)
 ​
 #include "kernel/types.h"
 #include "kernel/stat.h"
 #include "user/user.h"
 ​
 #define PrimeNum 35
 ​
 void run_child(int* parent_pipe) {
     close(parent_pipe[1]);
 ​
     // 首先读取第一个数字,如果read返回0,说明已经筛选完全部数字,那么关闭所有文件描述符,并退出即可
     // 因为上一个进程在发送完全部数据后,就将写端关闭了,当前进程也将写端关闭了,此时通过管道读的话,如果管道中已经没有数据了,那么read就会返回0
     int n;
     if(read(parent_pipe[0], &n, sizeof(int)) == 0) {
         close(parent_pipe[0]);
         exit(0);
     }
 ​
     int child_pipe[2];
     pipe(child_pipe);
     int tid = fork();
     if (tid < 0) {
         exit(1);
     }
     else if (tid == 0) {
         close(parent_pipe[0]); // 子进程的子进程已经用不到祖父进程的管道了,所以关掉
         run_child(child_pipe);
     }
     else {
         close(child_pipe[0]);
         printf("prime %d \n", n);
         int temp = n;
         while (read(parent_pipe[0], &n, sizeof(int)) != 0) {
             if (n % temp != 0) {
                 write(child_pipe[1], &n, sizeof(int));
             }
         }
         close(child_pipe[1]);
         wait(0);
     }
     exit(0);
 ​
 }
 ​
 int main(int argc, char* argv[]) {
     int parent_pipe[2]; 
     pipe(parent_pipe);
 ​
     int tid = fork();
     if (tid < 0) {
         exit(1);
     }
     else if (tid == 0) {
         run_child(parent_pipe);
     }
     else {
         close(parent_pipe[0]);
         for (int i = 2; i <= PrimeNum; ++i) {
             write(parent_pipe[1], &i, sizeof(int));
         }
         close(parent_pipe[1]);
         wait(0);
     }
     exit(0);
 }

测试(通过):

image-20220409213208760

find(moderate)

要求:

 编写一个简单版本的Unix find 程序:在 目录树 中查找具有特定名称的 所有文件 。
 解决方案应该放在 user/find.c中

提示:

  • 可以查看 user/ls.c 了解如何读目录
  • 通过使用递归允许find查找子目录
  • 不要递归到 '.' 和 '..'
  • 在qemu运行期间,文件系统的更改将一直存在,如果想要还原文件系统可以通过命令 make clean 然后重新sudo make qemu
  • 使用C字符串
  • 注意使用 strcmp()比较字符串,而不是使用==

dirent结构体

当前分支下,xv6中的dirent结构体定义如下:

 // Directory is a file containing a sequence of dirent structures.
 #define DIRSIZ 14
 struct dirent {
   ushort inum;
   char name[DIRSIZ];
 };

所以,dirent起一个类似索引的作用,将文件名存储在结构体中的name成员中。

stat结构体

当前分支下,xv6中的stat结构体定义如下:

 #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
   uint64 size; // Size of file in bytes
 };

所以,stat结构体中存储了文件的详细信息。

当文件的类型是目录时,可以通过以下代码读取该目录下的文件:

 struct dirent de;
 while(read(fd, &de, sizeof(de)) == sizeof(de));

fstat()函数和stat()函数

 int fstat(int fd, struct stat*);

fstat()函数就是用来获取已在文件描述符fd上打开文件的有关信息,将信息存储在stat结构体中

 int stat(const char*, struct stat*);

stat()函数用来将const char* 所指的文件状态,复制到stat结构中
代码:

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"

void find(char* directory, char* file_name) {
    char buf[512], *p;
    int fd;
    struct dirent de;
    struct stat st;
    if ((fd = open(directory, 0)) < 0) {
        fprintf(2, "find: cannot open %s \n", directory);
        exit(1);
    }

    if (fstat(fd, &st) < 0) {
        fprintf(2, "find: cannot stat %s \n", directory);
        close(fd);
        exit(1);
    }

    // 首先要判断传入的directory类型(如果是文件则直接和file_name进行比较)
    // 如果是目录,则循环read到dirent结构(通过read可以读取该目录下的文件),得到其子文件/目录名,拼接得到当前路径后进入递归调用
    switch (st.type) {
        case T_FILE:
            // 如果当前的directory是文件,则只需要判断路径结尾是否是file_name即可
            if (strcmp(directory + strlen(directory) - strlen(file_name), file_name) == 0) {
                printf("%s \n", directory);
            }
            break;
        case T_DIR:
            if (strlen(directory) + 1 + DIRSIZ + 1 > sizeof(buf)) {
                printf("find: path is too long \n");
                break;
            }
            strcpy(buf, directory);
            p = buf + strlen(directory);
            *p = '/';
            ++p;
            while (read(fd, &de, sizeof(de)) == sizeof(de)) {
                if (de.inum == 0)
                    continue;
                memmove(p, de.name, DIRSIZ); // 将当前目录下的文件名复制到目录名之后
                p[DIRSIZ] = 0;  // 进行截断
                if (stat(buf, &st) < 0) {
                    printf("find: cannot stat %s \n", buf);
                    continue;
                }
                // 不要进入'.'和'..'
                switch(st.type) {
                    case T_FILE:
                        if (strcmp(de.name, file_name) == 0) {
                            printf("%s \n", buf);
                        }
                        break;
                    case T_DIR:
                        if ((strcmp(de.name, ".") != 0) && (strcmp(de.name, "..") != 0)) {
                            find(buf, file_name);
                        }
                        break;
                }
            }
            break;
    }
    close(fd);
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        fprintf(2, "find: this call needs two parameters!");
        exit(1);
    }

    char* directory = argv[1];
    char* file_name = argv[2];
    find(directory, file_name);
    exit(0);
}

测试(通过):

image-20220410171818731

xargs(moderate)

要求:

 实现一个简单版本的Unix xargs 程序。
 从标准输入读取行,并为每一行运行命令,将该行作为参数提供给命令
 即将标准输入作为参数一起输入到 xargs 后面跟的命令中,如果标准输入有多行,那么也要多次执行命令
 解决方案应该放在 user/xargs.c中

提示:

  • 使用forkexec在每一行输入上调用命令。使用wait等待子进程退出
  • 读取输入的各个行,每次读取一个字符,直到出现换行符('\n')
  • kernel/param.h声明了MAXARG = 32,它表明了max exec arguments
  • 传入的argv中,第一个参数也就是程序名为xargs,所要执行的参数为第二个参数
  • 通过read命令将之前的输出放到当前命令的后方,如果遇到回车可以直接执行
  • 管道命令用于将前一个命令的标准输出作为下一个命令的标准输入,不过由于大部分命令不接受标准输入作为参数,所以需要用xargs将标准输入转换为命令行参数

代码:

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/param.h"
#define MAXLEN 100  // 定义每个参数的长度最大值

int main(int argc, char* argv[]) {
    char* command = argv[1];
    char ch;
    char param[MAXARG][MAXLEN];  // MAXARG 表示调用exec的最大参数个数
    char* arg[MAXARG];

    while (1) {
        int count = argc - 1;
        memset(param, 0, MAXARG * MAXLEN);
        // 先将xargs后的参数拷贝 (command作为第一个参数)
        for (int i = 1; i < argc; ++i) {
            strcpy(param[i - 1], argv[i]);
        }
        int index = 0;  // 拷贝字符时的下标
        int flag = 0;   // 判断空格前是否有参数
        int result;
        // 拷贝之前的标准输入参数(管道可以将左侧的标准输出转化为标准输入)
        // 如果遇到换行符,则将当前行作为参数进行调用,然后再重新拷贝下一行
        while ((result = read(0, &ch, 1)) > 0 && ch != '\n') {
            // 若遇到空格,且前面已经有一个参数了,则拷贝下一个参数
            if (ch == ' ' && flag == 1) {
                count++;
                index = 0;
                flag = 0;
            }
            else if (ch != ' ') {
                param[count][index++] = ch;
                flag = 1;
            }
        }
        if (result <= 0)
            break;
        for (int i = 0; i < MAXARG - 1; ++i) {
            arg[i] = param[i];
        }
        arg[MAXARG - 1] = 0; 
        if (fork() == 0) {
            exec(command, arg);
            exit(0);
        }
        wait(0);
    }
    exit(0);
}

测试(通过): image-20220411195625234

总体测试

创建一个 time.txt 文件,里面写入一个整数,代表完成此Lab的时间,然后运行命令make grade可以对所有实验进行测试

image-20220412220423401