開始日期:22.3.11
操作系統:Ubuntu20.0.4
Link:Lab Syscall
個人博客:Memory Dot
github repository: duilec/MITS6.081-fall2021/tree/syscall
Lab Syscall
寫在前面
遇到的問題或bug
-
如何在虛擬機中調試(debug),需要打開兩個終端,過程中需要打開兩個終端,一個用來啟動qemu,一個用來正常debug。(課程建議debug時,啟動qemu只用一個cpu)
one windows $ make CPUS=1 qemu-gdb another windows $ gdb-multiarch kernel/kernel $ target remote localhost:26000 -
沒有切換分支到syscall lab,導致trace相關無法運行(因為只有切換到syscall分支之后才會有相關文件)
官網lab中其實已經提示了,在整個實驗開始前必須完全分支切換$ git fetch $ git checkout syscall $ make clean -
Timeout! trace children超時!-
超時bug如下所示,原因是電腦性能不夠強
== Test trace children == $ make qemu-gdb Timeout! trace children: FAIL (30.2s) ... 9: syscall fork -> 56 6: syscall fork -> -1 7: syscall fork -> -1 8: syscall fork -> -1 qemu-system-riscv64: terminating on signal 15 from pid 5581 (make) MISSING '^ALL TESTS PASSED' QEMU output saved to xv6.out.trace_children -
解決方案,在.py文件
gradelib.py中改變超時判斷時間30s為50s或更長時間(在428行)- 將
timeout=30改為timeout=50或更長時間
428 def run_qemu_kw(target_base="qemu", make_args=[], timeout=50): 429 return target_base, make_args, timeout 430 target_base, make_args, timeout = run_qemu_kw(**kw) - 將
-
學到的知識
-
spinlock vs mutex
- 前者(自旋鎖)會不斷嘗試直到成功,后者(互斥鎖)失敗一次就放棄,然后等待啟用;前者適用於等待時間短的,后者適用於等待時間長的。
- spin lock 和mutex,自旋鎖(spin lock)與互斥量(mutex)的比較
-
系統調用函數的添加流程(一開始可能讀不懂,可以先閱讀后面的實驗內容同時多看看材料)
-
user/user.h,添加用戶調用聲明(prototype)
-
user/usys.pl,添加存根(stub)到腳本文件
usys.pl-
可以看到其作用與壓棧相關(打印了匯編代碼)
/* usys.pl */ sub entry { my $name = shift; print ".global $name\n"; print "${name}:\n"; print " li a7, SYS_${name}\n"; print " ecall\n"; print " ret\n"; } -
以系統調用函數trace為例,事實上是要調用時把SYS_trace(trace的系統調用編號)壓入到寄存器a7當中,然后調用ecall進入kernel
/* usys.S */ li a7, SYS_trace ecall ret
-
-
kernel/syscall.h,添加系統調用編號(syscall number)
-
kernel/syscall.c,添加系統調用編號對應的系統調用函數,系統函數外部調用聲明以及系統調用編號對應的函數名字
- 第一個:系統調用編號對應的系統調用函數,聽起來有點繞口,其實這條添加的內容是存放在函數指針表
static uint64 (*syscalls[])(void)中的,該表的功能是:根據系統調用編號,找到並調用對應的函數 - 第二個:為了能讓
static uint64 (*syscalls[])(void)根據系統調用編號,找到並調用對應的函數,因為這些函數存放的位置都不統一,只能用外部調用的方式來聲明 - 第三個:系統調用編號對應的函數名字,就是調用函數的名字,用來給
syscall.c跟蹤打印
- 第一個:系統調用編號對應的系統調用函數,聽起來有點繞口,其實這條添加的內容是存放在函數指針表
-
實驗內容
System call tracing (moderate)
-
任務:實現系統調用跟蹤
-
功能:跟蹤一個或多個系統調用進程,打印該進程的pid,名字和該進程返回值
return valueThe line should contain the process id, the name of the system call and the return value;
eg.
3: syscall read -> 1023推出:
<pid>: syscall <syscall_name> -> <return_value> -
注意不同進程的返回值不同,需要注意的是fork()的返回值可以恰好是
pid -
按照提示(hints)一步步走即可。
-
Add
$U/_traceto UPROGS in Makefile -
即系統調用函數的添加流程,共四次添加,但提示只給到了三次添加,事實上,要結合最后一步才能更好的理解為什么要第四次添加,可以先不添加,看到最后一步再添加
-
trace.c已給出,它需要一個int參數 -
/* syscall.c */缺了系統調用編號對應的函數名字的添加,最后一步會提到/* user/user.h */ // syscall.h int fork(void); ... int trace(int); /* ADD */ /* user/usys.pl */ entry("fork"); ... entry("trace"); /* ADD */ /* kernel/syscall.h */ // System call numbers #define SYS_fork 1 ... #define SYS_trace 22 /* ADD */ /* kernel/syscall.c */ extern uint64 sys_chdir(void); ... extern uint64 sys_trace(void); /* ADD */ static uint64 (*syscalls[])(void) = { [SYS_fork] sys_fork, ... [SYS_trace] sys_trace, /* ADD */
-
-
在
kernel/sysproc.c中編寫sys_trace(),它的功能是獲得該程序的mask,編寫時主要參考kernel/proc.h,kernel/syscall.c,kernel/sysproc.c中的部分內容-
我們需要使用mask,mask是用來檢查當前系統函數和用戶所要跟蹤的系統函數mask是否對應,如果是,才打印跟蹤內容。eg.
trace 32 grep hello README,其中32是1 << SYS_read即1 << 5,需要跟蹤read -
如何使用mask來判斷是最后一步(修改
syscall())的內容,這里先不提 -
我們先要解決如何獲得mask的問題
-
首先,每個進程都有一個其相關信息的結構體,我們在這個結構體當中添加多一條mask
/* kernel/proc.h */ // Per-process state struct proc { struct spinlock lock; // these are private to the process, so p->lock need not be held. ... int mask; // mask for check and trace -
然后,編寫
sys_trace()獲得該程序的mask,這個mask其實是我們一開始給的參數(在trace.c中放入),它存放在寄存器a0當中,所以我們要調用0模式(argint(0, &n)),具體內容要看4.3和4.4 -
eg.
trace 32 grep hello README,其中32是1 << SYS_read即1 << 5,它就存放在a0當中。 -
當使用
myproc()->mask = n時,是對當前進程的mask賦值-
eg.
trace 32 grep hello README,該語句就是一個進程,包含了多個系統函數,但我們只跟蹤read()$ trace 32 grep hello README 3: syscall read -> 1023 3: syscall read -> 966 3: syscall read -> 70 3: syscall read -> 0 -
eg.
trace 2147483647 grep hello README,該語句就是一個進程,該進程包含了多個系統函數,同時,全部調用到的系統函數我們都跟蹤$ trace 2147483647 grep hello README 4: syscall trace -> 0 4: syscall exec -> 3 4: syscall open -> 3 4: syscall read -> 1023 4: syscall read -> 966 4: syscall read -> 70 4: syscall read -> 0 4: syscall close -> 0
-
-
參考代碼:
uint64 sys_trace(void) { int n; /* get mask by trap (mask in a0 of trapframe)*/ if(argint(0, &n) < 0) return -1; myproc()->mask = n; return 0; } -
需要理解的
argint()和argraw()以及user.c/trace.cstatic uint64 argraw(int n) { struct proc *p = myproc(); switch (n) { case 0: return p->trapframe->a0; case 1: return p->trapframe->a1; case 2: return p->trapframe->a2; case 3: return p->trapframe->a3; case 4: return p->trapframe->a4; case 5: return p->trapframe->a5; } panic("argraw"); return -1; } // Fetch the nth 32-bit system call argument. int argint(int n, int *ip) { *ip = argraw(n); return 0; }int main(int argc, char *argv[]) { int i; char *nargv[MAXARG]; if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){ fprintf(2, "Usage: %s mask command\n", argv[0]); exit(1); } if (trace(atoi(argv[1])) < 0) { fprintf(2, "%s: trace failed\n", argv[0]); exit(1); } for(i = 2; i < argc && i < MAXARG; i++){ nargv[i-2] = argv[i]; } exec(nargv[0], nargv); exit(0); }
-
-
-
為了通過
trace children,即子程序也能被跟蹤,需要把父程序的mask復制到子程序當中,很簡單,在fork()中添加語句即可。/* kernel/proc.c */ int fork(void) { ... // copy saved user registers. *(np->trapframe) = *(p->trapframe); // copy saved mask for trace. np->mask = p->mask; ... -
最后一步,我們修改
syscall()滿足功能:打印被跟蹤進程的pid,名字和該進程返回值return value-
首先,需要在
syscall.c中多添加trace的系統外部調用聲明以及trace的系統調用編號對應的系統函數調用,為了能夠跳轉並執行sys_trace(),獲得它的返回值/* kernel/syscall.c */ extern uint64 sys_chdir(void); ... extern uint64 sys_trace(void); static uint64 (*syscalls[])(void) = { [SYS_fork] sys_fork, ... [SYS_trace] sys_trace, }; -
其次,獲得調用系統函數名字,我們需要一個指針數組,以便
syscall()使用You will need to add an array of syscall names to index into.
// an array of syscall names to index into static char *syscall_name[] = { [SYS_fork] "fork", [SYS_exit] "exit", [SYS_wait] "wait", [SYS_pipe] "pipe", [SYS_read] "read", [SYS_kill] "kill", [SYS_exec] "exec", [SYS_fstat] "stat", [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", }; -
最后,修改
syscall()- 寄存器
a0本身被約定用來存放返回值(c-style),打印出來即可;寄存器a7是用來存放SYS_<syscall_name>的數值的
位移處理后,我們用來和當前進程的mask比較,從而檢查當前系統函數和用戶所要跟蹤的系統函數mask是否對應
用&操作,我們可以檢查一個或多個系統函數調用。 - eg.
trace 32 grep hello README,其中32是1 << SYS_read即1 << 5,需要跟蹤是系統函數read - 注意,要先判斷是不是系統函數,再判斷是不是需要跟蹤的系統函數
- 我們實際上是用syscall()來打印跟蹤內容,trace()只是用來傳遞mask的
參考代碼:
void syscall(void) { int num; struct proc *p = myproc(); num = p->trapframe->a7; if(num > 0 && num < NELEM(syscalls) && syscalls[num]) { p->trapframe->a0 = syscalls[num](); // check mask, if OK print if(p->mask & (1 << num)) printf("%d: syscall %s -> %d\n", p->pid, syscall_name[num], p->trapframe->a0); } else { printf("%d %s: unknown sys call %d\n", p->pid, p->name, num); p->trapframe->a0 = -1; } } - 寄存器
-
-
-
參考鏈接
Sysinfo (moderate)
-
任務:實現一個系統函數
sysinfo() -
功能:收集正在運行的系統信息,包括freemem(空閑內存的大小)和nproc(運行程序的數量:即程序狀態不是
UNUSED) -
按照提示寫
-
Add
$U/_sysinfotestto UPROGS in Makefile,注意添加的是$U/_sysinfotest, 不是$U/_sysinfo -
系統函數添加流程
-
這里要格外添加一個結構體sysinfo,它就是系統信息,包含了freemem和nproc
/* kernel/sysinfo.h */ struct sysinfo { uint64 freemem; // amount of free memory (bytes) uint64 nproc; // number of process }; -
添加
/* user/user.h */ struct stat; ... struct sysinfo; // syscall.h int fork(void); ... int sysinfo(struct sysinfo *); /* ADD */ /* user/usys.pl */ entry("fork"); ... entry("sysinfo"); /* ADD */ /* kernel/syscall.h */ // System call numbers #define SYS_fork 1 ... #define SYS_sysinfo 23 /* ADD */ /* kernel/syscall.c */ extern uint64 sys_chdir(void); ... extern uint64 sys_sysinfo(void); /* ADD */ static uint64 (*syscalls[])(void) = { [SYS_fork] sys_fork, ... [SYS_sysinfo] sys_sysinfo, /* ADD */ // an array of syscall names to index into static char *syscall_name[] = { [SYS_fork] "fork", ... [SYS_sysinfo] "sys_sysinfo", };
-
-
參考
filestat()(kernel/file.c),sys_fstat()(kernel/sysfile.c) 以及copyout(),編寫sys_sysinfo(),從內核中獲取freemem和nproc,輸出給用戶uint64 sys_sysinfo(void) { struct proc *p = myproc(); struct sysinfo info; uint64 addr; /* get VA(virtual address)*/ if(argaddr(0, &addr) < 0) return -1; /* get info*/ info.freemem = get_amount_freemem(); info.nproc = get_nproc(); /* Copy len bytes from src(info) to virtual address dstva(addr) in a given page table. */ if(copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0) return -1; return 0; } -
接下來,就是要編寫
get_amount_freemem()和get_nproc()從而獲得freemem和nproc-
注意寫完之后要在
defs.h中聲明/* kernel/defs.h */ // kalloc.c ... uint64 get_amount_freemem(void); // proc.c ... uint64 get_nproc(void);
-
-
get_amount_freemem()-
主要參考
kalloc.c,我們可以看到內核的內存是如何初始化,如何分配,從kfree()中可以看到kmem.freelist一直是指向空閑內存,它是一個空閑鏈表,而且是倒着組裝的,我們只要遍歷即可獲得空閑內存的總量(一個鏈表的結點就是一個頁表,一個頁表的字節數為PGSIZE即4096),但是它沒有初始化指向struct run { struct run *next; }; struct { struct spinlock lock; struct run *freelist; } kmem; void kinit() { initlock(&kmem.lock, "kmem"); freerange(end, (void*)PHYSTOP); } void freerange(void *pa_start, void *pa_end) { char *p; p = (char*)PGROUNDUP((uint64)pa_start); for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE) kfree(p); } // Free the page of physical memory pointed at by v, // which normally should have been returned by a // call to kalloc(). (The exception is when // initializing the allocator; see kinit above.) void kfree(void *pa) { struct run *r; if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP) panic("kfree"); // Fill with junk to catch dangling refs. memset(pa, 1, PGSIZE); r = (struct run*)pa; acquire(&kmem.lock); r->next = kmem.freelist; kmem.freelist = r; release(&kmem.lock); } // Allocate one 4096-byte page of physical memory. // Returns a pointer that the kernel can use. // Returns 0 if the memory cannot be allocated. void * kalloc(void) { struct run *r; acquire(&kmem.lock); r = kmem.freelist; if(r) kmem.freelist = r->next; release(&kmem.lock); if(r) memset((char*)r, 5, PGSIZE); // fill with junk return (void*)r; } -
參考
kalloc(void)來寫,這里沒有使用自旋鎖,因為沒有更改kmem.freelist的指向,只是簡單的查看。uint64 get_amount_freemem() { uint64 num_page = 0; struct run *page = kmem.freelist; while(page){ /* count for number of page */ num_page++; /* perare next page */ page = page->next; } return PGSIZE * num_page; }
-
-
get_nproc()-
這個編寫很簡單,多線程的
state訪問,需要用自旋鎖保護 -
參考
proc.c的內容enum procstate { UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE }; // Per-process state struct proc { struct spinlock lock; // p->lock must be held when using these: enum procstate state; // Process state ... -
統計狀態不是
UNUSED的線程即可uint64 get_nproc(void) { uint64 nproc = 0; struct proc *p; /* use spinlock to avoid race(accessing 'nproc') between different threads */ for(p = &proc[0]; p < &proc[NPROC]; p++){ acquire(&p->lock); if(p->state != UNUSED) nproc++; release(&p->lock); } return nproc; }
-
-
-
參考鏈接
總結
- 完成日期22.3.21
- 寫
sysinfo筆者一開始誤把p < proc[NPROC]寫成了p < p[NPROC],找了好幾個小時這個bug,是抄procdump()的時候抄錯了 = ^ = - 筆者一開始把系統函數和進程弄混了,誤認為一個系統函數就是一個進程,事實上,一個進程一般都會調用多個系統函數
- 倒着組裝空閑鏈表,需要不斷地改變頭指針(即
freelist)的指向 - 艾爾登法環真好玩!
- 修改了一些錯誤22.8.06
