Lab xv6 and Unix utils

- 13 mins

1 Boot xv6

导入实验和启动系统

git clone git://g.csail.mit.edu/xv6-labs-2022
cd xv6-labs-2022
git cheakout util
git commit -am 'my solution for util lab exercise 1'
make qemu

测试自己的代码的正确性

make grade #for all test
make GRADEFLAGS='test' grade # test just for test

或者

.\grade-lab-util <test name>

2 sleep(easy)

实现一个sleep程序,可以暂停用户要求的时间。

2.1 系统调用sleep

本实验只用实现系统调用。

2.2 程序代码

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

int main(int argc, char *argv[]) {
    if (argc < 2 || argc > 2) {
        fprintf(2, "Usage: sleep time\n");
        exit(1);
    }

    for (int i = 0; i < strlen(argv[1]); ++i) {
        if (argv[1][i] < '0' || argv[1][i] > '9') {
            fprintf(2, "Usage: sleep time\n");
            exit(1);
        }
    }

    sleep(atoi(argv[1]));

    exit(0);
}

3 pingpong(easy)

实现一个程序,完成两个进程之间一个字节的信息交流。

实现方法:通过管道

父进程向子进程通过管道发送一个字节的信息后,子进程接受到消息后打印接受消息信息,并向父进程发送一个字节的信息,父进程收到消息之后,打印接受到消息的信息。

3.1 系统调用pipe,read,write,getpid,fork

对于子进程和父进程,他们共享文件描述符,但是相互独立。

3.1.1 pipe

管道是一个小型的系统缓存,并向进程提供两个文件描述符,一个管道读取描述符一个管道写入描述符。用户只能在管道的一端写入,并在另一端读取。管道以这种方式向进程提供了信息交流的方式。

管道具有以下特点:

基于以上特性,数据在管道中只能单向传输,而且不能长期存在(读取后就不存在了)。

3.1.2 获取一个pipe

通过系统调用pipe函数,可以申请得到一个pipe。

int p[2];
pipe(p);

如果申请成功,pipe[0]储存的读取描述符,而p[1]储存的是写入描述符。

3.1.3 fork

fork的作用是创建一个子进程,在父进程中返回子进程的pid,而在子进程中返回0;

有关fork的详细细节,可以参考CSAPP3E的8.4节。

由于父子进程共享文件描述符,所以子进程和父进程都需要手动关闭相关的文件描述符

3.1.4 read & write

详细内容可以参考CSAPP3E第10章。

3.1.5 getpid

获得当前进程的pid。

3.2 代码实现

在父进程创建管道,之后会与子进程共享。

由于子进程和父进程几乎并行,所以我们要先等待子进程结束后再继续执行父进程。(否则子进程的管道就没有读的了,要发生阻塞直到对应的文件描述符关闭,而且没有相应的收到信息的输出)

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

int main(int argc, char *argv[]) {
    if (argc > 1) {
        fprintf(2, "Usage: pingpong\n");
        exit(0);
    }
    int p[2];

    pipe(p);

    char buf[2];
    buf[0] = ' ';
    buf[1] = '\0';

    write(p[1], buf, 1);

    if (fork() == 0) {

        read(p[0], buf, 1);

        fprintf(1, "%d: received ping\n", getpid());

        write(p[1], buf, 1);

        close(p[0]);
        close(p[1]);

        exit(0);
    }

    wait(0);

    read(p[0], buf, 1);

    fprintf(1, "%d: received pong\n", getpid());

    close(p[0]);
    close(p[1]);

    exit(0);
}

4 primes(hard)

阅读 Bell Lab and CSP Thread材料,用进程间的通信完成材料中质数筛选的实验

该程序的思路的流程图;

image-20221018214030001

向每个质数分配一个子进程,用pipe管道向下一个子进程传输不是自己的倍数的数。

4.1 写入管道阻塞

4.1.1 产生原因

因为管道提供的缓存大小有限(很小),当输入到管道中的数据的大小达到管道的最大的容量的时候,写入就会阻塞。

4.1.2 解决方案

为了解决这个问题,需要先创建子进程开始从管道中读取,父进程在此之后提供输入。

4.1.3 新的问题

由于每个进程都要先创建子进程,所以进程会一直创建下去,然后就崩溃了,0.0

4.2 无限子进程

4.2.1 产生原因

见4.1.3

4.2.2 解决方法

该方法的问题:父进程创建一个子进程的时候,子进程会复制父进程的变量的副本(子进程是父进程的复制品),然而父进程一开始就会创建子进程,即使后面父进程的检查发现了问题打上了这个标记,子进程也不知道,因为变量之间相互独立。

由于是筛质数,所以进程数有限,当进程数超过要判断的数的总数之后,就可以不用在产生新的进程了。(特殊解罢了,一旦要判断数的总数增加,这个程序产生的无用的进程就更多了,。。)

4.3 创建进程

由于子进程很多,不太可能写条件嵌套吧?

用递归函数来创建子子进程

形如:

void forkandfork() {
    if (fork() == 0) {
        forkandfork();
    }
    
    exit(0);
}

4.4 进程间的沟通

进程间的沟通参考3 pingpong

实现

while (read(fd0, &value, 32)) {
    if (!prime) {
        prime = value;
        fprintf(1, "prime %d\n", prime);
    }

    if (value % prime != 0) {
        write(p[1], &value, 32);
    }
}

4.5 代码实现

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

int p[2];
int flag = 1;
int count = 0;
int num = 35;

void forkPrime() {
    count++;

    if (count > num) {
        close(p[0]);
        close(1);
        exit(0);
    }


    int fd0 = p[0];

    pipe(p);

    if (fork() == 0) {
        close(fd0);
        close(p[1]);
        
        forkPrime();
    }

    close(p[0]);

    int prime = 0;
    int value = 0;

    while (read(fd0, &value, 32)) {
        if (!prime) {
            prime = value;
            fprintf(1, "prime %d\n", prime);
        }

        if (value % prime != 0) {
            write(p[1], &value, 32);
        }
    }

    close(fd0);
    close(p[1]);
    close(1);

    wait(0);

    exit(0);
}

int main(int argc, char *argv[]) {
    close(0);
    close(2);

    pipe(p);

    if (fork() == 0) {
        close(p[1]);
        forkPrime();
    }

    close(p[0]);

    for (int i = 2; i <= num; ++i) {
        write(p[1], &i, 32);
    }

    close(p[1]);
    close(1);

    wait(0);

    exit(0);
}

5 find(moderate)

编写一个程序,实现在指定文件夹以及子文件夹中搜索要查找到的文件。

5.1 阅读ls.c

ls.c 源码

#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 %s\n", path);
    return;
  }

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

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

  case T_DIR:
    if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){
      printf("ls: path too long\n");
      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 %s\n", buf);
        continue;
      }
      printf("%s %d %d %d\n", 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);
}

5.1.1 fmtname函数

该函数的作用是:提取路径中的文件名。

对于memset(buf + strlen(p), ' ', DIRSIZ - strlen(p))的理解,此处用‘ ’对末尾数组的初始化并不是错误的,这样初始化的目的是为了ls在打印的时候列对齐。(不知道这个目的的意义有多大。。。

5.1.2 ls 函数

很明显,该函数的目的就是打印当前目录下的内容

5.1.2.1 目录类型

按照xv6的参考文档,目录可以分成三种类型:

三种形式都可以打开,并且我们可以用fstat函数获得该目录的信息。信息储存在struct stat中。该结构体的type属性表示了该目录的类型:

5.1.2.2 实现的思路

打开该目录,保存对应的文件描述符。并用fstat获得该文件描述符中的信息。如果该目录的类型是文件,或者是设备文件,就直接打印文件名。如果该目录类型是目录,则该文件里描述了该目录中的文件信息。每个文件信息可以用struct dirent来表示。

struct dirent

struct dirent {
    int inum;
    char *name;
};

inum表示inode number;目录进入节点编号,如果inode的节点编号为0,则表示该文件不存在可以跳过。name就是表示文件名了。读取到该信息之后,就把文件名拼接到路径名上。再用stat函数获得该文件的信息,并打印出文件名,文件类型,文件的节点编号,文件的大小。

5.2 仿写ls.c

如果说文件系统是一棵树,那么我认为每个文件和设备文件都是叶子节点,每个文件夹目录就是一个内节点。

5.2.1 仿写fmtname函数

唯一要注意的一点是,最后的memset的初始化的值是0,因为我们此时的目的不在是列表对齐了,而是纯纯的一个文件名!!

5.2.2 仿写ls函数

首先,我们也必须知道当前的目录路径是什么类型,所以也需要对目录进行打开并且获得他的文件描述符中的相关信息。如果该目录路径是一个文件或者是一个设备文件,我们就获得他的文件名和要查找的文件名进行比较,如果匹配,就输出当前的路径。不匹配,则继续。

如果该目录路径类型是一个文件夹,则我们要查找该文件夹中的所有文件,从该目录文件里每次读取一个struct dirent,如果读取内容有效,拼接文件名到当前目录路径上,然后继续搜素。

5.2.3 防止死循环

由于每个文件中都有.文件夹和..文件夹,分别表示当前文件夹和父文件夹。搜素并不需要访问父文件夹,所以..文件我们可以直接跳过,然后对于.文件夹,我们就不嫩直接跳过了(因为程序开始就是.文件夹,跳过就不用搜索辣:)。

为了解决这个问题,我们保存当前目录路径的iNode编号,以及即将要查找的文件路径的iNode编号,如果相同,则我们就不用再继续搜索这个文件夹。

5.2.4 仿写出来的find函数

void search(char *path, const char *file) {

    if (strcmp(fmtname(path), "..") == 0) {
        return;
    }

    char buf[512], *p;
    int fd;
    struct dirent de;
    struct stat st;

    if ((fd  = open(path, 0)) < 0) {
        fprintf(2, "find: cannot open %s\n", path);
        exit(1);
    }

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

    switch (st.type) {
        case T_FILE:
            if (strcmp(file, fmtname(path)) == 0) {
                fprintf(1, "%s\n", path);
            }
            break;
        case T_DEVICE:
            if (strcmp(file, fmtname(path)) == 0) {
                fprintf(1, "%s\n", path);
            }
            break;
        case T_DIR:
            if (strlen(path) + 1 + DIRSIZ + 1 > sizeof(buf)) {
                fprintf(2, "find: path too long\n");
                close(fd);
                exit(1);
            }

            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;

                struct stat prest;

                if (stat(buf, &prest) < 0) {
                    fprintf(2, "find: cannot stat %s\n", p);
                    continue;
                }

                if (st.ino == prest.ino) {
                    continue;
                }

                search(buf, file);
            }

            break;
        default:
            break;
    }

    close(fd);
}

至此整体代码就基本实现了。

5.3 代码实现

#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;

    for (p = path + strlen(path); p >= path && *p != '/'; p--);
    p++;

    if (strlen(p) >= DIRSIZ) {
        return p;
    }

    memmove(buf, p, strlen(p));
    memset(buf + strlen(p), 0, DIRSIZ - strlen(p));

    return buf;
}

void search(char *path, const char *file) {

    if (strcmp(fmtname(path), "..") == 0) {
        return;
    }

    char buf[512], *p;
    int fd;
    struct dirent de;
    struct stat st;

    if ((fd  = open(path, 0)) < 0) {
        fprintf(2, "find: cannot open %s\n", path);
        exit(1);
    }

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

    switch (st.type) {
        case T_FILE:
            if (strcmp(file, fmtname(path)) == 0) {
                fprintf(1, "%s\n", path);
            }
            break;
        case T_DEVICE:
            if (strcmp(file, fmtname(path)) == 0) {
                fprintf(1, "%s\n", path);
            }
            break;
        case T_DIR:
            if (strlen(path) + 1 + DIRSIZ + 1 > sizeof(buf)) {
                fprintf(2, "find: path too long\n");
                close(fd);
                exit(1);
            }

            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;

                struct stat prest;

                if (stat(buf, &prest) < 0) {
                    fprintf(2, "find: cannot stat %s\n", p);
                    continue;
                }

                if (st.ino == prest.ino) {
                    continue;
                }

                search(buf, file);
            }

            break;
        default:
            break;
    }

    close(fd);
}

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

    if (argc < 2 || argc > 3) {
        fprintf(2, "Usage: find <filename>\n");
        exit(1);
    }

    search(argv[1], argv[2]);

    exit(0);
}

6 xargs(moderate)

编写一个程序,实现linux xargs基础功能。

xargs通常和管道使用,例如

echo bye too | xargs echo hello

首先管道的左边执行echo bye too此时会从标准输出流中输出bye too。而管道右侧执行xargs程序,该程序从管道的左侧的输出读取信息(从该程序的标准输入流中读取),作为即将要运行的echo程序的扩展参数。

即整个命令行相当于执行了echo hello bye too,所以打印出了hello bye too

6.1 实现方式

6.1.1 读取左侧的参数

定义一个全局变量buf来保存读取到的信息流。按照题目描述的要求,每一行就是一个参数,所以只要读到换行符,我们就将该换行符保存为‘\0’,表示一个字符串(即参数)的结束,并累计参数计数器。

6.1.2 整合程序参数

新声明一个args字符指针数组,保存每个参数的起始地址。首先将命令行中本有的参数保存在args中,在从buf中读取累计的参数个数的参数的起始地址。

int argptr;
char *args[MAXARG];
char buf[512 * MAXARG];

void getargs() {
    char *p = buf;
    char ch;
    int count = 0;

    while (p && read(0, &ch, 1)) {
        if (ch == '\n') {
            count++;
            *p++ = '\0';
        } else {
            *p++ = ch;
        }
    }

    *p = '\0';

    if (read(0, &ch, 1)) {
        fprintf(2, "xargs: args too long\n");
        exit(1);
    }

    if (count > MAXARG) {
        fprintf(2, "xargs: args too long\n");
        exit(1);
    }

    p = buf;
    
    while (count--) {
        args[argptr++] = p;
        p += strlen(p);
        p++;
    }
}

整个程序的难点就在这里了。

6.1.3 新建子进程

用fork和exec运行程序即可。

6.2 代码实现

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

int argptr;
char *args[MAXARG];
char buf[512 * MAXARG];

void getargs() {
    char *p = buf;
    char ch;
    int count = 0;

    while (p && read(0, &ch, 1)) {
        if (ch == '\n') {
            count++;
            *p++ = '\0';
        } else {
            *p++ = ch;
        }
    }

    *p = '\0';

    if (read(0, &ch, 1)) {
        fprintf(2, "xargs: args too long\n");
        exit(1);
    }

    if (count > MAXARG) {
        fprintf(2, "xargs: args too long\n");
        exit(1);
    }

    p = buf;
    
    while (count--) {
        args[argptr++] = p;
        p += strlen(p);
        p++;
    }
}

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

    for (int i = 1; i < argc; ++i) {
        args[argptr++] = argv[i];
    }

    getargs();

    if (fork() == 0) {
        if (exec(argv[1], args) < 0) {
            fprintf(2, "xargs: exec %s fail\n", argv[1]);
            exit(1);
        }
    }

    wait(0);

    exit(0);
}

7 END

整个lab就写完了,测试结果如下。

== Test sleep, no arguments == 
$ make qemu-gdb
sleep, no arguments: OK (4.9s) 
== Test sleep, returns == 
$ make qemu-gdb
sleep, returns: OK (0.9s) 
== Test sleep, makes syscall == 
$ make qemu-gdb
sleep, makes syscall: OK (0.9s) 
== Test pingpong == 
$ make qemu-gdb
pingpong: OK (0.9s) 
== Test primes == 
$ make qemu-gdb
primes: OK (1.0s) 
== Test find, in current directory == 
$ make qemu-gdb
find, in current directory: OK (1.2s) 
== Test find, recursive == 
$ make qemu-gdb
find, recursive: OK (1.1s) 
== Test xargs == 
$ make qemu-gdb
xargs: OK (1.3s) 
== Test time == 
time: OK 
Score: 100/100
Pei Meng

Pei Meng

Someone who interested in Computer Architecture.