1. 程式人生 > 其它 >MIT 6.S081 2021: Lab Utilities

MIT 6.S081 2021: Lab Utilities

實驗準備

課程主頁上面給了安裝工具鏈的方法,根據自己的系統來按步驟操作就可以了。我的執行環境是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);
}