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