• MIT6.S081Lab1: Xv6 and Unix utilities


    MIT6.S081 Lab1: Xv6 and Unix utilities

    官方文档

    一.Boot xv6

    如何成功的boot xv6可以看之前的文章MIT6.S081实验环境搭建,只是多一个步骤,在clone的文件夹中执行

    git checkout util
    
    • 1

    切换为util分支即可。

    二.sleep

    在user/sleep.c中编写程序完成用户可以指定tricks数目休眠的sleep程序。

    step1

    检查参数,转换参数

    #include "kernel/types.h"
    #include "user.h"
    
    int main(int argc, char* argv[]) {
      // 检查参数数量
      if (argc != 2) {
        fprintf(2, "输出参数数量错误!\n");
        exit(1);
      }
    
      // 转换参数数量
      int times = atoi(argv[1]);
      exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    step2

    调用user中的sleep从而触发系统调用

    #include "kernel/types.h"
    #include "user.h"
    
    int main(int argc, char* argv[]) {
      // 检查参数数量
      if (argc != 2) {
        fprintf(2, "输出参数数量错误!\n");
        exit(1);
      }
    
      // 转换参数数量
      int times = atoi(argv[1]);
      sleep(times);
      exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    step3

    进行xv6中运行sleep可观察到明显的延迟

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    在对应文件夹命令行执行

    sudo ./grade-lab-util sleep
    
    • 1

    可看到

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    编写成功!

    三.pingpong

    父子进程互相发送一个字节并打印内容输出。

    step1

    创建两个管道

    #include "kernel/types.h"
    #include "user.h"
    
    int main() {
      // 用于接收一个字节
      char buf[1];
    
      // 两个管道,p1用于父写子读,p2用于子写父读
      int p1[2], p2[2];
      pipe(p1);
      pipe(p2);
      exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    step2

    fork一个子进程

    #include "kernel/types.h"
    #include "user.h"
    
    int main() {
      // 用于接收一个字节
      char buf[1];
    
      // 两个管道,p1用于父写子读,p2用于子写父读
      int p1[2], p2[2];
      pipe(p1);
      pipe(p2);
    
      int pid = fork();
    
      if (pid > 0) {
    
      } else if (pid == 0) {
    
      } else {
        printf(2, "fork error!\n");
      }
      exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    step3

    互相传递一个字节,注意写完后要及时关闭否则read会一直阻塞。

    #include "kernel/types.h"
    #include "user.h"
    
    int main() {
      // 用于接收一个字节
      char buf[1];
    
      // 两个管道,p1用于父写子读,p2用于子写父读
      int p1[2], p2[2];
      pipe(p1);
      pipe(p2);
    
      int pid = fork();
    
      if (pid > 0) {
        // 发送字节
        write(p1[1], "1", 1);
        close(p1[1]);
        // 接收字节
        read(p2[0], buf, 1);
        fprintf(1, "%d: received pong\n", getpid());
        exit(0);
      } else if (pid == 0) {
        read(p1[0], buf, 1);
        fprintf(1, "%d: received ping\n", getpid());
        write(p2[1], "1", 1);
        close(p2[1]);
        exit(0);
      } else {
        fprintf(2, "fork error!\n");
        exit(1);
      }
      exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    step4

    在xv6中运行可看到

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    在对应文件夹命令行中执行

    sudo ./grade-lab-util pingpong
    
    • 1

    可看到

    !外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    编写成功!

    四.primes

    利用fork和pipe组成一个pipeline,并进行2到35的素数筛选。

    具体的解决方法可见官方教程。总结来说,就是从左邻居读取的第一个数打印出来作为自己的基数,此后循环从左邻居读取数,判断是否可以被基数整除,如果可以说明不是素数,如果不可以则移交给右邻居。循环这个过程就可以把素数筛选出来。

    有两个关键点:1,左右邻居之间怎么通信;2,及时地关闭不需要的文件描述符,否则xv6支持不了这么多文件描述符。

    左右邻居之间怎么通信:所谓左右邻居就是父子进程。对于每个素数,我们都要创建一个进程,也就是每个进程(除了最后一个)都要fork一次,在fork前我们要创建管道用于通信,但子进程并不知道管道的文件描述符,所以我们要把管道的读端传给子进程,而子进程又要复刻父进程的行为,很自然就想到要用递归函数,函数的参数就是管道的读端文件描述符。

    及时地关闭文件描述符:fork之后父进程用不到读端关闭,子进程用不到写端关闭,子进程用不到父进程本来有的之前的读端关闭

    然后注意每个进程只fork一次,如果fork了之后的循环就要判断不fork了。

    其实这个最主要的就是利用fork和pipe组成一个pipeline,也就是每个线程要有一个读端一个写端,从父进程那里读,写向子进程,不需要的文件描述符关闭,读端来自于父进程的开辟,写端来自于自己的开辟,然后每个子进程要重复父进程的行为,所以用递归函数,参数传递的读端。

    代码如下:

    #include "kernel/types.h"
    #include "user.h"
    
    
    // 每个进程都要执行的操作
    void ProcessOperate(int listen_fd) {
      // 把第一个读取的数作为基数并打印出来
      int base = -1;
      if (read(listen_fd, &base, 4) == -1) {
        fprintf(2, "%d read listen_fd error!\n", getpid());
        exit(-1);
      }
      fprintf(1, "prime %d\n", base);
      
      int is_fork = 0; // 判断当前线程是否已经fork过
    
      // 持续读取数据并处理
      int num = -1;
      int pipes[2]; // 用于父子进程通信
      while (read(listen_fd, &num, 4)) {
        if (num % base != 0) {
          if (!is_fork) {
            is_fork = 1;
            if (pipe(pipes) == -1) {
              fprintf(2, "%d process create pipe error!\n", getpid());
              exit(-1);
            }
            int pid = fork();
            if (pid > 0) {
              close(pipes[0]);
            } else if (pid == 0) {
              close(pipes[1]);
              close(listen_fd);
              ProcessOperate(pipes[0]);
            } else {
              fprintf(2, "%d process fork error\n", getpid());
              exit(-1);
            }
          }
          write(pipes[1], &num, 4);
        }
      }
      close(listen_fd);
      close(pipes[1]);
      wait(0);
      exit(0);
    }
    
    int main() {
      int pipes[2];
      if (pipe(pipes) == -1) {
        fprintf(2, "main process create pipe error!\n");
        exit(-1);
      }
    
      // 先把所有数据写到第一个管道里面
      for (int i = 2; i <= 35; ++i) {
        if (write(pipes[1], &i, 4) == -1) {
          fprintf(2, "main process write pipe error!\n");
          exit(-1);
        }
      }
    
      // 关闭不需要的写端
      close(pipes[1]);
    
      // 传递读端,从第一个进程开始筛选素数
      ProcessOperate(pipes[0]);
    
      exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    在命令行中运行

    sudo ./grade-lab-util primes
    
    • 1

    可看到

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    成功!

    五.find

    写一个简化版本的UNIX的find程序:查找目录树中具有特定名称的所有文件。

    这个程序其实只要看懂了ls程序是怎么写的基本就会写了。

    具体的思路就是对我们输入的目录中的文件或目录进行循环遍历,当遍历到文件,就比较文件的名字和我们要找的名字;当遍历到目录,就递归这个过程。其中用到的函数接口在ls中也会用到,所以看懂了ls程序就知道那些接口怎么用了。

    有几点需要注意的地方:

    • 比较文件的名字和我们要找的名字的时候,我们需要提取简化文件的名字,我们当然可以用ls中的fmtname函数,但是注意fmtname中有一个memset(buf+strlen§, ’ ', DIRSIZ-strlen§); 这个是我们不需要的,因为这个是补空方便输出,我们加上这个名字比较就不对了。
    • 每个目录开头的是两个目录".“和”…",如果不加以排除的话会无限递归,所以要注意
    • 对我来说还有一个,exit是让程序直接退出了,在函数里面还是用return(这个错误找了好久,晕)。

    代码如下:

    #include "kernel/types.h"
    #include "kernel/stat.h"
    #include "user/user.h"
    #include "kernel/fs.h"
    
    // 获取名称
    char* fmtname(char *path)
    {
      static char buf[512];
      char *p;
      // 提取最后一个/跟的名字
      for(p=path+strlen(path); p >= path && *p != '/'; p--)
        ;
      p++;
    
      memmove(buf, p, strlen(p));
      buf[strlen(p)] = 0;
    
      return buf;
    }
    
    
    void find(char *path, char *name) {
      char buf[512], *p;
      int fd;
      struct dirent de;
      struct stat st;
    
      // 打开目录
      if ((fd = open(path, 0)) < 0) {
        fprintf(2, "ls: cannot open %s!\n", path);
        exit(-1);
      }
    
      // 复制路径
      strcpy(buf, path);
      p = buf + strlen(buf);
      *p++ = '/';
    
      // 循环判断目录下的文件或目录
      while (read(fd, &de, sizeof(de)) == sizeof(de)) {
        if (de.inum == 0) {
          continue;
        }
        // 排除目录开头的.和..
        if (!strcmp(de.name, ".") || !strcmp(de.name, "..")) {
          continue;
        }
        // 把文件或目录名加到buf后面
        memmove(p, de.name, strlen(de.name));
        p[strlen(de.name)] = 0;
        if (stat(buf, &st) < 0) {
          fprintf(1, "ls: cannot stat %s \n", buf);
          continue;
        }
        switch(st.type) {
          case T_FILE:
            // 如果是文件直接比较
            if (!strcmp(fmtname(buf), name)) {
              fprintf(1, "%s\n", buf);
            }
            break;
          case T_DIR:
            // 目录递归
            find(buf, name);
            break;
        }
      }
    
      close(fd);
      return;
    
    
    }
    
    int main(int argc, char* argv[]) {
      // 调用函数进行递归查找
      if (argc < 3) {
        find(".", argv[1]);
        exit(0);
      }
      find(argv[1], argv[2]);
      exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    六.xargs

    编写一个简化版UNIX的xargs程序:它从标准输入中按行读取,并且为每一行执行一个命令,将行作为参数提供给命令。

    说明白点,xargs就是执行命令,命令的参数即来自于自身的参数,也来自于标准输入,为标准输入中每一行都执行这样的命令和自身的参数。

    那其实就很简单了,就直接从标准输入中读取数据,然后根据\n来划分行,每找到一行就fork一次让子进程exec来执行它。父进程调整一下指针的位置继续执行。

    代码如下:

    #include "kernel/types.h"
    #include "kernel/param.h"
    #include "user.h"
    
    int main(int argc, char* argv[]) {
      if (argc > MAXARG) {
        fprintf(2, "argc > MAXARG\n");
        exit(-1);
      }
    
      // xargs自身输入的命令及参数
      char* new_argv[MAXARG];
      for (int i = 0; i < argc - 1; ++i) {
        new_argv[i] = argv[i + 1];
      }
    
      char buf[32];
      char *p = buf;
      // 读取命令行的输入
      read(0, buf, 32);
      int pid;
      for (int i = 0; i < 32; ++i) {
        // 如果检测到换行符说明遇到了新的一行,那么就fork让子进程exec
        if (buf[i] == '\n') {
          pid = fork();
          if (pid == 0) {
            // 设定结尾
            buf[i] = 0;
            // 设定标准输入作为一个参数
            new_argv[argc - 1] = p;
            exec(new_argv[0], new_argv);
            fprintf(2, "exec error\n");
          } else {
            // 更新指针,指向下一行
            p = &buf[i + 1];
            wait(0);
          }
        }
      }
    
      wait(0);
      exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    在这里插入图片描述
    在这里插入图片描述

    本来我还有一个版本的,就是用while一个字节一个字节的读,但是死活报错,不知道为什么。感觉应该是对于换行符的判断有问题,所以还是这种直接把所有的标准输入读下来确保会结束的比较保险。

    总结

    这几个实验因为中间有其他事情,所以时间跨度还是挺长的,不过我觉得MIT6.S081的实验真的很有价值,之前可能用过这些指令,但是不清楚这些指令具体怎么实现的,在实现这些指令的过程我对fork,pipe,exec这些系统调用的使用更加熟练了,之前其实没怎么使用过这些系统调用。

    期待下一个实验!

  • 相关阅读:
    【MedusaSTears】怎么禁用edge浏览器截图功能?
    实践小记—静态成员的使用注意(或许由此产生的不知名Bug)
    【图数据库实战】图数据库典型应用场景
    TensorFlow 2.9的零零碎碎(一)-tf.keras里的兜兜转转
    华为仓颉编程语言观感
    分布式事务seata的使用
    uniapp小程序更新逻辑,按实际开发为主
    向日葵远程控制引起惠普战笔记本亮度无法调节问题
    Redis数据库的事物机制(详细讲解)
    OpenCV-Python快速入门(十五):霍夫变换
  • 原文地址:https://blog.csdn.net/qq_46514141/article/details/133913426