System call tracing
这个实验里我们要实现一个trace系统调用,要求是传入一个待追踪的系统调用的掩码,输出所有系统调用的名称和返回值。
首先我们回顾一下,当程序使用系统调用的时候操作系统会怎么做。执行系统调用的时候,操作系统要执行一个trap指令,也就是所谓trap to the kernel,把特权级别提升到kernel mode,这样才能执行特权级指令。系统调用返回的时候,也要执行一个从trap返回的指令(return-from-trap)指令,返回到发起调用的程序中,同时回到user mode。每个用户进程都有user stack和kernel stack。一般的用户代码都是在user stack里执行的,但是系统调用需要跳转到kernel stack里执行。下面这张表详细列出了trap to kernel的整个过程:
xv6操作手册里4.2节介绍了xv6这个操作系统trap to the kernel的全过程,感兴趣可以看一下。
了解了系统调用之后,来看实验给我们的提示。
提示1
提示1是要我们在makefile里加入trace这一项,这没什么好说的。
提示2
提示2说,在user/user.h里添加这个trace函数的原型,这个原型我们设计成
int trace(int trace_mask)
然后在kernel/syscall.h里加入trace的编号,然后在user/usys.pl里面加入这个函数的存根,这个usys.pl在make的时候会生成一个usys.S文件,这个文件就能用ecall执行trap to the kernel。我们来看一下这个usys.S文件,发现每个系统调用名字下面的汇编代码都是一样的。根据xv6手册第4.3节,用户代码是把系统调用编号存到寄存器a7里面然后执行ecall指令。那么显然只要仿照其他系统调用的格式加入trace就可以了:
.global trace
trace:
li a7, SYS_trace
ecall
ret
4.3节里说,ecall执行trap之后,会有一个syscall从a7里取出系统调用编号。看一下这个syscall:
函数从一个结构体里面取出了这个a7并存在num中,然后从uint64 (*)(void)类型的函数指针数组syscalls里取出第num个函数指针并调用它,将它的返回值存在寄存器a0里。也就是说,用户通过一段汇编代码向操作系统内核传系统调用的编号,内核根据编号来在kernel stack里执行对应的sys函数。
那么这个p是什么呢?看一下proc.h里面struct proc的定义就知道了。根据2.5节,struct proc是进程的状态表。也就是说,这个myproc()可以返回一个指向当前进程状态表的struct proc*类型的指针,通过这个指针可以读写当前进程的状态表。
提示3
提示3说,在sysproc.c里加入sys_trace()函数,在proc里新建一个变量,把传给trace()系统调用的参数存到这个变量里。syscall里有很多名为arg.*的函数,根据注释可以判断它们正式用来取出传给系统调用的参数的。我们选择argint(),这个函数可以"Fetch the nth 32-bit system call argument"。所以sys_trace()的思路就很明确了,就是在结构体里新加入一个trace_mask变量,用sys_trace()把用户传进来的参数存进去:
uint64 sys_trace(void)
{
//取出trace的参数
int trace_arg;
if(argint(0,&trace_arg)<0)
{
return -1;
}
//存到进程proc里面
struct proc *p = myproc();
//printf("trace_arg:%d\n",trace_arg);
p->trace_mask=trace_arg;
//printf("trace_mask:%d\n",p->trace_mask);
return 0;
}
为了执行函数,还要记得在syscall.c里把sys_trace加入函数指针数组syscalls,并添加它的extern外部链接。
提示4
提示4说,修改fork()函数,把trace_mask变量传给子进程。看一下proc.c里fork()的代码,新定义了一个struct* proc类型的指针np,还有一个指向当前进程表的指针p。发现这个函数主要就是把p里面的值想方设法赋给np,于是可以确定np代表的是子进程的状态表。因此只需要在进程中加入一行代码:
np->trace_mask=p->trace_mask;
提示5
现在可以修改syscall()函数来打印进程信息了,为了打印进程名,我把所有系统调用的名称按照syscall.h里面的顺序存进一个char* 类型的数组syscall_name中。然后将trace_mask这个掩码和1<<num做或运算(这是掩码的基本操作),不为0就说明当前这个系统调用是trace_mask里指定过的,需要打印。
void
syscall(void)
{
int num;
struct proc *p = myproc();
static int flag=0;
num = p->trapframe->a7;
//printf("%d ",num);
if(num > 0 && num < NELEM(syscalls) && syscalls[num])
{
p->trapframe->a0 = syscalls[num]();
if(num==SYS_trace)
{
//printf("%d",num);
flag=1;
}
if(flag)
{
if((1<<num)&(p->trace_mask))
{
//printf("num:%d p->trace_mask:%d &:%d ",num,p->trace_mask,num&(p->trace_mask));
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;
}
}
由于现在还不知道proc这个结构是怎么初始化的,在没有调用trace()之前trace_mask是未知的,严谨起见,可以增设一个静态变量flag=0,只在第一次调用trace()时初始化一次flag=1,当flag为1时才允许比对num和trace_mask。
Sysinfo
这个要求输出进程信息。用户把一个指向struct sysinfo的指针info传给sysinfo(),sysinfo()负责把它指向的内存空间填满。和上一题一样,也是按照提示步骤进行。
提示1,提示2和上一题一样,还是在user.h等文件中加入函数声明、系统调用号的宏等等。从提示3开始:
提示3
思路很明显,就是创建一个struct sysinfo,写入其中的两个参数,然后用copyout写入info。稍后我们来讨论怎么获取参数。注意要在文件开头#include “sysinfo.h”。
uint64 sys_sysinfo(void)
{
struct sysinfo info_tmp;
//准备存数据
//存进程的统计数据
info_tmp.nproc=collect_process_unused();
//存内存数据
info_tmp.freemem=free_mem_count();
uint64 user_info_ptr; // user pointer to struct sysinfo
if(argaddr(0, &user_info_ptr) < 0)//先取得用户传过来的指针
return -1;
//得到的结果写回
struct proc *p = myproc();
if(copyout(p->pagetable, user_info_ptr, (char *)&info_tmp, sizeof(info_tmp)) < 0)
return -1;
return 0;
}
根据提示观察sysfile.c里面的sys_fstat()函数和file.c里面的fstat()函数:sys_fstat抓取了传入的文件描述符fd和传入的struct stat类型指针st,并调用fstat();fstat()又声明了一个struct stat st,明显是用一个stati(f->ip, &st)把一些东西存到st里,然后用copyout做写操作。copyout参数1是页表,参数2是待写入指针,参数3是指向待写数据的指针(明显是要逐字节写入),参数4是写入大小。在trace()里模仿一下就行了。
提示4
现在实现上面代码里的free_mem_count()。根据手册,xv6分配从内核末尾到PHYSTOP的内存,一次分配一整页(4096B),使用一个linked list来管理空闲内存。下图是linked list的示例:
根据提示,我们来看kalloc.c。
这是一个很明显的链表结构,匿名结构kmem中的freelist指向linked list的开头,next指向下一个空闲页面。(这个struct run结构是直接存在空页面里面的)。因此遍历这个链表即可得到总空闲字节数:
uint64 free_mem_count(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
uint64 total_free_mem=0;
while(r)
{
total_free_mem+=PGSIZE;
r=r->next;
}
release(&kmem.lock);
return total_free_mem;
}
注意文档里提到了free list是被一个spin lock保护的,因此要仿照上面的kalloc()做一个PV操作,把遍历链表放在PV之间。另外要注意把free_mem_count函数的声明加入到defs.h中。
提示5
现在实现collect_process_unused()。struct proc里有一个enum类型的变量指示了进程状态。观察到proc.h里面的struct proc proc[NPROC],这明显是存储所有进程的数组。直接遍历:
uint64 collect_process_unused(void)
{
struct proc *p;
uint64 total=0;
for(p = proc; p < &proc[NPROC]; p++)
{
if(p->state != UNUSED)
{
total++;
}
}
return total;
}