Part1:sleep
實驗要求與提示
- 可以參考
user/echo.c
,user/grep.c
和user/rm.c
檔案 - 如果使用者忘記傳遞引數,
sleep
應該列印一條錯誤訊息 - 命令列引數傳遞時為字串,可以使用
atoi
函式將字串轉為數字 - 使用系統呼叫
sleep
,有關實現 sleep 系統呼叫的核心程式碼參考kernel/sysproc.c
(查詢sys_sleep
),關於可以從使用者程式呼叫的 sleep 的 C 定義,參閱user/user.h
,以及user/usys.S
表示從使用者跳轉到核心休眠的彙編程式碼 - 確保 main 呼叫
exit()
以退出程式 - 在
Makefile
中將 sleep 程式條件到UPROGS
中,這樣可以使得make qemu
能夠編譯程式,並在xv6 shell
中執行
遇到的問題
問題一
- 問題:執行
./grade-lab-util sleep
顯示錯誤/usr/bin/env: ‘python’: No such file or directory
,可能是沒裝 python2 或者裝的是 python3 - 解決:將
grade-lab-util
檔案第一行的!/usr/bin/env python
改為!/usr/bin/env python3
問題二
- 問題:
make qemu
後無法退出 - 解決:輸入
ctrl+a
後抬起按鍵,然後再輸入x
最終程式碼
#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 has not input parameters!\n"); //將錯誤資訊寫入到標準錯誤2
exit(1); // 非正常執行導致退出程式
}
sleep(atoi(argv[1])); // 使用sleep系統呼叫,使用atoi將輸入的字串轉為數字
exit(0); // 正常退出,注意這裡沒用return
}
注意要將 sleep 新增到 Makefile 的 UPROGS 中
- 可以使用
./grade-lab-util sleep
來進行打分,使用make grade
可以給整個實驗打分
實驗思考
- 實現
sleep
比較容易,但是要掌握sleep
、exit
、atoi
等的使用 - exit 和 return 的不同點:
-
exit(0)
:正常執行程式並退出程式 -
exit(1)
:非正常執行導致退出程式 -
return()
:返回函式,若在主函式中,則會退出函式並返回一值return
返回函式值,是關鍵字;exit
是一個函式return
是語言級別的,由 C 語言提供,它表示了呼叫堆疊的返回;而exit
是系統呼叫級別的,是由作業系統提供的(或者函式庫中給出的),它表示了一個程序的結束return
是函式的退出(返回);exit
是程序的退出return
用於結束一個函式的執行,將函式的執行資訊傳出個其他呼叫函式使用;exit
函式是退出應用程式,刪除程序使用的記憶體空間,並將應用程式的一個狀態返回給 OS,這個狀態標識了應用程式的一些執行資訊,這個資訊和作業系統有關,一般是 0 為正常退出,非 0 為非正常退出- 非主函式中呼叫
return
和exit
效果很明顯,但是在main
函式中呼叫return
和exit
的現象就很模糊,多數情況下現象都是一致的
Part2:pingpong
實驗要求與提示
- 呼叫一對管道(每個方向一個管道)在兩個程序間"ping-pong"傳遞一個位元組。父程序向子程序傳送一個位元組,子程序輸出
<pid>: received ping
,其中<pid>
是它的程序 ID,然後子程序將位元組寫入管道,隨後退出,父程序從子程序讀取位元組,列印<pid>: received pong
,隨後退出 - 使用
pipe
建立一個管道;使用fork
建立子程序;使用read
從管道中讀資料,使用write
將資料寫入到管道;使用getpid
查詢程序的 ID - xv6 上的使用者程式中可供使用的庫函式可以在
user/user.h
中檢視,它們的原始碼(除了用於系統呼叫)在user/ulib.c
、user/printf.c
和user/umalloc.c
中
遇到的問題
問題一
-
問題:VScode 中怎麼除錯使用者程式
-
配置:首先應該將
launch.json
中的"stopAtEntry":
改為true
-
除錯步驟:
- 點除錯按鍵開啟除錯,此時會停在
kernerl/main.c
的入口處 - 在除錯控制檯輸入
-exec file ./user/_filename
,filename
為需要除錯的檔名稱 - 在終端輸入
filename
,點選繼續開始除錯的按鍵,然後就可以進入檔案除錯了 - 如果需要對該檔案進行多次除錯,直接在終端重新輸入
filename
就行
注意:第一次除錯某檔案時,不要先設定斷點,有的地方設定斷點可能會導致進入不了該檔案,等第一次除錯之後再將斷點打在能變為紅色的地方
- 點除錯按鍵開啟除錯,此時會停在
問題二
- 問題:python 用多了,C 語言中關於字串、指標的用法就有點模糊了,程式錯誤都是因為這裡
- 解決:韋東山有個影片是關於指標的,然後再找一個陣列的影片或文件看一看
最終程式碼
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[]){
int p1[2];
int p2[2];
int pid;
char recv1[64];
char recv2[64];
pipe(p1);
pipe(p2);
pid = fork();
if(pid == 0){ // 子程序
close(p1[1]); // 關閉寫通道
read(p1[0], recv1, sizeof("ping")); // 等待父程序將資料寫入通道
printf("%d: received %s\n", getpid(), recv1);
close(p1[0]);
close(p2[0]);
write(p2[1], "pong", sizeof("pong"));
close(p2[1]);
exit(0);
}else{ // 父程序
close(p1[0]); // 關閉寫通道
write(p1[1], "ping", sizeof("ping")); // 寫入通道
close(p1[1]);
close(p2[1]);
read(p2[0], recv2, sizeof("pong"));
printf("%d: received %s\n", getpid(), recv2);
close(p2[1]);
}
exit(0);
}
實驗思考
- 關於通道讀寫的過程一定要知道在什麼情況下會發生什麼,在不使用讀端或者寫端的時候一定要關閉,不然可能會造成自己被自己阻塞的現象
- 這個實驗實現起來比較簡單,但是能深挖的邏輯關係有很多,之後需要再進行復習,理清之間的關係
Part3: primes
實驗要求與提示
- 使用 pipe 和 fork 來設定管道,首先將數字 2 到 35 輸入管道。對於每個素數將安排建立一個程序,該程序透過一個管道從其左側鄰居讀取資料,並透過另一個管道向其右側鄰居寫入資料。由於 xv6 的檔案描述符和程序數量有限,第一個程序可以在 35 時停止
- 要小心關閉程序不需要的檔案描述符,否則程式將在第一個程序達到 35 之前耗盡 xv6 的資源
- 一旦第一個程序達到 35,它應該等到整個管道終止,包括所有的子程序、孫子程序等等。因此,主質數程序應該只在所有輸出都列印出來之後退出,並且在所有其他質數程序都退出之後退出
- 當管道的寫端關閉時,
read
返回零 - 最簡單的方法是直接將 32 位(4 位元組)整數寫入管道,而不是使用格式化的 ASCII I/O
- 僅在需要時在管道中建立程序
最終程式碼
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#define WRITE 1
#define READ 0
void primeprocess(int p[]){
int first_num;
close(p[WRITE]);
if(read(p[READ], &first_num, sizeof(first_num)) == 0){ // 遞迴終止條件,讀不到資料
close(p[READ]);
exit(0);
}
printf("prime %d\n", first_num); // 第一個進入管道的肯定是素數
int p_child[2];
pipe(p_child); // 建立下一個pipe
int pid = fork();
if(pid == 0){ // 子程序
primeprocess(p_child); // 遞迴函式
}else{ // 父程序
int num;
close(p_child[READ]);
while(read(p[READ], &num, sizeof(num)) != 0){
if(num % first_num != 0){
write(p_child[WRITE], &num, sizeof(num));
}
}
close(p[READ]);
close(p_child[WRITE]);
wait(0); // 需要等待子程序退出才能退出
}
exit(0); // 子程序結束
}
int main(int argc, char *argv[]){
int p[2];
pipe(p);
int pid = fork();
if(pid == 0){ // 子程序
primeprocess(p);
}else{
close(p[READ]);
for(int i = 2; i < 36; i++){
write(p[WRITE], &i, sizeof(i)); // 注意這裡是將i的地址給write函式
}
close(p[WRITE]);
wait(0);
}
exit(0);
}
實驗思考
- 這道題關鍵在於理解問題所表達的意思,用遞迴的方法主要是因為父程序需等待子程序退出,不過遞迴的思路比較簡單
- 注意
write(p[WRITE], &i, sizeof(i))
中是傳遞的i
的地址
Part4: find
實驗要求與提示
- 檢視
user/ls.c
瞭解如何讀取目錄 - 使用遞迴查詢子目錄,但除去"."和".."
- 對檔案系統的更改在
qemu
執行期間持續存在;要獲得一個乾淨的檔案系統,請執行make clean
,然後執行qemu
- 需要使用 C 字串,注意比較字串不能像 python 一樣直接
==
,而是應該用strcmp()
等
遇到的問題
問題一
- 問題:不太熟悉 find 函式的使用,不知道它後面都帶能帶哪些函式
- 解決:這個實驗僅僅是實現了 find 函式的部分功能,它的語法為
find [路徑] [匹配條件] [動作]
,之後可以再嘗試實現它裡面更多的功能
最終程式碼
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
#include "kernel/fcntl.h"
char* fmtname(char *path)
{
char *p;
// 查詢末尾斜槓後的第一個字元
for(p=path+strlen(path); p >= path && *p != '/'; p--);
p++;
return p;
}
void find(char *path, char *target)
{
char buf[512], *p;
int fd;
struct dirent de; // 記錄檔案字首
struct stat st; // inode
if((fd = open(path, O_RDONLY)) < 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;
}
switch(st.type){
case T_FILE: // 檔案
if(strcmp(fmtname(path), target) == 0)
printf("%s\n", path);
break;
case T_DIR: // 目錄
if(strlen(path) + 1 + DIRSIZ + 1 > sizeof(buf)){
printf("ls: path too long\n");
break;
}
strcpy(buf, path); // 複製path到buf裡
p = buf+strlen(buf); // 將p指向buf的末尾
*p++ = '/'; // 將buf的末尾新增/,從a/b變為a/b/
while(read(fd, &de, sizeof(de)) == sizeof(de)){ // 依次讀取目錄裡面的檔案
// 這裡的判斷注意加上"."和".."的判斷,它們不進入遞迴
if(de.inum == 0 || strcmp(de.name, ".") == 0 || strcmp(de.name, "..") == 0)
continue;
memmove(p, de.name, DIRSIZ); // 合併檔案為a/b/de.name
p[DIRSIZ] = 0; // 結束字串
if(stat(buf, &st) < 0){
printf("find: cannot stat %s\n", buf);
continue;
}
find(buf, target); // 遞迴,從開始路徑一直往深處查詢檔案
}
break;
}
close(fd);
}
int main(int argc, char *argv[])
{
if(argc != 3){
printf("Usage: find <dirName> <fileName>\n");
exit(1);
}
find(argv[1], argv[2]);
exit(0);
}
實驗思考
read(fd, &de, sizeof(de))
是讀取檔案的方法,其中struct dirent de
用來記錄檔案字首,它的結構體如下:
struct dirent {
ushort inum;
char name[DIRSIZ];
};
- 這道題在
user/ls.c
的基礎上進行修改,但要注意在檔案判斷時,要排除"."
和".."
的情況,它們不能進入遞迴
Part5: xargs
實驗要求與提示
- 使用
fork
和exec
對每一行輸入呼叫命令。在父程序中使用wait
來等待子程序完成命令 - 要讀取單獨的輸入行,每次讀取一個字元,直到出現換行符
'\n'
。 kernel/param.h
宣告MAXARG
,如果需要宣告argv
陣列,這可能很有用。
最終程式碼
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
#include "kernel/fcntl.h"
#include "kernel/param.h"
#define MAXBUF 1024
int main(int argc, char *argv[]){
char *xargs_argv[MAXARG]; // 字串陣列
char buf[MAXBUF]; // 字元陣列
int i;
if(argc < 2){
printf("Usage: xargs <command>\n");
exit(1);
}
for(i = 0; i < argc; i++){
xargs_argv[i - 1] = argv[i]; // argv裡面為管道|後面的輸入,字串陣列
}
while(1){
int index = 0; // buf寫入位元組順序
int buf_index = 0; // buf遇到' '或'\n'的首地址
int xargs_index = argc - 1;
int re; // read返回值
char ch; // 讀到的一個位元組
while(1){
re = read(0, &ch, sizeof(ch)); // 讀取shell標準輸入的一個位元組
if(re == 0){
exit(0); // 表示沒有讀到位元組,結束程式(這裡是程式正常結束的唯一出口)
}
if(ch == ' ' || ch == '\n'){
buf[index++] = '\0';
xargs_argv[xargs_index++] = &buf[buf_index]; //將buf當前的字串傳給xargs_argv
buf_index = index; // 更新buf當前命令首地址
if(ch == '\n')break; // 跳出迴圈,執行一行命令
}else{
buf[index++] = ch;
}
}
xargs_argv[xargs_index] = (char *)0; // 結束一行命令
int pid = fork();
if(pid == 0){ // 子程式
exec(xargs_argv[0], xargs_argv);
}else{
wait((int *) 0); //等待子程式執行完畢
}
}
exit(0);
}
實驗思考
- 這道題主要是要理解
xargs
的用法以及靈活使用指標和陣列,其中字串陣列和字元陣列的用法要區分清楚 - 可以用
'\0'
來標記字串的結束 argv
的字串只包括了管道最後一個輸入,這裡是整個程式碼的關鍵