【MIT 6.S081】实验一:Xv6 and Unix utilities 思路与代码

前言

在华南理工大学的第五个年头,学OS已经第三年了,纸上得来终觉浅啊!所以决定动手完成一下MIT 6.S081的实验。 关于6.S081的美誉我也不赘述了。

实验使用win10 + wsl2 Ubuntu 20.04完成。

内容总览

本实验共有六个内容,都是自己通过调用系统函数实现一些工具函数,包括:

  • sleep:什么也不敢,休眠一段时间。
  • pingpong:父进程给子进程发消息,子进程收到后打印ping,子进程给父进程发消息,父进程收到后打印pong
  • primes:用多进程与管道的方法计算质数,是比较有意思的实验。
  • find:查找目标名称的文件。
  • xargs:实现类似unix的xargs,用来传参的。

reference

  • 第一章实验讲义
  • 课程内容翻译
  • MIT 6.S081 Operation system Engineering-2021 lab1-util
  • 【MIT 6.S081】 ubuntu20.04配置gdb.

环境搭建

亲测成功!【MIT6.S081/6.828】手把手教你搭建开发环境

内容1:sleep

Implement the UNIX program sleep for xv6; your sleep should pause for a user-specified number of ticks. A tick is a notion of time defined by the xv6 kernel, namely the time between two interrupts from the timer chip. Your solution should be in the file user/sleep.c.

提示和理解

  1. 先读xv6 book的第一章

  2. 看e.g., user/echo.c, user/grep.c, and user/rm.c,看看人家是怎么读入参数的

  3. 如果忘记传参,程序需要报错。

  4. atoi (see user/ulib.c). 可以将默认传参的string转为int

  5. 使用系统调用sleep.

  6. 看xv6的核心代码kernel/sysproc.c如何实现sys_sleep

  7. 确保main最后调用了exit

  8. 在makefile里面,UPROGS加入sleep程序。这样make qemu会编译你的sleep并且可以命令行执行。

代码

第一个程序比较简单,就是简单的包装一下系统调用sys_sleep(),这个程序主要是为了熟悉一下做lab的流程:

  1. 创建user/sleep.c
  2. MakefileUPROGS下添加$U/_sleep\
  3. 编写sleep.c
  4. 运行测试程序grade-lab-util并且指明要测试的函数。如 grade-lab-util sleep
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

void main(int argc, char *argv[])
{
    if (argc < 2)
    {
        fprintf(2, "Usage: %s  \n", argv[0]);
        exit(1);
    }
    if (sleep(atoi(argv[1])) < 0)
    {
        fprintf(2, "Sleep error\n");
    }
    exit(0);
}

调用成功的话会显示:

【MIT 6.S081】实验一:Xv6 and Unix utilities 思路与代码_第1张图片

内容2:pingpong

Write a program that uses UNIX system calls to ‘‘ping-pong’’ a byte between two processes over a pair of pipes, one for each direction.

  1. The parent should send a byte to the child;
  2. the child should print “: received ping”, where is its process ID, write the byte on the pipe to the parent(这里没有指定具体写什么内容,什么都可以,只是作为标志,且这个标志不需要也不要打印,否则会影响测评), and exit;
  3. the parent should read the byte from the child, print “: received pong”, and exit.

Your solution should be in the file user/pingpong.c.

提示和理解

  • Use pipe to create a pipe. 我们知道管道是单向的,所以要有两条管道,一个是父进程到子进程,一个是子进程到父进程。如何用管道在父子进程之间通信呢?老生常谈的问题了。
    • 先创建pipe,即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。
    • fork一下,父子进程会共享这俩fds
    • 父子进程通过关闭相对应的fds,就可以实现读写了。
      • 譬如父进程关闭用于写的fd[1],子进程关闭用于读的fd[0],所以就这个管道就可以用来:子进程写,父进程读。 反过来同理
      • 下图是:父进程写,子进程读

【MIT 6.S081】实验一:Xv6 and Unix utilities 思路与代码_第2张图片

  • Use fork to create a child. 写个if判断是否是子进程
  • Use read to read from the pipe, and write to write to the pipe.
  • Use getpid to find the process ID of the calling process.

代码

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

#define BUFFER_SIZE 16
#define INDEX_READ 0
#define INDEX_WRITE 1

int main(int argc, char *argv[])
{
    int fds_p2c[2]; //父进程到子进程
    int fds_c2p[2]; //子进程到父进程

    //创建两个管道
    pipe(fds_c2p);
    pipe(fds_p2c);

    int pid;
    pid = fork(); //fork出子进程
    //子进程
    if (pid == 0)
    {
        //通信方向:子进程到父进程,子进程不需要读
        close(fds_c2p[INDEX_READ]);
        //通信方向:父进程到子进程,子进程不需要写
        close(fds_p2c[INDEX_WRITE]);

        //step2:子进程收到ping,打印
        char buf[BUFFER_SIZE];
        if (read(fds_p2c[INDEX_READ], buf, 1) == 1)
        {
            printf("%d: received ping\n", getpid());
        }

        //step3: 子进程继续向父进程发出信号
        write(fds_c2p[INDEX_WRITE], "f", 1);

        exit(0);
    }
    //父进程
    else
    {
        //通信方向:子进程到父进程,父进程不需要写
        close(fds_c2p[INDEX_WRITE]);
        //通信方向:父进程到子进程,父进程不需要读
        close(fds_p2c[INDEX_READ]);

        //step1: 父进程给子进程发ping
        write(fds_p2c[INDEX_WRITE], "f", 1);

        //step4:父进程读取子进程发出的ping
        char buf[BUFFER_SIZE];
        if (read(fds_c2p[INDEX_READ], buf, 1) == 1)
        {
            printf("%d: received pong\n", getpid());
        }
    }

    //父进程结束
    exit(0);
}

内容3:primes

Write a concurrent version of prime sieve using pipes. This idea is due to Doug McIlroy, inventor of Unix pipes. The picture halfway down this page and the surrounding text explain how to do it. Your solution should be in the file user/primes.c.

Your goal is to use pipe and fork to set up the pipeline.

  1. The first process feeds the numbers 2 through 35 into the pipeline.
  2. For each prime number, you will arrange to create one process that reads from its left neighbor over a pipe and writes to its right neighbor over another pipe.

Since xv6 has limited number of file descriptors and processes, the first process can stop at 35.

提示和理解

  1. 谨慎地去关闭不需要再用的fd。否则在第一个进程到35前,fd不够用
  2. 如果第一个进程达到了35,那么它需要等待整个pipeline终止。
  3. read函数返回0,如果管道写的那一侧已经关闭了。
  4. 相比写字符,最简单的方法往管道是写int
  5. 只有在需要进程的时候才创建它

这道题,最关键的理解看下面这张图:

【MIT 6.S081】实验一:Xv6 and Unix utilities 思路与代码_第3张图片

这张图一共打印了这几个数字:2,3,5,7,11

有几个重要的理解:

  1. 第一个进程,会向第二个进程通过管道写入**[2,35]** 一共34个数字。
  2. 第二个进程,首先会输出第一个收到的数字i(它一定是质数,因为前面的都除以过了,而没有被筛选掉)。然后它就会看看后面来的数是否可以被i整除
    1. 如果可以被整除,说明这个数肯定不是质数了,不用往后传。
    2. 如果不可以被整除,那么往后传,可能是质数。
  3. 第三个进程的逻辑跟第二个进程是一样的。

思考1:什么时候创建管道和fork?

想想内容2的pingpong,肯定是先pipe再fork。

思考2: 什么时候关闭进程?

提示让我们谨慎关闭不要的进程,但同时第一个进程需要等待所有进程的结束。所以不可以不等待,直接退出。每个父进程使用wait(0)去等待子进程完成。

代码

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

#define INDEX_READ 0
#define INDEX_WRITE 1

//创建函数是因为会反复调用这个函数(递归)
void child(int fds_p2c[])
{
    //通讯方向:父进程到子进程,子进程不需要写
    close(fds_p2c[INDEX_WRITE]);

    int i;
    if (read(fds_p2c[INDEX_READ], &i, sizeof(i)) == 0)
    {
        //如果已经读不到数字了,说明已经全部素数都输出完毕了。这个时候子进程可以关闭管道,直接退出。
        close(fds_p2c[INDEX_READ]);
        exit(0);
    }
    printf("prime %d\n", i);
    int num = 0;
    //子进程到孙子进程的管道
    int fds_c2gc[2];
    pipe(fds_c2gc);
    int pid;
    //孙子进程,递归调用本函数
    if ((pid = fork()) == 0)
    {

        child(fds_c2gc);
    }
    //子进程
    else
    {
        //通讯方向:子进程到孙子进程,子进程不需要读
        close(fds_c2gc[INDEX_READ]);
        while (read(fds_p2c[INDEX_READ], &num, sizeof(num)) > 0)
        {
            //如果可以不整除才发出去
            if (num % i != 0)
            {
                write(fds_c2gc[INDEX_WRITE], &num, sizeof(num));
            }
        }
        close(fds_c2gc[INDEX_WRITE]);
        //一样要等待所有的子进程结束
        wait(0);
    }

    //子进程结束
    exit(0);
}

int main(int argc, char *argv[])
{
    int fds_p2c[2]; //父进程到子进程
    pipe(fds_p2c);
    int pid;
    //子进程
    if ((pid = fork()) == 0)
    {
        child(fds_p2c);
    }
    //父进程
    else
    {
        //通讯方向:父进程到子进程,子进程不需要读
        close(fds_p2c[INDEX_READ]);
        for (int i = 2; i <= 35; i++)
        {
            //todo: 这里居然是传i的地址!
            write(fds_p2c[INDEX_WRITE], &i, sizeof(i));
        }

        //必须关闭管道,否则子进程在read函数阻塞
        close(fds_p2c[INDEX_WRITE]);

        //等待子进程结束
        wait(0);
    }

    //父进程结束
    exit(0);
}

请添加图片描述

内容4:find

Write a simple version of the UNIX find program: find all the files in a directory tree with a specific name. Your solution should be in the file user/find.c.

理解和提示

  1. user/ls.c怎么读取文件夹
  2. 用递归的方法来处理子文件,但除去...
  3. Changes to the file system persist across runs of qemu; to get a clean file system run make clean and then make qemu.
  4. strcmp()来比较c string

代码

这部分代码涉及很多的字符串处理,我很不熟悉,copy了不少。这两部分的不熟悉:

  1. 如何把前缀的文件目录拼接出来
  2. 如何去掉前缀目录删掉以做比较

参考了别人的代码,但写了很多注释。

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

char *fmtname(char *path)
{
    char *p;
    for (p = path + strlen(path); p >= path && *p != '/'; p--)
        ;
    p++;
    return p;
}
void equal_print(char *path, char *findname)
{
    if (strcmp(fmtname(path), findname) == 0)
        printf("%s\n", path);
}

void find(char *dir_name, char *file_name)
{
    int fd;

    if ((fd = open(dir_name, 0)) < 0)
    {
        fprintf(2, "ls: cannot open %s\n", dir_name);
        return;
    }

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

    //循环判断
    struct dirent de;
    //buf是用来记录文件前缀的,这样才会打印出之前的目录
    char buf[512], *p;
    switch (st.type)
    {
    case T_FILE:
        equal_print(dir_name, file_name);
        break;
    case T_DIR:
        if (strlen(dir_name) + 1 + DIRSIZ + 1 > sizeof buf)
        {
            printf("find: path too long\n");
            break;
        }
        //将path复制到buf里
        strcpy(buf, dir_name);
        //p为一个指针,指向buf(path)的末尾
        p = buf + strlen(buf);
        //在末尾添加/ 比如 path为 a/b/c 经过这步后变为 a/b/c/<-p
        *p++ = '/';
        // 如果是文件夹,则循环读这个文件夹里面的文件
        while (read(fd, &de, sizeof(de)) == sizeof(de))
        {
            if (de.inum == 0 || (strcmp(de.name, ".") == 0) || (strcmp(de.name, "..") == 0))
                continue;
            //拼接出形如 a/b/c/de.name 的新路径(buf)
            memmove(p, de.name, DIRSIZ);
            p[DIRSIZ] = 0;
            //递归查找
            find(buf, file_name);
        }
        break;
    }
    close(fd);
}

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

    if (argc != 3)
    {
        printf("Usage: find  \n");
        exit(-1);
    }

    find(argv[1], argv[2]);
    exit(0);
}

【MIT 6.S081】实验一:Xv6 and Unix utilities 思路与代码_第4张图片

内容5:xargs

Write a simple version of the UNIX xargs program: read lines from the standard input and run a command for each line, supplying the line as arguments to the command. Your solution should be in the file user/xargs.c.

Please note that xargs on UNIX makes an optimization where it will feed more than argument to the command at a time. We don’t expect you to make this optimization. To make xargs on UNIX behave the way we want it to for this lab, please run it with the -n option set to 1.

You may have to go back and fix bugs in your find program. The output has many $ because the xv6 shell doesn’t realize it is processing commands from a file instead of from the console, and prints a $ for each command in the file.

提示和理解

xargs用得少,可以先学习一下。Linux xargs 命令

真是考到我不会的东西了,等我用的多再回来写这个。

后言

做这个实验工作量还是很大的,需要耐心和信心,慢工出细活。

欢迎大家留言交流。

你可能感兴趣的:(笔记,后端,linux)