6.S081-Lab1 总结笔记(0基础向)

6.S081总结笔记

笔记目标:

  • 按照11个lab的顺序,整理所涉及到的知识
  • 对每个lab,给出清晰的解答
  • 整理自己在做实验过程中遇到的问题与思考
  • 对于课程中有涉及但是没有相应lab的内容做出补充(比如,后边的那些论文

笔记说明:

  • 针对的是20的课程,其他年份可能会有出入
  • 如果读到一些在本章课程、指导书中完全没提及的内容,那很有可能是引用了后续实验的知识,我会尽量标明,如果遇到看不懂的,可以先跳过
  • 因为自己刚经历了纯小白的苦恼,所以会努力写得让零基础的人也能看懂,但这也会导致笔记比较琐碎

1. Lab: Xv6 and Unix utilities

1.1 Boot xv6

根据提示一点点地来就好了,有报错就查报错。

我是在windows下用vmware装的Ubuntu 20.04,以下所有代码均在此环境中完成测试。

另:Ubuntu 22.04 版本不适用于20年的课程,在根据20年课程提示进行依赖安装时会出错。

1.2 sleep

1.2.1 理论知识

[1] Makefile

当编译含有多个文件的项目时,往往需要指定文件的编译顺序,Makefile文件就担当了这样的功能。

如果要编译多个文件,或者不同文件夹中的文件,需要生成不同的库文件,以及确定这些文件的编译先后顺序,往往所需要的命令行特别多,而且比较复杂,甚至对于以后项目的维护也比较麻烦。那么这个时候如果我们能够把所有的编译规则全部规范到文件中,然后通过解析该文件去执行对应的编译指令,这样就大大简化指令的复杂度,同时降低了编译程序过程中所带来的错误。我们的编译和处理规则就放在Makefile文件中,通过Makefile工具解析Makefile文件中的命令来指导整个工程的编译过程。

在实验中,很多时候需要往系统中添加一个新程序。显然,为了能让新程序能正常编译并运行,必须更新Makefile的内容,告诉它,“我要新增加一个程序”。Makefile的内容有很多,其中,UPROGS片段指的是用户空间的程序,按照提示,我们在这个片段底下加入自己需要的新程序的名字。

[2] sleep系统调用

这里暂时仅把它当作一个已被写好定义的函数,像平常调用函数一样调用它就好了。

1.2.2 实验步骤

[1] 获取参数
[1.1] 如何获取?

根据提示,通过观察已有的程序学习如何从命令行获取参数。阅读程序的完整代码,理解参数是如何获取及使用的。

  • echo.c

    for(i = 1; i < argc; i++){
        write(1, argv[i], strlen(argv[i]));
        /*因为第0个参数往往是程序名,从第1个参数开始,向文件描述符1所指向的文件写参数,
          文件描述符1是标准输出*/
        ......
      }
    
  • grep.c

    char *pattern = argv[1];
    

可以发现,命令行参数是通过数组argv[]传给main函数的。

int main(int argc, char *argv[]);
[1.2] 获取什么?

知道了如何获取参数,还必须知道需要获取什么参数。

根据user.h中对sleep()函数的定义:

int sleep(int);
[1.3] 最终代码

发现sleep函数只需要输入一个参数,于是,根据提示有:

// user/sleep.c(需新建sleep.c文件)
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h" // 一开始做实验,并不清楚需要什么头文件,可以模仿其他程序,尝试

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(2, "Usage: sleep ticks\n");
        exit(1); // 当参数个数不等于所要求的2时,模仿其他程序输出错误提示
    }
    sleep(atoi(argv[1])); // 使用atoi()函数将string参数转化为sleep()所需的int
    exit(0);
}
[2] 修改Makefile文件

根据1.1.1中的内容,容易推断出,我们需要在UPROGS下加入sleep函数,模仿得:

UPROGS=\
        ......
        $U/_zombie\
        $U/_sleep\

一个可能会用到的vim小技巧,查找目标字符串:在vim命令模式下,输入斜杠/+要查找的字符串string,如/UPROGS,按回车,即可跳转到目标字符串的位置。此时,按n为下一处位置,N为上一处位置。%UPROGS为自下而上查找,/UPROGS为自上而下查找。

[3] 编译、运行

xv6-labs-2020目录下输入make qemu

在这里插入图片描述

$后输入sleep n,为了让实验效果更加明显,可以让n稍微大一些:

我们可以看到,在输入sleep 20按下回车后,有一个明显的停顿,这便是sleep了20个ticks。

按住Ctrl + a + x,退出xv6,在xv6-labs-2020目录下输入make grade

6.S081-Lab1 总结笔记(0基础向)_第1张图片

可以看到通过了测试。

1.3 pingpong

1.3.1 理论知识

多看实验指导书。遇到不懂的,找到对应的章节,反复阅读。

[1] pipe

管道是一个小的内核缓冲区,作为一对文件描述符暴露给进程,一个用于读,一个用于写。将数据写入管道的一端就可以从管道的另一端读取数据。管道为进程提供了一种通信方式。

[2] fork

可以使用 fork 系统调用创建一个新的进程。fork 创建的新进程,称为子进程,其内存内容与调用的进程完全相同,原进程被称为父进程。

[3] read/write

read/write 系统调用可以从文件描述符指向的文件读写数据。调用 read(fd, buf, n)从文件描述符 fd 中读取不超过 n 个字节的数据,将它们复制到 buf 中,并返回读取的字节数。当没有更多的字节可读时,读返回零,表示文件的结束。write(fd, buf, n)表示将buf中的n个字节写入文件描述符fd中,并返回写入的字节数。若写入字节数小于 n 则该次写入发生错误。

1.3.2 实验步骤

阅读实验指导书,模仿学习pipe、fork、read/write的使用。

[1] 创建管道

由实验指导书的1.3可知,“程序调用 pipe,创建一个新的管道,并将读写文件描述符记录在数组 p 中” :

int p[2];
pipe(p);

从管道读数据是一次性操作,数据一旦被读取,它就从管道中被抛弃,释放空间以便写更多数据。管道只能采用半双工通信,即某一时刻只能单向传输。要实现父子进程双方互动通信,需要定义两个管道。

于是:

int p1[2], p2[2];
pipe(p1), pipe(p2);
[2] 创建子进程

由实验指导书1.1可知,通过fork创建子进程,通过返回值判断是父进程还是子进程:

int pid = fork();
if(pid > 0){
    //父进程代码;
}else if(pid == 0){
    //子进程代码;
}else{
    //fork出现错误。
}
[3] 实现父子进程之间的通信
[3.1] 对管道的操作

如果没有数据写入,读会无限阻塞,直到新数据不可能到达为止(写端被关闭)。

为避免被自己阻塞,读管道之前先将写端关闭:

close(p1[1]);
[3.2] read/write的调用

通过观察user/user.h中对read、write函数的定义以及实验指导书中的描述可知,参数二是一个指针,对于read、write函数来说,并不知道其指向的是什么类型的数据,也因此使函数获得了通用性。不论是int还是char还是什么类型,都只是按照参数要求,读/写n个字节的数据。

int write(int, const void*, int);
int read(int, void*, int);

调用时注意参数的一一对应:

read(0, buf, sizeof buf);
write(1, "pong\n", 5);
[3.3] 最终代码
// user/pingpong.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(int argc, char *argv[]) {
    int p1[2], p2[2];
    pipe(p1), pipe(p2);
    char buf[5]; // 用于保存从管道读取的信息
    int size;
    int pid = fork();
    if (pid == 0) {
        //读取父进程传过来的信息
        close(p1[1]); // 关闭管道1的写端
        if ((size = read(p1[0], buf, sizeof buf)) > 0) { // 从管道1读取不大于buf个字节的数据到buf
            printf("%d: received ", getpid());
            write(1, buf, size);
        } else {
            printf("%d: receive failed\n", getpid());
        }
        //向父进程写信息
        close(p2[0]); // 关闭管道2的读端
        write(p2[1], "pong\n", 5); // 向管道2写从“pong\n"开始的不大于5个字节的数据
        exit(0);
    } else if (pid > 0) {
        //向子进程写信息
        close(p1[0]);
        write(p1[1], "ping\n", 5);

        wait(0);
		//读取子进程传过来的信息
        close(p2[1]);
        if ((size = read(p2[0], buf, sizeof buf)) > 0) {
            printf("%d: received ", getpid());
            write(1, buf, size);
        } else {
            printf("%d: receive failed\n", getpid());
        }
    } else {
        printf("fork error\n");
    }
    exit(0);
}
[4] 编译、运行
  • Makefile文件中加入pingpong

    UPROGS=\
            ......
            $U/_sleep\
        	$U/_pingpong\
    
  • xv6-labs-2020目录下输入make qemu

  • 在命令行输入pingpong,得到结果:

6.S081-Lab1 总结笔记(0基础向)_第2张图片

  • 按住Ctrl + a + x,退出xv6,在xv6-labs-2020目录下输入make grade

    6.S081-Lab1 总结笔记(0基础向)_第3张图片

    通过测试。

1.4 primes

1.4.1 理论知识

这个实验所需的关于pipe和fork的知识在本文1.3中已有所涉及,理论知识部分不难,难点更多的是在理解题目意思与“通过管道实现并发”的模型上。请仔细阅读题目要求与并发模型的介绍。

1.4.2 实验步骤

[1] 理解管道并发模型并从中抽象出递归式

根据提示,容易发现每个进程的工作都是类似的(很重要,多理解几遍!):

p = get a number from left neighbor // 将从左边进程获得的第一个数字作为p
print p
loop: // 循环判断从左边进程获得的其余数字
    n = get a number from left neighbor 
    if (p does not divide n) // 若不能被p整除,则传向右边进程
        send n to right neighbor

即每个进程都是“读、判断、写”、“读、判断、写”,很自然地想到用递归的方式解决问题。

[2] 小心地处理文件描述符

受文件表的大小限制,整个系统的文件描述符是有限的,因此在递归过程中要及时关闭不需要的文件描述符,防止程序因为文件描述符不足而提早结束。当前进程只需要用到与父进程连接管道的读端和与子进程连接管道的写端,且用完后都需及时关闭。

另外,类似于1.3.2[3.1]的操作,为了让read能正常结束,需要解除管道写端的所有引用。

[3] 最终代码
// user/primes.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

#define SIZE 34

void recur(int p[2]) {
    int primes, nums;
    int p1[2];

    close(0); // 0的复用
    dup(p[0]);
    close(p[0]);
    close(p[1]);

    if (read(0, &primes, 4)) {
        printf("prime %d\n", primes); // 打印由父进程传来的第一个数字

        pipe(p1);
        if (fork() == 0) {
            recur(p1); // 由子进程筛选下一批质数
        } // 思考:考虑子进程已经在读、但是父进程还没写完的情况,子进程会等吗,还是报错呢?
        else {
            while (read(0, &nums, 4)) { // 从父进程读取数据
                if (nums % primes != 0) { // 筛查,将符合条件的数字传给子进程
                    write(p1[1], &nums, 4);
                }
            }
            close(p1[1]);
            close(0);
            wait(0);
        }
    } else {
        close(0); // 递归出口:若父进程无数据输入,则说明筛查完毕
    }
    exit(0);
}

int main() {
    int p[2];
    pipe(p);
    for (int i = 2; i < SIZE + 2; ++i) {
        write(p[1], &i, 4);
    }
    if (fork() == 0) {
        recur(p);
    } else {
        close(p[1]);
        wait(0);
    }
    exit(0);
}
[4] 编译、运行
  • Makefile文件中加入primes

    UPROGS=\
            ......
        	$U/_pingpong\
       		$U/_primes\
    
  • xv6-labs-2020目录下输入make qemu

  • 在命令行输入primes,得到结果:

    6.S081-Lab1 总结笔记(0基础向)_第4张图片

    可以看到,在打印出31之后,是有输出$符号的,说明程序正常结束了。

    另外,由于对文件描述符的优化做得比较到位,程序甚至可以实现比题目的“35以内的质数”的更高要求,筛选125以内的质数:

    6.S081-Lab1 总结笔记(0基础向)_第5张图片

  • 按住Ctrl + a + x,退出xv6,在xv6-labs-2020目录下输入make grade

    6.S081-Lab1 总结笔记(0基础向)_第6张图片

1.4.3 问题与思考

[1] 子进程的read与父进程的write是并发的,那怎么保证write在read之前完成呢?

疑问来自于指导书中对read的描述:“当没有更多的字节可读时,读返回零,表示文件的结束。"

试想,若write未在read之前完成,这个时候read会不会提前返回0,而错过正在write的剩余数据呢?

查看了read的源代码,发现这个担心是多余的。read会判断正在读取的文件的类型,若文件描述符指向的是pipe,read会等待write的完成。

while (pi->nread == pi->nwrite && pi->writeopen) {  //DOC: pipe-empty
    if (pr->killed) {
        release(&pi->lock);
        return -1;
    }
    sleep(&pi->nread, &pi->lock); //DOC: piperead-sleep
}

一个可能会用到的查找代码小技巧,如何在系统源代码中找到我们想要的函数代码:

  • 先判断是内核程序还是用户程序,如果是用户程序则去user/user.h中寻找,如果是内核程序则去kernel/defs.h中寻找
  • 找到函数声明所在的文件,再去对应文件中看具体的函数定义

比如,read是系统调用,属于内核代码,所以去kernel/defs.h中寻找,发现file.c、pipe.c文件下都有类似于read函数的声明,于是打开这些文件寻找我们想要的read代码。

不过有些函数并不会在头文件中声明,这个时候就需要发挥”想象力“去猜测它会出现在哪个文件中啦。根据代码的功能猜测它会在哪个文件中,比如关于进程的可能会在proc.c中,关于文件的可能会在file.c中等。

[2] 能正确输出2~35之间的质数,但是却不能正常结束primes程序

6.S081-Lab1 总结笔记(0基础向)_第7张图片

输出到"31"后程序无法结束

递归程序不能正常结束,首先查看递归出口处的条件是否不能满足。

if (read(0, &primes, 4)) {
    // 执行递归
} else {
    // 递归出口
}

read(0, &primes, 4) == 0是否永远不会成立呢?根据实验指导书,我们知道,“如果一个文件描述符仍然引用了管道的写端,那么read将永远看不到文件的关闭(被自己阻塞)”。所以,要让read能在没有数据可读时正确地返回0,我们需要关闭与管道写端相关联的所有文件描述符。

这里容易出现的一个问题是:仅仅close了当前进程中管道的写端,却忘记close父进程中的写端。要知道,父子进程共享的是同一个管道,子进程会复制父进程的打开文件表,当对子进程中的p[1]进行close时,并不会影响父进程的p[1],管道的写端仍然在被父进程的p[1]所引用。所以,如果要完全解除管道写端的引用,还需要close父进程的写端。

1.5 find

1.5.1 理论知识

[1] ls.c 源码分析

根据提示,仔细阅读user/ls.c代码,结合ls实际的使用过程,进行模仿。

一个可能会用到的小技巧,结合函数的实际使用分析源代码:比如,输入ls就会输出当前目录下的所有目录项,输入ls a就会输出当前目录中的a子目录下的所有目录项。将这个使用情景代入到源码的阅读当中,很多陌生的代码也会变得熟络起来。

另一个可能会用到的小技巧,如果对函数本身很陌生不会用怎么办:在linux命令行中输入man+func,比如man ls,就会跳出关于函数的详细说明。

void
ls(char *path)
{
  char buf[512], *p;
  int fd;
    /* 遇到陌生的结构体或函数,想办法找到源码,看看结构体里都有啥
    去哪儿找呢?看看程序开头都include了些什么头文件,去这些头文件里边找*/
  struct dirent de; // 在kernel/fs.h中定义:“目录项”,包含存储文件内容的inode号和文件名
    //这里有个疑问:如果某文件被打开两次,有两个文件描述符吗?那该文件de中的文件描述符项填充的是哪个呢?⁉️
  struct stat st; // 在kernel/stat.h中定义:存储文件的基本信息,如inode块号、文件类型、引用链接数、文件长度等
    
  if((fd = open(path, 0)) < 0){ // 打开path所指向的文件,返回对应打开文件的文件描述符
    fprintf(2, "ls: cannot open %s\n", path); // 若返回的文件描述符小于0,则意味着打开失败
    return;
  }

  if(fstat(fd, &st) < 0){ // 读取fd所指文件的信息
    fprintf(2, "ls: cannot stat %s\n", path);
    close(fd);
    return;
  }

  switch(st.type){ // 判断文件类型,即判断参数path所指向的文件的类型
  case T_FILE: // 若path指向的是“文件”,则报错,因为ls的功能是输出指定“目录”下的所有目录项
    printf("%s %d %d %l\n", fmtname(path), st.type, st.ino, st.size);
    // fmtname():输出文件本身的名字(删去路径中除文件外的其他部分)
    break;

  case T_DIR: // 若path指向的是“目录”,则进入下个流程
    if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){ // 若“path/FileName\n"(文件完整路径)的长度超过了缓存区的大小
      printf("ls: path too long\n"); // 则报错
      break;
    }
    strcpy(buf, path); // 复制path到buf
    p = buf+strlen(buf); // 指针p指向buf中已写path的末尾,准备续写文件路径
    *p++ = '/'; // 按照文件路径的输出规则,在目录后添加斜杠‘/’
    while(read(fd, &de, sizeof(de)) == sizeof(de)){ // fd是我们打开的目录,从目录中每次读取一个de,直到read读取失败为止,相当于是遍历了目录项
      if(de.inum == 0) // 若de.inum为0,则选择跳过不打印。结合ls的使用场景,什么情况下的目录项不打印呢?
          /* 在lab9 file system中有关于de.inum的进一步使用,可以发现de.inum是指保存文件内容的inode号
          若inode号为0,意味着文件没有保存,是无效文件*/
        continue;
      memmove(p, de.name, DIRSIZ); // 将从de.name开始的内容写DIRSIZ个长度到p指针所指位置
      p[DIRSIZ] = 0; // 在末尾写0,表示字符串的结束(在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); // 关闭打开的文件
}

1.5.2 实验步骤

[1] 分析findls的异同之处

find:find all the files in a directory tree with a specific name,找到指定目录中所有名为filename的文件

ls:打印出指定目录中的所有目录项

  • 都需要输入path参数
  • 都需要判断path参数所指向的文件类型
  • 都需要遍历目录项并读取目录项的名字

  • path参数外,find还需要filename参数,ls不需要
  • find需要递归遍历指定目录中的所有子目录,ls不需要
[2] 代码思路
find(path, filename){
    判断path类型
        若为文件,则报错退出
        若为目录{
        	遍历每个目录项{
                判断目录项类型
               		若目录项为文件,则判断是否为要找的filename
                	若目录项为目录,则递归find(path1,filename)
            }
    	}
}
[3] 最终代码
// user/find.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"

void
find(char *path, char *fileName) {
    char buf[128], *p;
    int fd, fd1;
    struct dirent de;
    struct stat st, st1;

    if ((fd = open(path, 0)) < 0) {
        fprintf(2, "path error\n");
        return;
    }

    if (fstat(fd, &st) < 0) {
        fprintf(2, "path stat failed\n");
        close(fd);
        return;
    }

    switch (st.type) {
        case T_FILE:
            fprintf(2, "path error\n");
            return; // 以上部分判断输入路径是否正确
        case T_DIR:
            strcpy(buf, path);
            p = buf + strlen(buf);
            *p++ = '/'; // 保存当前正在搜索目录的完整路径,作为模板输出,新内容都是固定附加在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); // 在模板后添加属于自己的内容:自己的文件名
                if ((fd1 = open(buf, 0)) >= 0) {
                    if (fstat(fd1, &st1) >= 0) {
                        switch (st1.type) {
                            case T_FILE:
                                if (!strcmp(de.name, fileName)) {
                                    printf("%s\n", buf); // 若文件名与目标文件名一致,则输出其完整路径
                                }
                                close(fd1); // 注意及时关闭不用的文件描述符
                                break;
                            case T_DIR:
                                close(fd1);
                                find(buf, fileName); // 若为目录,则递归查找子目录
                                break;
                            case T_DEVICE:
                                close(fd1);
                                break;
                        }
                    }
                }
            }
            break;
    }
    close(fd);
}

int
main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(2, "Usage:find path fileName\n");
        exit(0);
    }
    find(argv[1], argv[2]);
    exit(0);
}
[4] 编译、运行
  • Makefile文件中加入find

    UPROGS=\
            ......
       		$U/_primes\
       		$U/_find\
    
  • xv6-labs-2020目录下输入make qemu

  • 在命令行依次输入:

    $ echo > b
    $ mkdir a
    $ echo > a/b
    $ find . b
    

    得到结果:

    6.S081-Lab1 总结笔记(0基础向)_第8张图片

  • 按住Ctrl + a + x,退出xv6,在xv6-labs-2020目录下输入make grade

    6.S081-Lab1 总结笔记(0基础向)_第9张图片

    通过测试。

1.5.3 问题与思考

在写代码的过程中,有遇到过许多问题,比如只能在一级子目录中查找而无法继续递归查找下去等,在适当的位置输出一些中间信息或许可以很好地帮助我们找到问题:判断可能会出错地位置,打印出可以判断是否错误的信息。

1.6 xargs

1.6.1 理论知识

[1] exec

通过给定参数加载并执行一个文件。

exec 需要两个参数:包含可执行文件的文件名和一个字符串参数数组。

exec的具体使用示例可以查看实验指导书。

与1.4 primes实验一样,本实验的理论也不难,重点是在理解题目意思上:“read lines from the standard input and run a command for each line, supplying the line as arguments to the command”,从标准输入读取,然后将读取的内容作为命令参数执行。

我们的工作只要从“从标准输入读取内容”开始就可以了,至于这additional arguments是如何通过|操作符跑到标准输入去的,并不需要我们关心。

这么一来,整个实验的内容就变得清晰起来了:1.读;2.添加命令;3.执行命令。

1.6.2 实验步骤

[1] 从标准输入读取数据
char stdIn[512];
int size = read(0, stdIn, sizeof stdIn);
[2] 将数据分行存储在数组中

根据示例二:

$ echo "1\n2" | xargs -n 1 echo line
line 1
line 2

及提示:“To read individual lines of input, read a character at a time until a newline (‘\n’) appears.”,我们知道,标准输入中可能会有多行内容出现,且不同行的内容会作为不同的参数进行执行。因此,我们需要将读取的内容分行存储,以便下一步作为不同的命令参数执行:

int i = 0, j = 0;
int line = 0;

for (int k = 0; k < size; ++k) {
    if (stdIn[k] == '\n') { // 根据换行符的个数统计数据的行数
        ++line;
    }
}

char output[line][64]; 
for (int k = 0; k < size; ++k) {
    output[i][j++] = stdIn[k];
    if (stdIn[k] == '\n') {
        output[i][j - 1] = 0; // 用0覆盖掉换行符。C语言没有字符串类型,char类型的数组中,'0'表示字符串的结束
        ++i; // 继续保存下一行数据
        j = 0;
    }
}
[3] 将数据分行拼接到原命令后,然后分别运行

根据1.2.2[1.1]如何获取参数的内容可知,argv数组中保存着的是命令参数,其中argv[0]是命令本身,argv[1]往后是命令参数。注意参数不一定只有一个,有可能从argv[1]开始一直到argv[MAXARG]都是命令参数,所以不能把数据直接加在argv[1]后边。另外,由于这里多了一个xargs命令,因此所有参数都往后递增一个位置。

char *arguments[MAXARG];
for (j = 0; j < argc - 1; ++j) {
    arguments[j] = argv[1 + j]; // 从argv[1]开始,保存原本的命令+命令参数
}
i = 0;
while (i < line) {
    arguments[j] = output[i++]; // 将每一行数据都分别拼接在原命令参数后
    if (fork() == 0) { 
        exec(argv[1], arguments);
        exit(0);
    }
    wait(0);
}
[4] 最终代码
// user/xargs.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/param.h"

int main(int argc, char *argv[]) {
    //从标准输入读取数据
    char stdIn[512];
    int size = read(0, stdIn, sizeof stdIn);
    //将数据分行存储
    int i = 0, j = 0;
    int line = 0;
    for (int k = 0; k < size; ++k) {
        if (stdIn[k] == '\n') { // 根据换行符的个数统计数据的行数
            ++line;
        }
    }
    char output[line][64]; // 根据提示中的MAXARG,命令参数长度最长为32个字节
    for (int k = 0; k < size; ++k) {
        output[i][j++] = stdIn[k];
        if (stdIn[k] == '\n') {
            output[i][j - 1] = 0; // 用0覆盖掉换行符。C语言没有字符串类型,char类型的数组中,'0'表示字符串的结束
            ++i; // 继续保存下一行数据
            j = 0;
        }
    }
    //将数据分行拼接到argv[2]后,并运行
    char *arguments[MAXARG];
    for (j = 0; j < argc - 1; ++j) {
        arguments[j] = argv[1 + j]; // 从argv[1]开始,保存原本的命令+命令参数
    }
    i = 0;
    while (i < line) {
        arguments[j] = output[i++]; // 将每一行数据都分别拼接在原命令参数后
        if (fork() == 0) {
            exec(argv[1], arguments);
            exit(0);
        }
        wait(0);
    }
    exit(0);
}
[5] 编译、运行
  • Makefile文件中加入xargs

    UPROGS=\
            ......
       		$U/_find\
        	$U/_xargs\
    
  • xv6-labs-2020目录下输入make qemu

  • 在命令行依次输入:sh < xargstest.sh

    得到结果:

    6.S081-Lab1 总结笔记(0基础向)_第10张图片

  • 按住Ctrl + a + x,退出xv6,在xv6-labs-2020目录下输入make grade

    通过测试。

1.6.3 问题与思考

[1] exec的参数问题

int exec(char *file, char *argv[])

  • 第二个参数是指向char类型数组的指针,这里在定义变量的时候很容易搞错,我一开始定义了二维数组,误认为char **argumentschar *arguments[]是等价的,但最后发现不行。建议按照参数列表的格式进行定义:char *arguments[]
  • 从命令行读取的参数长度不一定是3,比如:xargs echo good morning就是长度为4的参数,所以在拼接参数时,不能直接在固定位置后拼接,需要根据具体的参数长度进行拼接:arguments[j] = output[i++];,“j“为计算出的原参数长度。

1.7 Lab1总结

1.7.1 Processes and memory

每个进程都有自己的父进程,操作系统的初始化进程相当于是第一个进程,是所有进程的祖先进程。当需要一个新进程执行命令时,调用fork函数创建一个子进程,子进程会复制父进程的内存、文件描述符表等,复制后的内容和父进程是相互独立的,更改内容时互相不会影响。子进程调用exit退出,但实际的子进程资源的释放,是在父进程的wait函数中进行的,这也是”每个进程都有自己的父进程“的原因。

exec函数会使用新的内存映像来替换进程的内存。先fork,再exec,因为父子进程是相互独立的,因此在子进程exec的内容不会影响到父进程,这为执行提供了更多的自由度。

1.7.2 I/O and File descriptors

文件描述符是一个小整数,代表了一个内核管理对象,这将文件、管道和设备之间的差异抽象化,隐藏了管理对象的细节,使操作者不需要了解底层的原理,只需要关心接口,从而能够更加高效地设计程序。这是实现操作系统”既能提供复杂功能又能实现简单接口“的重要一环。

I/O指输入输出,文件描述符使我们能够很方便地从各个对象读取、写入信息。每个进程单独维护一个以文件描述符为索引的表,因此不同进程的文件描述符n,可能指向不同的对象。如果两个文件描述符是通过一系列的 fork和dup调用从同一个原始文件描述符衍生出来的,那么这两个文件描述符共享一个偏移量。

1.7.3 Pipes

管道是一个小的内核缓存区,(相当于加了限定的共享内存),提供一对文件描述符作为读写的接口提供给进程,可以从读端读到从写端写入的数据。

注意,在读取数据前,需要先关闭管道的写端,否则会出现管道中没有数据导致读取无限等待的情况。

1.7.4 File system

lab1没有用到本节的内容。

讲述了文件名与inode之间的关系,等Lab9文件系统用到再细说。

你可能感兴趣的:(6.S081总结笔记,学习,unix,服务器,risc-v)