Lab1 Xv6 and Unix utilities

Boot xv6 (easy)

从指定仓库clone代码,然后编译-运行,尝试一部分命令,没问题之后就可以正式开始了!

sleep (easy)

实验要求

实现Unix程序sleep,使程序暂定指定数量的ticks。并将解决方案放在文件user/sleep.c中。

一些提示:

  • 参考其他代码如果获取用户输入参数,并对用户输入参数做出一定判断(未指定时间报错exit(1)
  • 可以使用系统调用sleep
  • main函数最后退出应调用exit(0)
  • 将程序添加到Makefile中

具体实现

注意参数判断和时间小于0的情况。主函数实现如下:

int main(int argc, char* argv[]) {
    if (argc < 2) {
        printf("Usage: sleep [time]\n");
        exit(1);
    } else {
        int time = atoi(argv[1]);
        if (time < 0) exit(1);
        sleep(time);
        exit(0);
    }
}

实验结果

  • 程序效果:
    image.png
  • 测试结果:
    image.png

pingpong (easy)

实验要求

实现父子进程之间传输数据并打印进程ID和结果。具体为:父进程发一字节,子进程收到打印"<pid>: receive ping",再把数据发回父进程,退出;父进程收到数据打印"<pid>: receive pong"并退出。文件保存在user/pingpong.c中。

一些提示:

  • 创建管道pipe
  • 创建子线程fork
  • read从管道读取,write写入管道
  • getpid获取进程ID

具体实现

注意事项:

  • 管道读写的方向(0读1写)
  • 注意关闭文件描述符
  • 父进程wait等待子进程结束,这样可保证输出顺序

具体实现:

int main() {
    int fd1[2], fd2[2], pid;
    char buf;
    pipe(fd1);
    pipe(fd2);
    pid = fork();
    if (pid < 0) {
        fprintf(2, "fork() error\n");
        exit(1);
    } else if (pid > 0) { // father
        close(fd1[0]);
        close(fd2[1]);
        write(fd1[1], "a", 1);
        read(fd2[0], &buf, 1);
        close(fd1[1]);
        close(fd2[0]);
        wait(0);
        printf("%d: received pong\n", getpid());
    } else { // child
        close(fd2[0]);
        close(fd1[1]);
        read(fd1[0], &buf, 1);
        write(fd2[1], &buf, 1);
        close(fd2[1]);
        close(fd1[0]);
        printf("%d: received ping\n", getpid());
    }
    exit(0);
}

实验结果

  • 失败结果
    其实最开始我没有加wait等待子进程结束,这样的结果就会造成最基本的并发编程错误,导致父子进程输出的结果可能是交织在一起的,比如这样:
    image.png
    当然也可以通过sleep或其他方式解决
  • 正常结果
    image.png
  • 测试结果
    image.png

primes (moderate)/(hard)

实验要求

实现一个并发的素数筛选程序。具体思路参考:

Hins:

  • 由于XV6资源不足,应及时关闭文件描述符
  • 当写入端关闭时read返回0
  • 直接将int类型数据写入管道

具体实现

实验思路:

  • 首先可以明确父进程的任务:创建管道、开启子进程、写入2~35数据、然后等待就完了,因此父进程相对简单。
  • 根据图片,子进程的任务:

    • 创建管道、创建子进程
    • 从管道接收数据,第一个一定为素数
    • 然后利用该素数淘汰后续数据中是该素数整数倍的数据
    • 未淘汰的数据写入管道传给自己的子进程
  • 不难发现,所有的子进程行为非常相似,因此可用递归实现。
  • 若用递归,则首先需要考虑终止条件:子进程一个数据都收不到时。即其父进程找不到任何符合条件的数据要发送,就关闭了管道的写入端,子进程read时一个数据都没收到就返回了0。
  • 因此,在子进程中可先read一个int数据,若结果大于0则将收到的数作为判断后续数据的基准素数,若结果等于0,则满足递归终止条件直接结束进程。

注意事项:

  • 及时关闭文件描述符,确实是不够
  • 及时回收子进程

实验代码:

  • 子进程递归函数:

    void processSon(int* dad_fd) {
      close(dad_fd[1]);
      int first_n, fd[2], pid, ret;
      ret = read(dad_fd[0], &first_n, sizeof(int));
      if (ret == 0) exit(0);
      printf("prime %d\n", first_n);
      pipe(fd);
      pid = fork();
      if (pid < 0) {
          fprintf(2, "fork() error\n");
          exit(1);
      } else if (pid > 0) {
          int n;
          close(fd[0]);
          while (1) {
              ret = read(dad_fd[0], &n, sizeof(int));
              if (ret > 0) {
                  if (n % first_n != 0) {
                      write(fd[1], &n, sizeof(int));
                  }
              } else {
                  break;
              }
          }
          close(fd[1]);
          close(dad_fd[0]);
          wait(0);
      } else {
          processSon(fd);
      }
    }
  • 主函数

    int main() {
      int fd[2], pid;
      pipe(fd);
      pid = fork();
      if (pid < 0) {
          fprintf(2, "fork() error\n");
          exit(1);
      } else if (pid > 0) { // father
          close(fd[0]);
          for (int i = 2; i < 36; ++i) {
              write(fd[1], &i, sizeof(int));
          }
          close(fd[1]);
          wait(0);
      } else { // son
          processSon(fd);
      }
      exit(0);
    }

测试结果

  • 程序效果
    image.png
  • 测试结果
    image.png

find (moderate)

实验要求

实现简易版UNIX find程序:查找指定目录下所有文件。保存于user/find.c

hints:

  • 可参考ls.c
  • 可通过递归下降到子目录,但不要递归“.”和“..”
  • 使用strcmp()判断字符串是否相同

具体实现

ls.c中得到的提示:

  • 可在main实现缺省参数,在本体函数值专注于有参数正常情况
  • 利用struct statint fstat(int, struct stat*)获取文件状态,其中type字段可判断文件类型(XV6有文件、目录、设备三种类型)
  • 目录文件本身包含的文本信息,可利用struct dirent提取,该结构体中的name字段保存目录中文件名

实现思路:

  • 基本与ls.c思路一样。main函数实现缺省参数。然后调用find函数
  • find函数每次打开一个文件,然后判断该文件类型:
  • 若是文件或设备,就直接打印(递归终止条件)
  • 若是目录,就利用struct dirent读取目录下所有文件名,然后每个文件都递归调用find

注意事项:

  • 不要递归“.”和“..”
  • 提前结束时注意关闭打开的文件描述符

实验代码

  • find函数——实现部分,不考虑缺省参数

    void find(const char* dir) {
      int fd;
      struct stat st; 
    
      if ((fd = open(dir, 0)) < 0) {
          fprintf(2, "find: cannot open %s\n", dir);
          return;
      }
      if (fstat(fd, &st) < 0) {
          fprintf(2, "find: cannot stat %s\n", dir);
          close(fd);
          return;
      }
    
      switch (st.type) {
      case T_DEVICE:
      case T_FILE: {
          printf("%s\n", dir);
          break;
      }
      case T_DIR: {
          struct dirent de;
          char newpath[512];
          char* p;
          if (strlen(dir) + 1 + DIRSIZ + 1 > sizeof(newpath)) {
              printf("find: path too long\n");
              break;
          }
          strcpy(newpath, dir);
          p = newpath + strlen(newpath);
          *p++ = '/';
          while (read(fd, &de, sizeof(de)) > 0) {
              if (de.inum == 0 || strcmp(de.name, ".") == 0 ||
                  strcmp(de.name, "..") == 0)
                  continue;
              memmove(p, de.name, DIRSIZ);
              p[DIRSIZ] = '\0';
              find(newpath);
          }
          break;
      }
      default:
          break;
      }
      close(fd);
    }
  • 主函数——考虑缺省参数,调整后调用find

    int main(int argc, char* argv[]) {
      int i;
      if (argc < 2) {
          find(".");
      } else {
          for (i = 1; i < argc; i++)
              find(argv[i]);
      }
      exit(0);
    }

测试结果

  • 程序效果
    image.png
  • 测试结果
    image.png

xargs (moderate)

实验要求

实现简易版UNIX xargs程序:该程序在命令行输入参数描述一个命令作为其指定命令,然后从标准输入读取数据,将每行字符作为其指定命令附加的参数运行该命令。保存在user/xargs.c中。

hits:

  • 使用forkexec,并在父进程中等待子进程结束
  • 每次读取一个字符直到出现换行符('\n')
  • kernel/param.h中声明了MAXARG表示最大参数数量

具体实现

实现代码:

  • 主要逻辑函数xargs

    void xargs(int argc, char* argv[]) {
      char buf[BUF_SIZE], *start, *cur;
      int len, pid, read_idx = 0;
    
      while ((len = read(0, buf + read_idx,
                         BUF_SIZE - read_idx - 1)) > 0) {
          read_idx += len;
          start = cur = buf;
          while (1) {
              while (cur - buf < read_idx && *cur != '\n')
                  ++cur;
              if (*cur == '\n')
                  *cur = '\0';
              else
                  break;
              argv[argc] = start;
              pid = fork();
              if (pid > 0) {
                  wait((int*)0);
                  // printf("%d end\n", pid);
              } else {
                  // for (int i = 0; i <= argc + 1; ++i) {
                  //     printf("argv[%d]=%s, ", i, argv[i]);
                  // }
                  // printf("begin %s by %d ...", argv[0], getpid());
                  // 我也不知道为什么我的这个文件打不开,利用cat或者vi命令都会卡住不动
                  if (strcmp(argv[2], "./console") == 0) exit(0);  
                  if (exec(argv[0], argv) < 0) {
                      fprintf(2, "exec() error\n");    
                  }
                  exit(0);
              }
              start = cur + 1;
          }
          memmove(buf, start, cur - start);
          read_idx = cur - start;
      }
    }
  • 依然利用主函数main实现缺省参数

    int main(int argc, char* argv[]) {
      char* xargv[MAXARG];
      if (argc == 1) {
          xargv[0] = "echo";
          xargs(argc, xargv);
      } else {
          for (int i = 0; i < argc - 1; ++i) {
              xargv[i] = argv[i + 1];
          }
          xargs(argc - 1, xargv);
      }
      exit(0);
    }

碰到的坑:

  • ./console文件打不开问题
    在过了find | xargs echo ...的命令之后觉得应该没问题了(事实上确实没问题了),然后去尝试find | xargs grep hello,然后就会卡住不动,只能退出虚拟机。贴出来的代码也可以看到,我在exec前打印了要执行的命令、参数和进程号,结束进程也打印一下。加上打印的find | xargs grep hello运行结果如图:
    image.png
    可以发现这个29号进程怎么都结束不了,其对应的文件就是./console。我一直以为是我程序本身的问题(也可能确实是我的问题),就一直debug,一行一行去试,发现确实是这个子进程不结束,父进程一直wait。(现在想想,其实可以在grep中打个断点再继续debug,之后再去尝试吧,但大概率会在read时阻塞)
    然后我明确了,确实是这个文件的问题。重启虚拟机后用其他命令比如cat/vi试了一试,发现也会卡住:
    image.png
    遂作罢,只能特定地跳过这个文件了(还是我太菜)
    通过ls可以查看这个文件的信息。发现这个文件是个设备文件,大小为0。据此,我也只能推测所有试图从该文件中读取数据的程序都读不到数据,也就是read会阻塞,也没办法返回0。
    image.png
    如果我的推测是对的,那么关于grep命令,或许需要先判断要搜索的文件大小是否大于0,然后再去读其中的内容会好一些。
  • 第二个坑——测试时标准输出的'hello'数量
    测试文件中关于xargs的一部分:
    image.png
    被注释掉的是原本的测试语句,通过判断"hello"在标准输出中出现的数量是否为3来确定正确与否。其中xargstest.sh的内容通过echomkdir创建一些文件用于检测,也就是运行find | xargs grep hello时出现的头几行:
    image.png
    它创建了三个有"hello"的文件。但是find命令还另外输入了b,也就是说文件b要被查两次,这么说结果应该是4个hello,而且命令也在标准输出里啊!于是我每次测试它就讲它需要3个而我出了8个。为了过关,我只能把测试的"hello"数量改了。

测试结果

  • 程序效果
    image.png
    image.png
  • 测试结果
    image.png

总结

Lab1还是比较友好也挺有趣的,带我这个小菜鸡正式入了这个课的门。但即便如此,也在最后一题的坑上卡了好久,还需要再接再励!

Lab1测试结果

image.png


Longfar
1 声望3 粉丝