mit6.s081 lab1:Unix Utilities

发布时间 2023-04-06 22:09:32作者: 白日梦想家-c

1 sleep(easy)

要求:为 xv6实现 UNIX 程序睡眠; 睡眠需要暂停一段用户指定的时间。刻度是由 xv6内核定义的时间概念,即定时器芯片两次中断之间的时间。解决的程序应该在 user/sleep. c 文件中。
 
一些小提示:

  • 查看user/中的其它程序,如echo.c,grep.c或rm.c,明白如何获取传递给程序的命令行参数。
  • 如果用户忘记传入参数,程序应打印一条错误信息
  • 命令行参数作为字符串传递,可以用atoi转换为整数(可参考ulib.c)
  • 需要使用系统调用sleep
  • user/user.h头文件中包含了sleep的C定义
  • 确保调用exit来退出程序
  • 完成解答后应将你的 sleep 程序添加到 Makefile 的 uPROGS 中,这样使用make qemu 才会编译你的程序,以后就可以在 xv6 shell 中运行它了。
  • 运行./grade-lab-util sleep来测试你的程序
     

解决:

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

int
main(int argc, char *argv[])
{
    if(argc <= 1) {
        fprintf(2, "sleep required the arguements\n");
        exit(1);
    }
    sleep(atoi(argv[1]));
    exit(0);
}

对以上程序的说明:

  • main的参数列表中,argc表示命令行中传入的参数个数,argv数组则是以字符串形式保存传入的参数,默认传入的第一个参数为文件本身
  • fprintf指定输出到一个流文件中,函数原型为int fprintf( FILE *stream, const char *format, [ argument ]...),fprintf()函数根据指定的格式(format),向输出流(stream)写入数据(argument)。这里将错误通过fd2写入屏幕中的stderr。或者也可以使用write函数来打印错误信息,毕竟fprintf函数最终也是要通过系统调用write的。形式为write(int fd, char *buf, int n),参数 fd 是文件描述符,0 表示标准输入,1 表示标准输出,2 表示标准错误。参数 buf 是程序中存放写的数据的字符数组。参数 n 是要传输的字节数,调用 user/ulib.c 的 strlen() 函数就可以获取字符串长度字节数。

2 pingpong(easy)

要求:编写一个程序,使用 UNIX 系统调用通过一对管道在两个进程之间“ ping-pong”一个字节,每个管道对应一个方向。父级应该向子级发送一个字节; 子级应该打印“pid: received ping”,其中 < pid > 是它的进程 ID,将管道上的字节写入父级,然后退出; 父级应该从子级读取字节,打印“ pid: received pong”,然后退出。将解决方案写在 user/pingpong.c 文件中。
 
一些提示:

  • 使用pipe来创建管道
  • 使用fork创建子进程
  • 使用read和write实现对管道的读和写
  • 对于当前进程使用getpid来获得子进程的PID
  • 在xv6中可使用的库函数有限,用户可以在user/user.h中看到这个列表;源代码(除系统调用外),位于ulib.c、printf.c和umalloc.c中。
     
    解决:
#include "kernel/types.h"
#include "user/user.h"

#define READFD 0
#define WRITEFD 1

int
main(int argc,char *argv[])
{
    int p1[2];//parent->child
    int p2[2];//child->parent
    char buf[1];

    pipe(p1);
    pipe(p2);

    int pid;
    pid=fork();

    if(pid<0){
        exit(1);
    }
    else if(pid==0){
        close(p1[WRITEFD]);//完成使用文件的操作后应该关上以便后续使用
        close(p2[READFD]);

        read(p1[READFD],buf,1);
        printf("%d: received ping\n",getpid());
        write(p2[WRITEFD],"6",1);

        close(p1[READFD]);
        close(p2[WRITEFD]);
        exit(0);
    }
    else {
        close(p1[READFD]);
        close(p2[WRITEFD]);

        write(p1[WRITEFD],"6",1);
        read(p2[READFD],buf,1);
        printf("%d: received pong\n",getpid());

        close(p2[READFD]);
        close(p1[WRITEFD]);
        exit(0);
    }
    exit(0);
}

对以上程序的说明:

  • 父进程中写-读-打印,子进程中读-打印-写
  • 每次使用完文件后应及时关闭
  • 管道是单向的,需要两个管道实现ping-pong,一个父->子,一个子->父

3 primes(moderate)

要求:使用管道编写一个基本筛选器的并发版本,将2-35的素数筛出来。程序应该使用pipe和fork来设置管道。第一个进程将数字2到35输入到管道中。对于每个素数,需要创建一个进程,该进程通过一个管道从左边的邻居读取数据,并通过另一个管道向右边的邻居写入数据。由于 xv6的文件描述符和进程数量有限,因此第一个进程的输入到35即可。相关提示文档在这里
 
一些提示:

  • 注意关闭进程不需要的文件描述符,否则程序将在第一个进程达到35之前耗尽资源来运行xv6。
  • 最开始的父进程要在所有子孙进程都退出之后才能退出。
  • 当管道的写端关闭时,read返回0
  • 最简单的方法是将32位int直接写入管道,而不是使用格式化的 ASCII I/O。
     

对帮助文档的说明:
里面大部分是讲历史和一些实验无关的东西,对于本实验的提示主要就是:首先将数字全部输入到最左边的管道,然后第一个进程打印出输入管道的第一个数 2 ,并将管道中所有 2 的倍数的数剔除。接着把剔除后的所有数字输入到右边的管道,然后第二个进程打印出从第一个进程中传入管道的第一个数 3 ,并将管道中所有 3 的倍数的数剔除。接着重复以上过程,最终打印出来的数都为素数。这个过程如下图所示:

 

实现:

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

#define READFD 0
#define WRITEFD 1

void
primes(int pp[])
{
    int pr[2];

    close(pp[WRITEFD]);
    int first_prime;
    if(read(pp[READFD],&first_prime,sizeof (int)))
    {
        printf("prime %d\n",first_prime);

        pipe(pr);//能从左边的管道读到再创建右边的
        int pid=fork();
        if(pid<0)
        {
            exit(1);
        }
        else if(pid>0)
        {
            int digit;
            close(pr[READFD]);
            while(read(pp[READFD],&digit,sizeof (int)))
            {
                if(digit%first_prime!=0)
                {
                    write(pr[WRITEFD],&digit,sizeof (int));
                }
            }
            close(pr[WRITEFD]);//所有数写完后就关掉这个文件
            wait(NULL);
            exit(0);
        }
        else
        {
            primes(pr);
        }
    }
    else
    {
        exit(0);
    }
}


int
main(int argc,char *argv[])
{
    int p[2];
    pipe(p);

    int pid=fork();
    if(pid<0)
    {
        exit(1);
    }
    else if(pid>0)
    {
        close(p[READFD]);
        for(int i=2;i<=35;i++)
        {
            write(p[WRITEFD],&i,sizeof (int));
        }
        close(p[WRITEFD]);
        wait(NULL);
    }
    else
    {
        primes(p);
    }
    exit(0);
}

对以上程序的说明:

  • 设计思想不是很难,在main中首先创建一个管道,然后从父进程中把2-35传给子进程,从第一个子进程开始用函数primes递归去看每一个子进程。如果从左边的管道读到了值,第一个值就是我们要输出的素数,然后创建新的管道和子进程,并且把满足条件的数送入管道中,否则就直接退出子进程。
  • 对于每一个父进程,都要用wait等待子孙进程全部退出后才能exit
  • 注意每一次对文件描述符操作后都要记得用close关上

4 find(moderate)

要求:编写一个简单版本的 UNIX 查找程序: 查找指定目录下指定名称的文件。解决方案放在 user/find.c 文件中。
 
一些提示:

  • 查看 user/ls.c 以了解如何读取目录。
  • 使用递归的方式以实现find访问到子目录
  • 不要循环递归“.”(当前目录)和“..”(父目录)
  • 对文件系统的更改在 qemu 运行期间保持不变; 要获得一个干净的文件系统,先运行 make clean,然后使用 make qemu
  • 关于C字符串的部分可以参考 《C程序设计语言》 例如第5.5节
  • 注意不能像 Python 那样用 == 比较字符串,而是使用 strcmp ()
     
    实现:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"

void
find(char *path,char *target)
{
    char buf[512], *p;//buf放路径名,p就是个中间指针,用来各种操作
    int fd;
    struct dirent de;
    struct stat st;

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

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

    if(st.type!=T_DIR){
        fprintf(2,"find: path is not a directory\n");
        close(fd);
        exit(1);
    }

    if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){
        //路径过长超出了缓冲区的长度
        fprintf(2,"path is too long\n");
        close(fd);
        exit(1);
    }

    strcpy(buf,path);//把path放到buf里
    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;

        memmove(p,de.name,DIRSIZ);//文件名复制给p
        p[DIRSIZ]=0;//设置文件名结束符
        if(stat(buf, &st) < 0){
            printf("find: cannot stat %s\n", buf);
            continue;
        }
        if(st.type==T_DIR){
            find(buf,target);
        }
        else if(st.type==T_FILE && !strcmp(de.name,target)){
            printf("%s\n",buf);
        }
    }
    close(fd);
}

int
main(int argc, char *argv[])
{
    if(argc==3)
    {
        find(argv[1],argv[2]);
    }
    else
    {
        fprintf(2,"you need three arguments\n");
        exit(1);
    }
    exit(0);
}

对以上程序的补充:

  • 整体设计思想参照ls的实现,在main函数中判断输入的参数是否满足条件,当输入参数不是三个的时候,报错并退出,否则调用find函数去找;
  • 在find函数中,首先写退出条件:
    如果open当前路径失败,直接退出;
    open成功后进入给定的路径,调用stat读取文件描述符的状态信息复制给结构体st,复制失败,直接退出;
    如果当前路径读到的(st.type)是文件属性,直接退出,因为我们的参数是在某个目录下找一个文件;
    路径大小超过设置的文件名缓冲区大小,直接退出(ls中就对这一点进行了判断);
    接着把绝对路径进行拷贝,循环获取路径下的文件名,与要查找的文件名进行比较,如果类型为文件且名称与要查找的文件名相同则输出路径,如果是目录类型则递归调用 find() 函数继续在这个目录下查找。
  • strcmp(const char* str1, const char* str2)函数,比较str1和str2,str1>str2返回大于0的数,=返回0,<返回小于0的数
  • fstat和stat都是用来获取相关文件状态信息的:
    int fstat(int fd, struct stat *st),将文件描述符fd所指文件的状态信息复制给第二个参数,成功就返回0,否则返回-1;
    int stat(const char *path, struct stat *st);与fstat的区别在于第一个参数的要求是文件全路径,而fstat需要的是open后得到的文件描述符fd,返回值的情况和fstat是一样的。

5 xargs(moderate)

要求:编写一个简单版本的 UNIX xargs 程序: 从标准输入中读取行,并为每一行运行一个命令,将该行作为参数提供给命令。解决方案应该在 user/xargs.c 文件中。
 
一些提示:

  • 使用 fork 和 exec 在每行输入中调用命令。在父进程中使用 wait 等待子级完成命令
  • 若要读取单个输入行,请一次读取一个字符,直到出现换行符(‘\n’),标准输入最后是有一个回车的
  • 如果你需要声明一个argv数组,kernel/param.h头文件中声明的MAXARG或许会有用
     
    对几个概念的区分:
  • 命令:我们在命令行里输入的那一整行东西整体叫做命令
  • 命令行参数:mkdir a b c命令中的a b c是mkdir所接收的参数,cd a命令中a是cd所接收的参数。
  • 标准化输入:我们输入一个命令后,等待我们继续输入的就是标准化输入,如grep a就是当我们的输入和a有匹配的时候,打印该输入。
  • 标准化输出:shell执行一个命令输出的东西
  • 管道符|:将符号前面命令的输入作为后面命令的命令行参数,如:
$ echo hello too | xargs echo bye
bye hello too

在这个命令中前一条命令打印出"hello too"来传给后一条命令,xargs在这里的作用是让它成为echo这条指令的命令行参数,于是最终的打印结果就是"bye hello too"如果不加xargs,那么"hello to"是要作为echo的标准化输入的。
 
实现:

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/param.h"
#include "stddef.h"
#define MAXSIZE 32

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

    char buf[MAXSIZE];
    read(0,buf,MAXSIZE);

    char *argvs[MAXARG];
    int ct=0;
    for(int i=1;i<argc;i++)
    {
        argvs[ct++]=argv[i];
    }

    char *p=buf;

    for(int i=0;i<MAXSIZE;i++)
    {
        if(buf[i]=='\n'){
            int pid=fork();

            if(pid==0){
                //首先拼接参数列表
                buf[i]=0;
                argvs[ct++]=p;

                exec(argvs[0],argvs);
                exit(0);
            }
            else if(pid>0){
                wait(NULL);
                p=&buf[i+1];//子进程完成了第一行的输出,开始执行下一行了
            }
        }
    }
    exit(0);
}

对以上程序的补充:

  • 根据实验目的,可以将xargs的实现分成三步:
    1.用read把前一条命令的结果读出来并保存
    2.把第二条命令的参数列表和前一条命令的结果合并
    3.用exec加载第二条命令
  • 加sleep(10)的原因是在测试find . b | xargs grep hello的时候,find操作比较慢,以致于xargs在执行的时候还没有得到find的标准输出,加上sleep后就可以通过了。

6 实验测试

注意要自己添加一个time.txt文件,记录你所花费的时间,不然最终的测评会在Cannot find time.txt那里扣一分

7 参考

樊潇的博客
b站up主阿苏EEer创建的笔记