源码地址
System call tracing (moderate)
实验目的
- 添加一个系统调用跟踪功能,该功能可以在以后的实验中为你提供帮助。
- 你将创建一个新的
trace
系统调用来控制跟踪。 - 它应该有一个参数,一个整数
mask(掩码)
,其指定要跟踪的系统调用。例如,为了跟踪fork
系统调用,程序调用trace(1<<SYS_fork)
,其中SYS_fork
是来自kernel/syscall.h
的系统调用号。 - 如果掩码中设置了系统调用的编号,则必须修改
xv6
内核以在每个系统调用即将返回时打印出一行。 - 该行应包含
进程ID
、系统调用名称
和返回值
;您不需要打印系统调用参数。trace
系统调用应该为调用它的进程和它随后派生的任何子进程启用跟踪,但不应影响其他进程。
实验要求及提示
- 将
$U/_trace
添加到Makefile
的\UPROGS
中 - 运行
make qemu
, 你将看到编译器无法编译user/trace.c
,因为系统调用的用户空间存根还不存在:将系统调用的原型添加到user/user.h
,将存根添加到user/usys.pl
,以及将系统调用号添加到kernel/syscall.h
中。Makefile
调用perl
脚本user/usys.pl
,它生成user/usys.S
,实际的系统调用存根,它使用RISC-V
ecall
指令转换到内核。修复编译问题后,运行trace 32 grep hello README
;它会失败,因为你还没有在内核中实现系统调用。 - 在
kernel/sysproc.c
中添加一个sys_trace()
函数,该函数通过在proc
结构中的新变量中记住其参数来实现新系统调用(请参阅kernel/proc.h
)。从用户空间检索系统调用参数的函数位于kernel/syscall.c
中,你可以在kernel/sysproc.c
中查看它们的使用示例。 - 修改
fork()
(参见kernel/proc.c
)以将跟踪的掩码从父进程复制到子进程。 - 修改
kernel/syscall.c
中的syscall()
函数以打印跟踪输出。你将需要添加要索引的系统调用名称数组。
实验样例
我们提供了一个跟踪用户级程序,该程序在启用跟踪的情况下运行另一个程序(请参阅 user/trace.c )。完成后,您应该看到如下输出:
$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
$
$ 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
$
$ grep hello README
$
$ trace 2 usertests forkforkfork
usertests starting
test forkforkfork: 407: syscall fork -> 408
408: syscall fork -> 409
409: syscall fork -> 410
410: syscall fork -> 411
409: syscall fork -> 412
410: syscall fork -> 413
409: syscall fork -> 414
411: syscall fork -> 415
...
$
实验步骤
首先按照提示在 kernel/syscall.h 中添加一个宏定义,定义一个系统调用的序号。
// kernel/syscall.h
#define SYS_trace 22
查看 user/trace.h 中的内容,Lab已经给出了用户态的trace
调用方式,所以我们可以在 user/user.h 中声明trace
系统调用。由Lab内容可知,trace
读入一个int类型,代表想要追踪的进程的代号,同时由 user/trace.h 可知需要返回一个int判断是否成功追踪。
// user/user.h
// system calls
...
int trace(int);
接下来按照提示查看 user/usys.pl ,这里perl语言会自动生成汇编语言usys.S
,是用户态系统调用接口,添加一个entry。
# user/usys.pl
...
entry("trace");
由课程视频后半段和Chapter2可以知道,汇编中会把系统调用号保存到a7
寄存器,然后执行ecall
。
.global trace
trace:
li a7, SYS_trace
ecall
ret
然后ecall
指令执行 kernel/syscall.c 中的syscall
函数。函数如下:
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]();
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
阅读代码可知,syscall
先将a7
寄存器中的系统调用号保存到num
变量中,如果满足条件就把syscall中的相应内容(函数指针)保存到a0
寄存器中去。
所以我们需要在syscalls
数组中添加函数指针,并且在文件开头给内核态的系统调用trace
加上声明:
// kernel/syscall.c
...
extern uint64 sys_trace(void);
static uint64 (*syscalls[])(void) = {
...
[SYS_trace] sys_trace,
};
同时我们需要给每一个进程增加一个变量mask
来指定追踪的系统调用。
// kernel/proc.h
struct proc {
...
int mask; // Mask for sys_trace
...
};
然后就可以在 kernel/sysproc.c 中编写sys_trace
的代码。
此处为何能直接从a0
寄存器中取出,个人理解是因为我们调用的trace
函数第一个且只有一个参数保存在a0
寄存器中,通过脚本生成的汇编文件默认放置在a0
未改变,所以此处可直接取用。
// kernel/sysproc.c
...
uint64
sys_trace(void)
{
int mask;
// 取出a0寄存器到mask中
if(argint(0, &mask) < 0)
return -1;
// 放入到相应进程中去
myproc()->mask = mask;
return 0;
}
接下来我们要实现相应的输出功能,由于源代码没有给出相应系统调用号对应的函数字符串,所以我们自己在代码中增加。我在 kernel/syscall.c 中添加了相应代码(0处为空因为没有为0的系统调用号):
// kernel/syscall.c
static char *syscall_names[] = {
"",
"fork",
"exit",
"wait",
"pipe",
"read",
"kill",
"exec",
"fstat",
"chdir",
"dup",
"getpid",
"sbrk",
"sleep",
"uptime",
"open",
"write",
"mknod",
"unlink",
"link",
"mkdir",
"close",
"trace",
};
按照提示修改fork
函数,在fork时同样复制结构体中的mask变量(基本任意添加)。
// kernel/proc.c
// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
...
np->mask = p->mask;
...
return pid;
}
然后在 kernel/syscall.c 中的syscall
函数中增加输出语句(需要用到mask)。
// kernel/syscall.c
...
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]();
// 下面为增加的语句
if ((p->mask) & (1<<num)) { // 在追踪集中
/* <pid>: syscall <syscall_name> -> <return_value> */
printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num], p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
最后记得在Makefile
中的UPROGS
添加编译即可:
UPROGS=\
...
$U/_trace\
实验结果
本地测试用例通过。
cheeselongan@cheeselongan-virtual-machine:~/Desktop/xv6-labs-2021$ ./grade-lab-syscall trace
== Test trace 32 grep == trace 32 grep: OK (0.9s)
== Test trace all grep == trace all grep: OK (0.9s)
== Test trace nothing == trace nothing: OK (1.0s)
== Test trace children == trace children: OK (11.2s)
Sysinfo (moderate)
实验要求
在本实验中,您将添加一个系统调用sysinfo
,它收集有关正在运行的系统信息。系统调用接受一个参数:一个指向struct sysinfo
的指针(参见 kernel/sysinfo.h )。内核应该填写这个结构体的字段:freemem
字段应该设置为空闲内存的字节数,nproc
字段应该设置为状态不是 UNUSED 的进程数。我们提供了一个测试程序sysinfotest
;如果它打印 “sysinfotest:OK” ,则实验结果通过测试。
实验提示
- 将
$U/_sysinfotest
添加到Makefile
的UPROGS
中。 - 运行
make qemu
, 你将看到编译器无法编译 user/sysinfotest.c 。添加系统调用sysinfo
,按照与之前实验相同的步骤。要在 user/user.h 中声明sysinfo()
的原型,您需要预先声明struct sysinfo
:struct sysinfo; int sysinfo(struct sysinfo *);
- 修复编译问题后,运行
sysinfotest
会失败,因为你还没有在内核中实现系统调用。 sysinfo
需要复制一个struct sysinfo
返回用户空间;有关如何使用copyout()
执行此操作的示例,请参阅sys_fstat()
( kernel/sysfile.c ) 和filestat()
( kernel/file.c )。- 要收集空闲内存量,请在 kernel/kalloc.c 中添加一个函数。
- 要收集进程数,请在 kernel/proc.c 中添加一个函数。
实验步骤
前面的步骤和上一个部分类似,首先定义一个系统调用号。
// kernel/syscall.h
#define SYS_sysinfo 23
在 user/user.h 中添加sysinfo
结构体以及sysinfo
函数声明。
// user/user.h
...
struct sysinfo;
// system calls
...
int sysinfo(struct sysinfo *);
在 kernel/syscall.c 中新增sys_sysinfo
函数的定义,同时在函数指针数组中新增sys_sysinfo
,syscall_names
新增一个sysinfo
:
// kernel/syscall.c
static char *syscall_names[] = {
...
"trace",
"sysinfo",
};
...
extern uint64 sys_sysinfo(void);
...
[SYS_sysinfo] sys_sysinfo,
完成以上步骤后开始编写函数实现。
kernel/proc.h 定义了和进程相关的结构体:
// kernel/proc.h
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
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// wait_lock must be held when using this:
struct proc *parent; // Parent process
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
int mask; // Mask for trace
};
其中有表示process state的state
变量是我们需要的,但是使用时要注意加锁。加锁操作可以看一些代码,使用已经编写好的acquire
函数。同时 kernel/proc.c 中前部代码可知进程限制最大为NPROC
。
// kernel/proc.c
uint64
cntproc(void)
{
struct proc* p;
uint64 tot = 0;
for (p = proc; p < &proc[NPROC]; ++p) {
acquire(&p->lock); // 加锁
if (p->state != UNUSED) {
++tot;
}
release(&p->lock); // 释放
}
return tot;
}
kernel/kalloc.c 定义了空闲内存的一些函数和结构体,其中有一个链表run
(使用时需要加锁),每个结点指向上一个可用的空间,这里的kmem.freelist
是链表的最后一个节点。
// kernel/kalloc.c
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
可以 kernel/kalloc.c 下面的代码大概理解一下具体的使用方法,这里简单说一个需要用到的点,可用空间按照PGSIZE byte
为单位初始化为1
,所以我们只要从kmem.freelist
向前跳直到null
为止。
// Count free memory
uint64
cntfreemem(void)
{
struct run* r;
uint64 tot = 0;
acquire(&kmem.lock);
r = kmem.freelist;
while (r) {
r = r->next;
++tot;
}
release(&kmem.lock);
return tot*PGSIZE;
}
然后在 kernel/defs.h 中添加新增函数声明。
// kernel/defs.h
// kalloc.c
...
uint64 cntfreemem(void);
// proc.c
...
uint64 cntproc(void);
接着按照提示查看 kernel/sysfile.c 的sys_fstat
函数,其中调用的filestat
函数调用的copyout
函数,其功能是从内核空间复制内存从内核地址src
开始的len
大小数据到用户进程pagetable
的一个虚拟地址dstva
中去。我们此处只需要知道copyout
的用法即可。
// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
n = PGSIZE - (dstva - va0);
if(n > len)
n = len;
memmove((void *)(pa0 + (dstva - va0)), src, n);
len -= n;
src += n;
dstva = va0 + PGSIZE;
}
return 0;
}
然后是 kernel/syscall.c 中的argaddr
函数,功能是从指定寄存器中取出内容放置到指定地址中。
// kernel/syscall.c
// Retrieve an argument as a pointer.
// Doesn't check for legality, since
// copyin/copyout will do that.
int
argaddr(int n, uint64 *ip)
{
*ip = argraw(n);
return 0;
}
下面编写sys_sysinfo
函数,还是从a0
读入我们需要保存到用户态数据sysinfo
的指针地址,然后调用上面编写的两个函数得到具体的空闲内存和总进程数,接着把这个结构体的内容复制到这个指针中去。
// kernel/sysproc.c
...
uint64
sys_sysinfo(void)
{
uint64 addr;
struct sysinfo info;
struct proc *p = myproc();
if (argaddr(0, &addr) < 0) {
return -1;
}
info.freemem = cntfreemem();
info.nproc = cntproc();
if (copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0) {
return -1;
}
return 0;
}
然后在用户态添加一个sysinfo
用户程序。
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/sysinfo.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
if(argc != 1){
fprintf(2, "Param fail", argv[0]);
exit(1);
}
struct sysinfo info;
sysinfo(&info);
printf("free space: %d\nused process: %d\n", info.freemem, info.nproc);
exit(0);
}
最后在Makefile
中的UPROGS
添加两个编译项sysinfotest
和sysinfo
,编译并测试。
实验结果
本地测试通过。
== Test sysinfotest == sysinfotest: OK (3.9s)