开始日期:22.3.11
操作系统:Ubuntu20.0.4
Link:Lab Syscall
个人博客:Memory Dot
目录- Lab Syscall
- 写在前面
- 遇到的问题或bug
- 学到的知识
- 实验内容
- System call tracing (moderate)
- Sysinfo (moderate)
- 总结
- 写在前面
-
如何在虚拟机中调试(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
跟踪打印
- 第一个:系统调用编号对应的系统调用函数,听起来有点绕口,其实这条添加的内容是存放在函数指针表
-
-
任务:实现系统调用跟踪
-
功能:跟踪一个或多个系统调用进程,打印该进程的pid,名字和该进程返回值
return value
The 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/_trace
to 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.c
static 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; } }
- 寄存器
-
-
-
参考链接
- MIT-6.S081-2020实验(xv6-riscv64)二:syscall (这位的画示画得好)
- MIT 6.S081 2020 Lab2 system calls讲解
-
任务:实现一个系统函数
sysinfo()
-
功能:收集正在运行的系统信息,包括freemem(空闲内存的大小)和nproc(运行程序的数量:即程序状态不是
UNUSED
) -
按照提示写
-
Add
$U/_sysinfotest
to 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(1, &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; 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; 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; }
-
-
-
参考链接
- MIT 6.S081 2020 Lab2 system calls讲解
- 完成日期22.3.21
- 写
sysinfo
笔者一开始误把p < proc[NPROC]
写成了p < p[NPROC]
,找了好几个小时这个bug,是抄procdump()
的时候抄错了 = ^ = - 笔者一开始把系统函数和进程弄混了,误认为一个系统函数就是一个进程,事实上,一个进程一般都会调用多个系统函数
- 倒着组装空闲链表,需要不断地改变头指针(即
freelist
)的指向 - 艾尔登法环真好玩!