MIT_6.s081_Lab1:Xv6 and Unix utilities

2022-12-08 14:38:55 浏览数 (1)

MIT_6.s081_Lab1:Xv6 and Unix utilities

于2022年3月1日2022年3月1日由Sukuna发布

运行环境:Ubuntu 20.04 qemu

在做6.s081的实验之前我们首先要先下载Xv6操作系统以及qemu虚拟机:

代码语言:javascript复制
sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu 

调试的gbd工具使用方法:在Ubuntu的终端输入这个命令即可

记住端口号,是tcp::26000

另起一个窗口,输入下面命令:

输入 file ./kernel/kernel载入符号表,然后target remote loaclhost:26000即可:

Lab1_1:Boot xv6

运行并安全退出xv6系统:

运行的方法很简单:cd进xv6的文件夹里面,然后输入`make qemu`即可,输入完之后就是进入了xv6的系统里面了

退出的方法就是,先Ctrl A,再输入x即可.

Lab1_2 sleep

本实验要为 xv6 实现 UNIX 程序 sleep; 您的睡眠应暂停用户指定的滴答数。 滴答是 xv6 内核定义的时间概念,即来自定时器芯片的两次中断之间的时间。

一些提示:

  • 在开始编码之前,请阅读xv6 书籍第 1 章。(中文版xv6 书籍)
  • 查看user/中的其他一些程序 (例如,user/echo.cuser/grep.cuser/rm.c)以了解如何获取传递给程序的命令行参数。
  • 如果用户忘记传递参数, sleep 应该打印错误消息。
  • 命令行参数作为字符串传递;您可以使用atoi将其转换为整数(请参阅 user/ulib.c)。
  • 使用系统调用sleep
  • 确保main调用exit()以退出您的程序。
  • 将你的睡眠程序添加到Makefile 中的UPROGS;完成后,make qemu将编译您的程序,您将能够从 xv6 shell 运行它。
  • 查看 Kernighan 和 Ritchie 的书*The C programming language (second edition)*(K&R)以了解 C。
代码语言:javascript复制
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(int argc,char *argv[]){
  for(int i=0;!argv[i];i  ){
    if(argv[1][i]>'9'||argv[1][i]<'0'){
      write(1, "errorn", 6);
      exit(-1);
    }
  }
  int times=atoi(argv[1]);
  sleep(times);
  exit(0);
}

首先我们知道在C语言中argc代表传入的参数数,argv是传入的参数.

第一个for循环就是判断这个是不是一个正常的数字(什么时候结束循环呢?),第二步用atoi函数转化成数字,最后执行系统调用sleep.

Lab1_3 pingpong

编写一个程序,使用 UNIX 系统调用在两个进程之间通过一对管道“乒乓”一个字节,每个管道一个。 父母应该向孩子发送一个字节; 子进程应该打印“: received ping”,其中 是它的进程 ID,将管道上的字节写入父进程,然后退出; 父母应该从孩子那里读取字节,打印“: received pong”,然后退出。

一些提示:

  • 使用管道创建管道。
  • 使用 fork 创建一个孩子。
  • 使用 read 从管道读取,并使用 write 写入管道。
  • 使用 getpid 查找调用进程的进程 ID。
  • 将程序添加到 Makefile 中的 UPROGS。
  • xv6 上的用户程序有一组有限的库函数可供它们使用。 可以在 user/user.h 中看到列表; 源(系统调用除外)位于 user/ulib.c、user/printf.c 和 user/umalloc.c。

我们可以认为pipe是一个Linux进程间通讯的一种方式,一个管道以一个两位的int类型数组构成,其中第一个元素是读端的接口编号,第二个元素是写端的接口编号.然后可以使用read和write来进行读取,注意管道的端口有读端口和写端口之分,读的时候只能从读端口读数据,写的时候只能从写的端口写数据.

有点像我们之前操作系统课上面学到的缓冲区的结构,缓冲区有两端,一端读一端写.

系统调用: 可以使用pipe(一个二位的数组)来初始化一个管道.经过pipe了之后,第一个元素就是一个读取的端口,第二个元素就是对应写入的端口, 可以使用read(读端口,读出来的元素写在哪里,长度)来从一个读的端口读出元素 可以使用write(写端口,写出来的元素写在哪里,长度)来把元素写进一个端口.

fork函数就是一次调用,两次返回,调用之后父进程和子进程都从获得函数的返回值开始继续往下运行,就像一条河流,遇到了一个分叉口,河流分成了两叉,这两叉都从分叉口开始继续往下流.在这里分叉口就像fork()调用,调用生成了两个分叉,两个分叉都从fork()调用结束后继续往下走.

代码语言:javascript复制
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(){
  int p_filedes[2],s_filedes[2];
  pipe(p_filedes);
  pipe(s_filedes);
  char buf[4];
  if(fork()==0){
    read(p_filedes[0],buf,4);
    printf("%d: received %sn",getpid(),buf);
    write(s_filedes[1],"pong",4);
  }else{
    write(p_filedes[1],"ping",4);
    read(s_filedes[0],buf,4);
    printf("%d: received %sn",getpid(),buf);
  }
  exit(0);
}

函数的思路就是先初始化两根管道,接着父进程先read再写,子进程先写再read.

Lab1_4 prime

编写一个程序,完成一个并发版本的prime初筛,第一个进程负责把2-35范围内的素数输入进管道,对于每一个素数,我们都需要fork一个进程来接受管道的数字然后输出出来.

  • 关闭进程不需要的文件描述符,否则你的程序将在第一个进程达到 35 之前耗尽 xv6 的资源。
  • 一旦第一个进程达到 35,它应该等到整个管道终止,包括所有子、孙等。因此,主素数进程应该只在所有输出都被打印出来,并且在所有其他素数进程都退出之后才退出。
  • 当管道的写端关闭时,read 返回零。
  • 将 32 位(4 字节)整数直接写入管道是最简单的,而不是使用格式化的 ASCII I/O。
  • 您应该仅在需要时在管道中创建流程。
  • 将程序添加到 Makefile 中的 UPROGS。

基本的思路在下面,每一个进程对应一个素数,主进程负责传输2-34的数据给子进程们,每个进程对应一个素数,如果这个数%这个素数不为0的话就可以传给下一级的进程,如果没有下一级的进程那么fork一个新的进程,这个数一定是素数,这个进程就会接着处理来自左边邻居的数据,处理的方式.这样子每一个进程就像一个筛子,筛选不可能是素数的数.

总的来说主进程的数据首先从左到右到第一个子进程,判断能不能被2除,不可以就继续从左到右交给下一个子进程,判断能不能被3除…,如果下一个子进程是不存在的,那么新建一个进程,这个进程就代表对应数.

Lab1_5 find

编写一个简单版本的 UNIX 查找程序:查找目录树中具有特定名称的所有文件。给定对应的文件名以及文件名在目录,找到文件名的位置.

  • 查看 user/ls.c 以了解如何读取目录。
  • 使用递归允许 find 访问到子目录。
  • 不要递归到“.” 和 ”..”。
  • 对文件系统的更改在 qemu 运行中持续存在; 要获得一个干净的文件系统,请运行 make clean 然后 make qemu。
代码语言:javascript复制
struct dirent {
  ushort inum;
  char name[DIRSIZ];
};

上面是文件系统中关于目录文件内容的说明,inum说明这个文件占用了几个inode.该操作系统类似于Linux系统,磁盘分成磁盘块,磁盘块的一些相关控制信息就储存在存放在内存中的inode.文件系统获得文件,先找到文件的file结构,根据file结构找到inode,再根据inode找到磁盘块.

说白了目录文件存储的就是一堆dirent类型的结构体.

下面就是stat信息,stat信息存放了文件的一些控制信息,比如说链接信息,大小和类型之类的.在我们利用open打开文件后,open函数会返回一个数字,我们再利用fstat这个调用找到stat控制块.

代码语言:javascript复制
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
};

我们先来看看xv6是如何完成对ls指令的支持的?

首先第一个函数:根据文件的的路径名提取出文件的名字,就是从后往前遍历,找到第一个‘/’,这之间的那一部分就是文件的名字.

接着就是ls函数,ls函数中只需要提供当前的path,找到path里面的所有文件即可.首先先打开当前path对应的文件(Linux内部目录文件和普通的文件都是文件),再利用fstat系统调用找到stat的值.由于目录文件里面就是连续地存储了一堆dirent类型的结构体,那我们可以把目录文件的内容当成一个struct dirent[MAX](结构体数组,一个结构体一个结构体地去读)

最后就是main函数,就是看看输入的指令罢了

代码语言:javascript复制
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"

char* fmtname(char *path)
{
  static char buf[DIRSIZ 1];
  char *p;

  // Find first character after last slash.
  for(p=path strlen(path); p >= path && *p != '/'; p--)
    ;
  p  ;

  // Return blank-padded name.
  if(strlen(p) >= DIRSIZ)
    return p;
  memmove(buf, p, strlen(p));
  memset(buf strlen(p), ' ', DIRSIZ-strlen(p));
  return buf;
}

void ls(char *path)
{
  char buf[512], *p;
  int fd;
  struct dirent de;
  struct stat st;

  if((fd = open(path, 0)) < 0){
    fprintf(2, "ls: cannot open %sn", path);
    return;
  }

  if(fstat(fd, &st) < 0){
    fprintf(2, "ls: cannot stat %sn", path);
    close(fd);
    return;
  }

  switch(st.type){
  case T_FILE:
    printf("%s %d %d %ln", fmtname(path), st.type, st.ino, st.size);
    break;

  case T_DIR:
    if(strlen(path)   1   DIRSIZ   1 > sizeof buf){
      printf("ls: path too longn");
      break;
    }
    strcpy(buf, path);
    p = buf strlen(buf);
    *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("ls: cannot stat %sn", buf);
        continue;
      }
      printf("%s %d %d %dn", fmtname(buf), st.type, st.ino, st.size);
    }
    break;
  }
  close(fd);
}

int main(int argc, char *argv[])
{
  int i;

  if(argc < 2){
    ls(".");
    exit(0);
  }
  for(i=1; i<argc; i  )
    ls(argv[i]);
  exit(0);

}

这个时候我们就可以对ls指令稍微修改修改,ls只用找当前目录第一层的所有文件,那么我们也是找当前目录的所有文件,对于目录文件,我们继续往下搜索这个目录,对于普通文件,我们只用看名字是否匹配即可.

Lab1_6 xargs

这个指令就是我们要把若干条指令合并在一块进行执行.其中前面指令的standard out会作为下一条的指令一个输入来进行执行.

举个例子:前面指令的hello too作为standard out作为下一条指令的输入.

代码语言:javascript复制
$ echo hello too | xargs echo bye
$ bye hello too
  • 使用 fork 和 exec 对每一行输入调用命令。 在父级中使用 wait 等待子级完成命令。
  • 要读取单行输入,请一次读取一个字符,直到出现换行符 (‘n’)。
  • kernel/param.h 声明了 MAXARG,如果您需要声明 argv 数组,这可能很有用。
  • 对文件系统的更改在 qemu 运行中持续存在; 要获得一个干净的文件系统,请运行 make clean 然后 make qemu。
  • 将程序添加到 Makefile 中的 UPROGS。

首先第一步把指令xargs给删除掉:然后把标准输出(来源:0)获取下来,放入最后一个参数中进行执行.

代码语言:javascript复制
#include "kernel/types.h"
#include "kernel/param.h"
#include "user/user.h"

int main(int argc, char *argv[]){
    char* argvs[MAXARG];
    for(int i=1;i<argc;i  ){
        argvs[i-1]=argv[i];
    }
    char buf[512];
    int index;
    int read_len;
    while(1){
        index = -1;
        do{
            index  ;
            read_len=read(0,&buf[index],sizeof(char));
        }while(read_len>0&&buf[index]!='n');
        if(read_len==0&&index==0){
            break;
        }
        buf[index]='';
        argvs[argc-1]=buf;
         if (fork() == 0) {
            exec(argvs[0], argvs);
            exit(0);
        } else {
            wait(0);
        }

    }
    exit(0);
}

0 人点赞