Part1:sleep 實驗要求與提示 可以參考 user/echo.c, user/grep.c 和 user/rm.c 文件 如果用戶忘記傳遞參數,sleep 應該列印一條錯誤消息 命令行參數傳遞時為字元串,可以使用 atoi 函數將字元串轉為數字 使用系統調用 sleep,有關實現 sleep ...
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
的字元串只包括了管道最後一個輸入,這裡是整個代碼的關鍵