前言
這個(gè)lab主要介紹了用戶態(tài)到內(nèi)核態(tài)的系統(tǒng)調(diào)用做了什么,并讓我們照貓畫(huà)虎完成了兩個(gè)系統(tǒng)調(diào)用的實(shí)現(xiàn)。
其他篇章
環(huán)境搭建
Lab1: Utilities
Lab2: System calls
Lab3: Page tables
Lab4: Traps
Lab5: Copy-on-Write Fork for xv6
參考鏈接
官網(wǎng)鏈接
xv6手冊(cè)鏈接,這個(gè)挺重要的,建議做lab之前最好讀一讀。
xv6手冊(cè)中文版,這是幾位先輩們的辛勤奉獻(xiàn)來(lái)的呀!再習(xí)慣英文文檔閱讀我還是更喜歡中文一點(diǎn),開(kāi)源無(wú)敵!
OSTEP,對(duì)OS不熟悉的同學(xué)做之前可以看一下這本經(jīng)典書(shū)籍,寫(xiě)得很好,也有中文版實(shí)體書(shū)。
個(gè)人代碼倉(cāng)庫(kù)
官方文檔
0. 前置準(zhǔn)備
很慚愧,以前github用得少,這一步折騰了老半天,我再說(shuō)一遍我個(gè)人的開(kāi)發(fā)流程——先在windows下git一個(gè)本地倉(cāng)庫(kù),然后用VS編輯,寫(xiě)完后git push
上去,在WSL的對(duì)應(yīng)地方git pull
下來(lái),然后編譯運(yùn)行。
前面環(huán)境配置中我為了連接到我個(gè)人的遠(yuǎn)程倉(cāng)庫(kù),是直接把原本的遠(yuǎn)程倉(cāng)庫(kù)刪了的,然后lab1做完做到lab2發(fā)現(xiàn)這個(gè)lab整體不是循序漸進(jìn)的,而是彼此分離的,每個(gè)實(shí)驗(yàn)需要選擇相應(yīng)的分支,因此就要重新弄一下:
git remote add base git://g.csail.mit.edu/xv6-labs-2022
git fetch base
git checkout syscall
git push --set-upstream origin syscall
當(dāng)然,別忘了加.gitignore
1. System call tracing (moderate)
1.1 簡(jiǎn)單分析
gdb教學(xué)我就不說(shuō)了,看看這個(gè)task。
先簡(jiǎn)單研究一下我們需求的這個(gè)trace是干什么的吧,trace顧名思義,tracing,追蹤、尋跡的意思,比如ray tracing,就是光線追蹤,這個(gè)命令接受一個(gè)傳參mask,內(nèi)涵是一個(gè)掩碼,每一位對(duì)應(yīng)一個(gè)系統(tǒng)調(diào)用的一個(gè)序號(hào),比如傳入32,代表 32 1<<SYS_read
,2147483647 代表追蹤所有syscall,具體的這些值定義在了kernel/syscall.h
里,我們待會(huì)也會(huì)寫(xiě)
初步了解之后,就寫(xiě)實(shí)現(xiàn)吧,這個(gè)task按照hint的步驟來(lái)很清晰:
1.2 Hint 1
Add $U/_trace to UPROGS in Makefile
首先添加makefile,司空見(jiàn)慣了。
1.3 Hint 2
Run make qemu and you will see that the compiler cannot compile user/trace.c, because the user-space stubs for the system call don’t exist yet: add a prototype for the system call to user/user.h, a stub to user/usys.pl, and a syscall number to kernel/syscall.h. The Makefile invokes the perl script user/usys.pl, which produces user/usys.S, the actual system call stubs, which use the RISC-V ecall instruction to transition to the kernel. Once you fix the compilation issues, run trace 32 grep hello README; it will fail because you haven’t implemented the system call in the kernel yet.
然后說(shuō)這個(gè)時(shí)候make,會(huì)找不到trace,我們要在用戶態(tài)user/user.h
里加上trace的聲明,根據(jù)原文 It should take one argument, an integer “mask”, whose bits specify which system calls to trace. 可知,這玩意應(yīng)該接受一個(gè)int
,然后返回也是一個(gè)int
(返回值其實(shí)不影響來(lái)著):
然后我們?cè)?code>user/usys.pl下添加這么一行,這是個(gè)Perl腳本,即使沒(méi)有用過(guò)Perl的同學(xué)應(yīng)該也能看出來(lái)這里的意思是聲明了一個(gè)trace系統(tǒng)調(diào)用的入口,再通過(guò)上文展開(kāi)為我們?cè)?code>usys.S中生成一段匯編代碼。
然后在內(nèi)核syscall.h
中給它注冊(cè)一個(gè)number
1.4 Hint 3
Add a sys_trace() function in kernel/sysproc.c that implements the new system call by remembering its argument in a new variable in the proc structure (see kernel/proc.h). The functions to retrieve system call arguments from user space are in kernel/syscall.c, and you can see examples of their use in kernel/sysproc.c.
然后模仿著添加原型?
這里簡(jiǎn)單解釋一下后面這個(gè)syscalls數(shù)組,可能很多人沒(méi)有看懂這,首先這是個(gè)static的不用說(shuō),然后這是個(gè)函數(shù)指針的數(shù)組(我一向很反感那些什么數(shù)組指針指針數(shù)組混著說(shuō)的,直接說(shuō)成裝指針的數(shù)組不就一目了然了嗎),函數(shù)返回值為uint64,參數(shù)為void,顯然是為上面extern的那些函數(shù)準(zhǔn)備的東西,這些都比較簡(jiǎn)單,后面的是個(gè)小feature了,它本身叫作指派初始化器(Designated Initializers),來(lái)自C99,意思就是給方括號(hào)里的那一位初始化為右邊的值
但是可以看到,C99的指派初始化器的形式是[N] = expr
的,中間需要一個(gè)等號(hào)連接,這里沒(méi)有,它是來(lái)自GCC私貨,原文出現(xiàn)在介紹指定初始化器的時(shí)候:An alternative syntax for this that has been obsolete since GCC 2.5 but GCC still accepts is to write ‘[index]’ before the element value, with no ‘=’. 意味著大家在自己使用時(shí)加個(gè)等號(hào)是更符合standard的寫(xiě)法。
然后叫我們仿照著kernel/sysproc.c
里的其他函數(shù)給trace寫(xiě)一個(gè)定義進(jìn)去:
uint64
sys_trace(void)
{
return 0;
}
使用argint
從寄存器取出用戶傳入的參數(shù):
int mask;
argint(0, &mask); // 保存用戶傳入的參數(shù)
然后我們要把接到的這個(gè)mask保存到進(jìn)程的元數(shù)據(jù)中,根據(jù)原文Add a sys_trace() function in kernel/sysproc.c that implements the new system call by remembering its argument in a new variable in the proc structure (see kernel/proc.h). T 我們?cè)?code>kernel/proc.h中可以找到一個(gè)結(jié)構(gòu)體struct proc
很顯然這個(gè)結(jié)構(gòu)體記錄著一些元數(shù)據(jù),我們?cè)谶@個(gè)基礎(chǔ)上再添加一條承載mask的:
int traceMask; // 用于接收trace的mask
顯然每一個(gè)進(jìn)程都有一個(gè)獨(dú)屬于自己的proc
對(duì)象,我們可以通過(guò)myproc()
來(lái)獲取這個(gè)對(duì)象的指針,就此我們可以完成我們的sys_trace
定義:
uint64
sys_trace(void)
{
argint(0, &myproc()->mask); // 嘗試從用戶空間讀取參數(shù)
return 0;
}
1.5 Hint 4
Modify fork() (see kernel/proc.c) to copy the trace mask from the parent to the child process.
我們知道fork出的子進(jìn)程會(huì)復(fù)制父進(jìn)程的內(nèi)存空間,根據(jù)hint我們可以找到它的實(shí)現(xiàn):
可以看到,這里明顯是要做一個(gè)p
到np
的拷貝,p
指向的是父進(jìn)程的proc
對(duì)象,np
則應(yīng)該是new proc
的縮寫(xiě)了:
我們這個(gè)mask的修改不需要持有鎖,因此只需要在alloc之后的合適時(shí)機(jī)將父進(jìn)程的值賦出即可:
既然提到了alloc,這里剛好就可以想到一個(gè)問(wèn)題——資源的分配與釋放呢?我們知道C語(yǔ)言訪問(wèn)未初始化變量的行為是UB,那么我們默認(rèn)狀態(tài)下的mask進(jìn)行初始化了嗎?在上面那張圖里我們可以清晰地看到(或者說(shuō)猜到)內(nèi)核依賴allocproc
分配內(nèi)存,依賴freeproc
釋放內(nèi)存,因此我們可以直接F12
進(jìn)去看一看實(shí)現(xiàn):
如圖,我們可以很容易地為mask
初始化以及釋放時(shí)賦0值。
1.6 Hint 5
Modify the syscall() function in kernel/syscall.c to print the trace output. You will need to add an array of syscall names to index into.
然后我們?yōu)?code>syscall這個(gè)總體的函數(shù)實(shí)現(xiàn)我們的功能,也就是前文中的那些打印:
我們分析一下需要做的事情:當(dāng)我們進(jìn)行了trace
調(diào)用時(shí),我們應(yīng)當(dāng)追蹤mask
標(biāo)記的所有調(diào)用,并打印出4: syscall close -> 0
這樣的內(nèi)容,不難看出,打印內(nèi)容分為三部分:PID、系統(tǒng)調(diào)用的名稱與系統(tǒng)調(diào)用的返回值,其中pid我們可以通過(guò)讀取proc
來(lái)獲取,返回值實(shí)際在框架中都告訴你了:
// and store its return value in p->trapframe->a0
p->trapframe->a0 = syscalls[num]();
可以看到,系統(tǒng)調(diào)用的返回值被保存在了寄存器a0中,至于系統(tǒng)調(diào)用的名稱呢?C語(yǔ)言中沒(méi)有反射,我們就只好提前建立一張syscall的名稱表,再根據(jù)mask
去尋址:
// 系統(tǒng)調(diào)用的名稱
static const char *syscallnames[] = {
[SYS_fork] "fork",
[SYS_exit] "exit",
[SYS_wait] "wait",
[SYS_pipe] "pipe",
[SYS_read] "read",
[SYS_kill] "kill",
[SYS_exec] "exec",
[SYS_fstat] "fstat",
[SYS_chdir] "chdir",
[SYS_dup] "dup",
[SYS_getpid] "getpid",
[SYS_sbrk] "sbrk",
[SYS_sleep] "sleep",
[SYS_uptime] "uptime",
[SYS_open] "open",
[SYS_write] "write",
[SYS_mknod] "mknod",
[SYS_unlink] "unlink",
[SYS_link] "link",
[SYS_mkdir] "mkdir",
[SYS_close] "close",
[SYS_trace] "trace",
};
搞清楚并完成了所有前置工作我們就可以開(kāi)始寫(xiě)邏輯了,最后的syscall
函數(shù)代碼,很簡(jiǎn)單:
void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
// Use num to lookup the system call function for num, call it,
// and store its return value in p->trapframe->a0
p->trapframe->a0 = syscalls[num]();
if ((p->mask >> num) & 1) { // 判斷系統(tǒng)調(diào)用是否被跟蹤
printf("%d: syscall %s -> %d\n",
p->pid, syscallnames[num], p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
1.7 測(cè)試
到這里就基本完成了,還是老規(guī)矩,我們make qemu
編譯,然后試一試文檔中的幾個(gè)命令:
trace 32 grep hello README
trace 2147483647 grep hello README
grep hello README
trace 2 usertests forkforkfork # 這一條輸入之后貌似要等一會(huì)才會(huì)出一大坨
…
最后跑一下總體批分./grade-lab-syscall trace
成功通過(guò)!
2. Sysinfo (moderate)
然后讓我們來(lái)完成一下task2,這個(gè)是也是添加一個(gè)系統(tǒng)調(diào)用,叫sysinfo
:
我們先搞清楚這個(gè)調(diào)用是干啥的,從介紹可以看到,這個(gè)sysinfo接收一個(gè)struct sysinfo
的指針,我們就是要寫(xiě)這個(gè)指針指向的對(duì)象,怎么寫(xiě)呢?就是將空閑的字節(jié)數(shù)存到對(duì)象里的freemem
字段,將state
不為UNUSED
的進(jìn)程數(shù)量寫(xiě)到nproc
字段。
有一個(gè)初步的印象后就可以去寫(xiě)實(shí)現(xiàn)了,整體思路和上文trace
的步驟差不多:
首先是增加$U/_sysinfotest\
到Makefile:
2.1 聲明
在user/user.h
里加聲明:
syscall.h中:
syscall.c中(第三個(gè)是上面的那個(gè)名稱表):
2.2 實(shí)現(xiàn)
2.2.1 框架
寫(xiě)完了聲明,就可以寫(xiě)實(shí)現(xiàn)了,實(shí)現(xiàn)我們依舊寫(xiě)在sysproc.c
下:
#include "sysinfo.h" // 由于要接收sysinfo類型的結(jié)構(gòu)體,我們先include一下
uint64
sys_sysinfo(void)
{
// TODO: 從用戶態(tài)到內(nèi)核態(tài)
// TODO: 計(jì)算空閑內(nèi)存的大小
// TODO: 計(jì)算內(nèi)存中非UNUSED的進(jìn)程的數(shù)量
// TODO: 從內(nèi)核態(tài)到用戶態(tài)
return 0;
}
關(guān)于具體實(shí)現(xiàn),文檔提供了三個(gè)hint,我們還是按照這三個(gè)hint的步驟去做就行了:
2.2.2 用戶態(tài)與內(nèi)核態(tài)交互
sysinfo needs to copy a struct sysinfo back to user space; see sys_fstat() (kernel/sysfile.c) and filestat() (kernel/file.c) for examples of how to do that using copyout().
首先依舊是獲取入?yún)?,hint給了我們兩個(gè)參考范例,我們可以看一看:
可以看(猜)到,這兩個(gè)文件以struct stat
類型為例子,分別向我們展示了獲取類型指針的方法以及將既存對(duì)象寫(xiě)入獲取到的指針的方法,分別使用argaddr
與copyout
函數(shù)實(shí)現(xiàn),因此我們可以依葫蘆畫(huà)瓢寫(xiě)出以下代碼:
uint64
sys_sysinfo(void)
{
uint64 addr; // 指向sysinfo結(jié)構(gòu)體的指針
struct sysinfo info;
argaddr(0, &addr); // 嘗試從用戶空間讀取參數(shù)
// TODO: 計(jì)算空閑內(nèi)存的大小
// TODO: 計(jì)算內(nèi)存中非UNUSED的進(jìn)程的數(shù)量
if (copyout(myproc()->pagetable, addr, (char *)&info, sizeof(info)) < 0) // 將內(nèi)核空間的sysinfo結(jié)構(gòu)體復(fù)制到用戶空間
return -1;
return 0;
}
2.2.3 計(jì)算空閑內(nèi)存的大小
To collect the amount of free memory, add a function to kernel/kalloc.c
hint提示我們想要計(jì)算空閑內(nèi)存的大小,需要在kalloc.c
下添加一個(gè)函數(shù),通過(guò)觀察該文件的內(nèi)容,我們不難發(fā)現(xiàn),這個(gè)文件主要負(fù)責(zé)維護(hù)一個(gè)名為kmem
的對(duì)象,這個(gè)名稱應(yīng)該是kernel memory
的縮寫(xiě),這個(gè)結(jié)構(gòu)體內(nèi)部有一個(gè)一看就是一把自旋鎖的lock
字段和一個(gè)一看就是負(fù)責(zé)記錄空閑page的鏈表的freelist
字段,通過(guò)綜合觀察我們可以知道freelist
確實(shí)維護(hù)的是空閑頁(yè)的數(shù)量,因此我們想要找到空閑內(nèi)存的總大小,只需要遍歷整個(gè)freelist
,就可以找到總共空閑頁(yè)的數(shù)量,而每個(gè)頁(yè)有PGSIZE
即4096個(gè)字節(jié),因此我們只需要將獲得的頁(yè)面數(shù)乘以PGSIZE
即可,于是不難寫(xiě)出以下代碼:
// 計(jì)算空閑內(nèi)存大小
uint64
kfree_mem_cnt(void)
{
struct run *r;
uint64 cnt = 0;
acquire(&kmem.lock); // 由于kmem.freelist是全局變量,所以需要加鎖
r = kmem.freelist;
while(r) {
cnt++;
r = r->next;
}
release(&kmem.lock);
return cnt * PGSIZE;
}
值得一提的是,由于kmem是一個(gè)全局變量,屬于臨界資源,因此我們?cè)谠L問(wèn)時(shí)需要加鎖。然后我們需要再kernel/defs.h
下添加這個(gè)函數(shù)的聲明,才能為我們所調(diào)用:
然后在我們的sysinfo
中調(diào)用它:
uint64
sys_sysinfo(void)
{
uint64 addr; // 指向sysinfo結(jié)構(gòu)體的指針
struct sysinfo info;
argaddr(0, &addr); // 嘗試從用戶空間讀取參數(shù)
info.freemem = kfree_mem_cnt(); // 獲取內(nèi)存中空閑的內(nèi)存大小
// TODO: 計(jì)算內(nèi)存中非UNUSED的進(jìn)程的數(shù)量
if (copyout(myproc()->pagetable, addr, (char *)&info, sizeof(info)) < 0) // 將內(nèi)核空間的sysinfo結(jié)構(gòu)體復(fù)制到用戶空間
return -1;
return 0;
}
2.2.4 計(jì)算非UNUSED進(jìn)程的數(shù)量
To collect the number of processes, add a function to kernel/proc.c
這個(gè)函數(shù)提醒我們寫(xiě)在kernel/proc.c
中,這個(gè)文件我們上一個(gè)task其實(shí)已經(jīng)接觸過(guò)了,再來(lái)看一看吧。前文我們已經(jīng)知道了每個(gè)進(jìn)程的信息依賴proc
結(jié)構(gòu)體維護(hù),進(jìn)一步閱讀不難發(fā)現(xiàn),這里用一個(gè)全局?jǐn)?shù)組來(lái)維護(hù)了我們的所有進(jìn)程,因此,我們只需要遍歷一遍這個(gè)數(shù)組,然后給其中非空閑的進(jìn)程計(jì)數(shù)即可。
同樣值得一提的是,我們翻閱struct proc
的定義可以發(fā)現(xiàn),注釋中提示了我們,state
屬于臨界資源,訪問(wèn)需要加鎖:
綜合上面的內(nèi)容,我們就可以比較輕松地寫(xiě)出如下代碼:
// 計(jì)算非空閑進(jìn)程的數(shù)量
uint64
get_free_proc_num(void)
{
uint64 num = 0;
for(struct proc* p = proc; p < &proc[NPROC]; p++){
acquire(&p->lock); // state是臨界資源,需要加鎖
if (p->state != UNUSED)
num++;
release(&p->lock);
}
return num;
}
我們同樣需要為它在defs.h
中添加聲明以供外部調(diào)用:
最后我們?cè)趕ysinfo的實(shí)現(xiàn)中調(diào)用這個(gè)函數(shù),完成了最終步驟:
uint64
sys_sysinfo(void)
{
uint64 addr; // 指向sysinfo結(jié)構(gòu)體的指針
struct sysinfo info;
argaddr(0, &addr); // 嘗試從用戶空間讀取參數(shù)
info.freemem = kfree_mem_cnt(); // 獲取內(nèi)存中空閑的內(nèi)存大小
info.nproc = get_free_proc_num(); // 獲取內(nèi)存中非UNUSED的進(jìn)程的數(shù)量
if (copyout(myproc()->pagetable, addr, (char *)&info, sizeof(info)) < 0) // 將內(nèi)核空間的sysinfo結(jié)構(gòu)體復(fù)制到用戶空間
return -1;
return 0;
}
2.3 測(cè)試
同樣的,make qemu
后,按照文檔中的提示運(yùn)行sysinfotest
,成功:
退出終端后運(yùn)行./grade-lab-syscall sysinfo
本地測(cè)試,成功:
3. 總測(cè)試
同樣的,我們需要在根目錄下創(chuàng)建一個(gè)time.txt
,里面寫(xiě)上本次lab用時(shí),比如我這個(gè)lab不算寫(xiě)博客花了差不多4個(gè)小時(shí),我就寫(xiě)個(gè)4,然后運(yùn)行make grade
(跑到這一步的時(shí)候我發(fā)現(xiàn)gdb也叫我填一個(gè)東西在answers-syscall.txt
里,答案是usertrap()
),弄好后又出了個(gè)錯(cuò)誤:
Timeout! trace children: FAIL (30.7s)
…
8: syscall fork -> -1
7: syscall fork -> -1
6: syscall fork -> -1
9: syscall fork -> -1
qemu-system-riscv64: terminating on signal 15 from pid 6958 (make)
MISSING ‘^ALL TESTS PASSED’
QEMU output saved to xv6.out.trace_children文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-636183.html
這個(gè)主要是由于WSL性能損失的原因,之前文檔也強(qiáng)調(diào)過(guò)這個(gè)問(wèn)題了,解決方法是自己手動(dòng)改測(cè)試腳本gradelib.py
,放寬時(shí)間(話說(shuō)上面單獨(dú)跑測(cè)試都25s過(guò)了,總的測(cè)試居然過(guò)不了,還得看運(yùn)氣呀):
然后再跑make grade
:
搞定!文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-636183.html
到了這里,關(guān)于6.s081/6.1810(Fall 2022)Lab2: System calls的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!