实验准备

课程主页上面给了安装工具链的方法,根据自己的系统来按步骤操作就可以了。我的运行环境是WSL中的Ubuntu 20.04.2 LTS,安装过程中没有遇到问题。安装好之后挑一个目录,运行

git clone git://g.csail.mit.edu/xv6-labs-2020

把代码clone到本地,然后启动qemu虚拟机

cd xv6-labs-2020
make qemu

就可以启动操作系统了。

Sleep.c

需要注意,这个实验是在给xv6操作系统编写程序而不是真实的linux,这个xv6操作系统能提供的系统调用和C语言库函数是很少的。一定要搞清楚能用的系统调用和库函数有哪些!你作为一个用户能够调用的函数都在user/user.h里面(xv6手册里也有)。还要注意的就是看每道题目的Hints!

现在开始写实验代码,第一个题目是实现一个Sleep()函数,这个还是比较简单的。只需要传一个整数当参数,然后调用sleep()系统调用就可以了。首先是传入参数。我们知道main()传参数是使用int argc,char* argv[]来进行的,argc是参数个数,argv里存储的是命令行输入的参数。直接写程序:

#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 time\n");
        exit(1);
    }
    int sleep_time=atoi(argv[1]);
    if(sleep_time==0)
    {
        fprintf(2, "sleep: args error\n");
        exit(1);
    }
    sleep(sleep_time);
    exit(0);
}

实验要求没有输入参数的时候输出一条错误信息,所以先检查argc个数,如果不是两个(包括Sleep自己和一个整数)就往stderror(也就是2号文件)输出信息并终止程序。

pingpong.c

实验要求是创建一对父子进程,用管道把一个字节从父进程传到子进程,再把这个字节传回父进程。

先总结一下管道的特点。

1.管道是一种进程间通信的机制,进程可以用read()和write()系统调用直接对管道进行读写。

2.管道提供无边界的字节流通信,写端write()发送一次数据可以多次调用read()读出,也可以多次发送数据之后用一次read()读出。注意,管道是不记录边界的,所以要设定好读写字节的数目。

3.管道是一种半双工的通信机制。如果两个进程之间只使用一个管道的话,需要提供可靠的同步机制,否则进程可能会读取自己不久前发出去的数据。因此,父子进程之间通信应该尽量使用两个管道,但是仍然需要考虑死锁的问题。

4.pipe()创建的是匿名管道,只能用于同祖先的进程之间相互通信。

5.读取管道时,关闭写端则读端read()调用返回0;关闭读端则写端进程收到SIGPIPE信号;若管道为空,且写端未关闭,则程序被阻塞。

知道了管道的特点之后来写代码。由于父子进程之间需要双向传递,因此使用两个管道pfd1和pfd2。解题思路很直白,父进程用pfd1向子进程里写入一个字节,子进程用read()等着读取pfd1,把它存在一个char*数组里,然后再用write()往pfd2里写这个字节。

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


int main()
{
    int pfd1[2];//parent写->child读
    int pfd2[2];//child写->parent读
    pipe(pfd1);
    pipe(pfd2);
    if(fork()!=0)
    {
        //parent
        int parent_pid=getpid();
        char parent_msg[50];
        close(pfd1[0]);
        close(pfd2[1]);
        //parent send a byte to child by pipe1
        char* ping="a";
        write(pfd1[1],ping,1);
        //close pipe write

        if(read(pfd2[0],parent_msg,1)!=0)
        {
            printf("%d: received pong\n",parent_pid);
        }
        exit(0);
    }
    else
    {
        //child
        int child_pid=getpid();
        char child_msg[50];
        close(pfd1[1]);
        close(pfd2[0]);
        //child receive byte

        if(read(pfd1[0],child_msg,1)!=0)
        {
            printf("%d: received ping\n",child_pid);
        }

        //close pipe read

        //child send a byte
        write(pfd2[1],child_msg,1);
        exit(0);
    }
}

需要注意的是:fork()创建的子进程会继承父进程的文件描述符表,也就是父子进程都可以访问管道的读端和写端。为了防止空管道阻塞程序,要关闭写进程的读端和读进程的写端。这操作要在fork()之后进行,否则会直接把父子进程的管道端口都关闭。

Primes.c

现在来看第三道题目,是需要我们用管道实现埃氏筛法找素数。点这里看埃氏筛法的原理 课程页面上也给出了算法:

p = get a number from left neighbor
print p
loop:
    n = get a number from left neighbor
    if (p does not divide n)
        send n to right neighbor

也就是说,我们需要创建一排进程,给最左边的进程注入数字,每个进程都筛一个素数出来,然后过滤掉一部分数字,把过滤得到的数字传给右边的进程,如此级联进行直到数字被过滤完。

程序思路就是按照算法复现。设计一个void sieve(int* pfd)函数递归创建子进程。函数接受管道数组pfd[2]作为参数。

1.父进程关掉pfd[1]写端,然后读第一个数,如果没读到数说明已经筛完了,直接终止进程(递归终止条件)。读到的数字就是p。

2.父进程创建通向下一级的管道pfd_new,fork()创建子进程,先关闭父进程的读描述符pfd[0]再调用sieve(pfd_new),准备接受上一级传来的数字。

3.因为父进程是相对下一级的写端,所以还要关闭通向下一级管道的读端pfd_new[0],执行loop里的算法,然后关闭上一级的读端pfd[0]和通往下一级的写端pfd_new[1],调用wait()等待子进程结束。

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



void sieve(int* pfd)
{
    //读端,关闭写
    close(pfd[1]);
    int min_prime;
    if(read(pfd[0],&min_prime,sizeof(int))==0)//如果未读入数据
    {
        close(pfd[0]);//关闭读端
        exit(0);
    }
    printf("prime %d\n",min_prime);
    int pfd_new[2];//创建通向下一级的管道
    pipe(pfd_new);

    if(fork()==0)
    {
        close(pfd[0]);//子进程也有这个描述符表
        sieve(pfd_new);
    }
    close(pfd_new[0]);//相对下一级的写端。关闭通向下一级读端
    int buffer;
    while(read(pfd[0],&buffer,sizeof(int)))
    {
        if((buffer%min_prime)!=0)
        {
            write(pfd_new[1],&buffer,sizeof(int));
        }
    }
    close(pfd[0]);//关闭上一级的读端
    close(pfd_new[1]);//关闭通向下一级的写端

    wait((int*)0);
    exit(0);


}


int main()
{
    int status;
    int pfd[2];
    pipe(pfd);

    if(fork()==0)
    {
        sieve(pfd);
    }
    close(pfd[0]);//写端,关闭读,务必要在fork后关闭
    for(int i=2;i<=35;i++)
    {
        write(pfd[1],&i,sizeof(int));
    }    
    close(pfd[1]);
    wait(&status);

    exit(0);


}

find.c

题目要求实现find函数。题目提示里面说,看user/ls.c,我们看一下这个ls.c的具体内容:

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);
    }

这就是实现ls的核心部分。ls函数首先用open()打开path,使用fstat()读取path的信息并放到stat类型的结构体st中。然后检查这个path的类型,如果是文件,就直接输出存在st中的path各项信息。如果是目录文件,则需要输出目录下的所有信息。首先在buf里拼出目录相对path的完整路径,然后读入目录。

根据xv6手册里关于目录的描述,"Its inode has type T_DIR and its data is a sequence of directory entries. Each entry is a struct dirent (kernel/fs.h:56), which contains a name and an inode number. The name is at most DIRSIZ (14) characters; if shorter, it is terminated by a NUL (0) byte. Directory entries with inode number zero are free."既然目录是一串dirent结构体,那么可以循环读入目录文件,每次读一个dirent结构体st。从st中得到文件名,和buf里的目录路径拼在一起得到新路径,然后用stat打开这个新路径并输出信息。fmtname()函数的作用是取出路径中最后一个斜杠里的文件名。

了解了ls的工作原理,我们来设计find。find有两个参数,第一个是目录名dir_name,第二个是文件名file_name。

find的基本思想是使用BFS算法,使用函数void search(char* dir_name, const char* file_name)递归搜索以输入路径为根节点的目录树。

首先确定递归的边界条件之一:第一个参数dir_name是一个文件名。使用fmtname(需要修改一下)处理文件名之后直接比对即可,然后返回函数。find遍历目录的方式和ls基本相同。遍历目录时,遇到.和..两个文件要跳过,遇到文件时就和file_name比对,如果相同就打印这个文件的相对路径。如果遇到了目录,就递归调用search函数。

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


const char* this_dir=".";
const char* parent_dir="..";


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 search(char* dir_name,const char* file_name)
{
char buf[512], *p;
int fd;
struct dirent de;
struct stat st;

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

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

if(st.type==T_FILE)//如果传入的是文件名(递归边界条件)
{
    if(!strcmp(fmtname(dir_name),file_name))
    {
        printf("%s\n",dir_name);//直接打印目录
    }
    return;
}
if(st.type==T_DIR)
{
    if(strlen(dir_name) + 1 + DIRSIZ + 1 > sizeof buf)
    {
    printf("find: path too long\n");
    return;
    }
    strcpy(buf, dir_name);
    p = buf+strlen(buf);//p是定位指针
    *p++ = '/';

    struct stat st_tmp;//遍历目录下的文件
    while(read(fd, &de, sizeof(de)) == sizeof(de))
    {
    if(de.inum == 0)
        continue;
    memmove(p, de.name, DIRSIZ);//把文件名复制到字符串buf的最后面
    p[DIRSIZ] = 0;//这里准备遍历文件名 准备好生成文件或者目录的相对路径,存在buf里面,把这串字符的后一位置为0来生成字符串

    if(stat(buf, &st_tmp) < 0)
    {
        //printf("ls: cannot stat %s\n", buf);
        continue;
    }
    if(st_tmp.type==T_FILE)//如果是普通文件
    {
        if(!strcmp(de.name,file_name))//找到文件
        {
        printf("%s\n",buf);//打印文件的相对路径
        }
    }
    if(st_tmp.type==T_DIR)//如果是目录
    {
        //递归搜索,使用BFS遍历directory tree
        //禁止遍历. .. 这两个目录
        if((!strcmp(de.name,this_dir))||(!strcmp(de.name,parent_dir)))
        continue;
        search(buf,file_name);//递归搜索

    }

    }
}
return;

}


int main(int argc, char *argv[])
{
if(argc==2)
{
    search(".",argv[2]);
}
else
{
    search(argv[1],argv[2]);
}

exit(0);
}

有一点很奇怪的是fmtname需要修改一下,因为

memset(buf+strlen(p), ' ', DIRSIZ-strlen(p))

是把buf里所有空余都填充为空格,如果不删这条,实验里的strcmp在比较fmtname(dir_name)和file_name会出现问题。

xargs.c

题目要求实现xargs指令,需要把前一条指令输出的结果传到后一条指令的参数列表里面。所以这个程序的核心其实就是处理前面指令的输出。

接下来以

find . b | xargs grep hello 

为例说明。

find要用exec执行grep,这需要使用exec()系统调用。xv6手册里提供的exec()原型如下:

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

真实世界里的exec是一个函数族,xv6提供的这个相当于是execv(),接受一个文件路径file和一个char*类型的指针数组argv,argv的每一个元素都指向一个字符串(也就是file的参数)。

首先分配一个指针数组argument,这就是稍后传exec的参数列表。先从argv[]里逐个读入grep的参数,然后使用malloc()分配一块能容下参数argv[i]的空间,把malloc()返回的指针存入argument[i]里,把argv[i]复制到这块内存中。

然后逐个字符读入从管道里流来的数据。shell的管道是把xargs的stdin文件重定向到了管道读端,所以可以用read直接从0号文件stdin里读取数据。设置一个读入缓冲buffer,用read逐个字符读取并存到buffer内,如果读到空字符就停止读取。这里使用一个char* p来指向read读入数据的位置。

如果遇到\n,说明读完了一行。把*p的内容换成'\0'来形成字符串,同样使用malloc()开辟空间并将这个字符串存到argument里面,并将p复位,清除buffer.另外根据xv6手册里面的例子,需要把argument所有参数的后面一项置为0。

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

int main(int argc,char* argv[])
{
    int status;
    if(argc==1)//如果xargs后面没参数
    {
        printf("Usage: xargs [OPTION]... COMMAND [INITIAL-ARGS]...\n");
        exit(0);
    }
    char** argument=(char**)malloc(sizeof(char*)*MAXARG);//稍后传exec的参数
    //从argv[1]开始是需要执行的程序和其参数。先将这部分参数传入argument中
    int i;
    for(i=0;i<argc-1;i++)
    {
        int len=strlen(argv[i+1])+1;
        argument[i]=(char*)malloc(sizeof(char)*len);//把argv[i+1]传到argument[i]

        strcpy(argument[i],argv[i+1]);
    }
    //现在传入管道流过来的数据。
    //然后存到argument中
    char buffer[60];//buffer是暂存空间
    char* p=buffer;//指示器,指示读入的地址
    read(0,p,1);//从stdin读入一个字符
    while (*p)//不断读入,直到遇到空字符位置
    {
        if(*p=='\n')//读到一行末尾
        {
            *p='\0';//附加上空字符从而构成字符串
            //存入argument中
            int len=strlen(buffer)+1;
            argument[i]=(char*)malloc(sizeof(char)*len);
            strcpy(argument[i],buffer);
            //指示器复位到暂存空间开头
            p=buffer;
            //暂存空间清0
            memset(buffer,0,60);
            i++;
            read(0,p,1);
            continue;
        }
        p++;//读入本行下一个字符
        read(0,p,1);

    }

    // for(int j=0;j<i;j++)
    // {
    //     printf("%s\n",argument[j]);
    // }
    // printf("%d\n",i);

    argument[i]=0;//argument最后一项应该置为0?

    if(fork()==0)
    {
        if(exec(argv[1],argument)==-1)
        {
            fprintf(2,"xargs: exec error\n");
            exit(1);
        }
    }


    //记得收回子进程
    wait(&status);
    exit(0);
}