mit6.s081 lab1:Unix Utilities

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来测试你的程序
     

解决:

int main(int argc, char* argv[]){
    if(argc < 2){
        fprintf(2, "Usage: sleep ...\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中。
     
    解决:
int main(){
    int p[2];
    pipe(p);
    char s[10];
    int pid;
    pid = fork();
    if(pid > 0){
        //close(p[0]);
        //char* tmp = "ping";
        write(p[1], "ping", 5);
        wait(0);//如果不使用wait会导致程序无法正常结束
        read(p[0], s, 5);
        char* tmp = "pong";
        if(strcmp(tmp,s) == 0){
            printf("%d: received pong\n", getpid());
        }
    }else if(pid == 0){
        //close(p[1]);
        read(p[0], s, 5);
        int n = getpid();
        char* tmp = "ping";
        if(strcmp(s,tmp) == 0) {
            printf("%d: received ping\n", n);
            write(p[1], "pong", 5);
        }
    }else{
        exit(1);
    }
    exit(0);
}

对以上程序的说明:

  • 父进程中写-读-打印,子进程中读-打印-写
  • 好像没有严格按照要求来,因为感觉一个管道就够用了

3 primes(moderate)

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

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

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

 

实现:

void primes(int p[]){
    int i;
    read(p[0], &i, sizeof(int));
    printf("prime %d\n", i);
    close(p[1]);//不关管道写端,会导致死循环输出0
    if(i == 31) exit(0);//只能以这样的方式来避免管道的非阻塞读,因为xv6里好像没法设置管道非阻塞模式

    int pp[2];
    pipe(pp);
    int pid = fork();
    if(pid > 0){
        int digit;
        close(pp[0]);
        while(1){
            if(read(p[0], &digit, sizeof(int)) < 1) {
                break;
            }
            if(digit%i != 0) write(pp[1], &digit, sizeof(int));
        }
        close(p[0]);
        close(pp[1]);
        wait(0);
    }else if(pid == 0){
        primes(pp);
    }else{
        exit(1);
    }
    exit(0);
}

int main(){
    int p[2];
    pipe(p);
    int pid = fork();
    if(pid > 0){
        close(p[0]);
        int i = 2;
        printf("prime %d\n", i);
        int j;
        for(j = i+1; j<=35; j++){
            if(j%i != 0){
                write(p[1], &j, sizeof(int));
            }
        }
        close(p[1]);
        wait(0);
    }else if(pid == 0){
        primes(p);
    }else{
        fprintf(2, "process create error!\n");
        exit(1);
    }
    exit(0);
}

对以上程序的说明:

  • 设计思想不是很难,在main中首先创建一个管道,然后从父进程中把2-35传给子进程,从第一个子进程开始用函数primes递归去看每一个子进程。如果从左边的管道读到了值,第一个值就是我们要输出的素数,然后创建新的管道和子进程,并且把满足条件的数送入管道中,否则就直接退出子进程。
  • 对于每一个父进程,都要用wait等待子孙进程全部退出后才能exit
  • 注意在每个进程中,对于不需要使用的文件描述符要及时关上,否则会导致死循环的问题
  • 每个进程所拥有的管道读写文件描述符都是独立的, 并不是说父进程关闭写端子进程也对应关了

4 find(moderate)

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

  • 查看 user/ls.c 以了解如何读取目录。
  • 使用递归的方式以实现find访问到子目录
  • 不要循环递归“.”(当前目录)和“..”(父目录)
  • 对文件系统的更改在 qemu 运行期间保持不变; 要获得一个干净的文件系统,先运行 make clean,然后使用 make qemu
  • 关于C字符串的部分可以参考 《C程序设计语言》 例如第5.5节
  • 注意不能像 Python 那样用 == 比较字符串,而是使用 strcmp ()
     
    实现:
void find(char* path, char* target){
    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);
        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: %s is not a directory\n", path);
        close(fd);
        return;
    }
    if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){
        printf("find: path too long\n");
        close(fd);
        return;
    }
    //把path拷贝到buf中,在要查找的路径后面加上/
    strcpy(buf, path);
    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;
        }
        //把文件名拷贝给p,也等同于设置了新的路径
        memmove(p, de.name, DIRSIZ);
        //设置字符串结束位置
        p[DIRSIZ] = 0;
        if(stat(buf, &st) < 0){
            printf("find: cannot stat %s\n", buf);
            continue;
        }
        if(st.type == T_DIR){
            find(buf, target);
        }
        if(st.type == T_FILE && !strcmp(de.name, target)){
            printf("%s\n", buf);
        }
    }
    close(fd);
}

int main(int argc, char* argv[])
{
    if(argc != 3){
        fprintf(2, "Format like: find . b\n");
        exit(1);
    }else{
        find(argv[1], argv[2]);
    }
    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的标准化输入的。
 
实现:

int main(int argc, char* argv[])
{
    sleep(10);
    char buf[MAXARG];
    read(0, buf, MAXARG);
    //printf("%s\n", buf);
    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<MAXARG;i++){
        if(buf[i] == '\n'){
            //换行符设置为0,因为字符串是读到0结束的
            buf[i] = 0;
            int pid = fork();
            if(pid == 0){
                argvs[ct++] = p;
                exec(argvs[0], argvs);
                exit(0);
            }else if(pid > 0){
                wait(0);
                //让p指向换行符后面
                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创建的笔记

posted @ 2023-04-06 22:03  白日梦想家-c  阅读(95)  评论(0编辑  收藏  举报