MIT6.S081 Lab 2 Syscall

Lab 2 Syscall

System call tracing (moderate)

在本次作业中,你将添加一项系统调用跟踪功能,这可能对以后实验调试有所帮助。 创建一个新的trace系统调用来控制跟踪。 trace接受一个参数:一个整数掩码mask,mask的位指定要跟踪的系统调用。 例如,要跟踪 fork 系统调用,mask的值为1<<SYS_fork,其中 SYS_fork 是 kernel/syscall.h 中的系统调用编号。你必须修改 xv6 内核,以便在每个被追踪的系统调用在即将返回时打印出一行。 该行应包含进程 ID、系统调用名称和返回值;无需打印系统调用参数。 trace除了追踪调用trace的进程外还要追踪其fork的子进程。

提示:

  • 在 Makefile 的 UPROGS 中添加 $U/_trace

  • 运行 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() 函数,打印跟踪输出。 您将需要添加一个系统调用名称数组,以便对其进行索引。

源码阅读

重点关注argintargaddr 的使用方式

系统调用不通过函数的形参获取参数,usertrap时系统会将参数保存在寄存器中,系统调用通过argintargaddr 获取寄存器中参数

完成实验

思路

设计trace.c用来追踪,设计sys_trace系统调用,trace的结果由syscall返回时负责打印

user/trace.c :负责处理shell输入的命令,调用sys_trace,并exec程序

sys_trace() in kernel/sysproc.c :负责写入mask

syscall() in kernel/syscall.c :负责校验mask,打印

添加函数声明

user/user.h 中添加系统调用的原型:int trace(int);

user/usys.pl 中添加一个存根:entry("trace");

kernel/syscall.h 中添加系统调用编号:#define SYS_trace 22

kernel/call.c 中将函数添加至调用列表:

extern uint64 sys_trace(void);
[SYS_trace] sys_trace,

sys_trace

trace调用通过argint获取需要追踪的系统调用参数,将其记录到进程结构体的mask中

uint64
sys_trace(void)
{
	int n;
	if(argint(0, &n) < 0)
		return -1;
	myproc()->mask = n;	
	return 0;	
}

打印trace结果

	//fork()
	// copy mask from parent to child
	np->mask = p->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]();
    if(p->mask > 0){ // trace
			if(p->mask & (1 << num)) // trace all syscall 
				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;
  }
}

实验结果:

trace trace test

补充

一开始做实验没注意掩码的使用,syscall中使用了等于进行了判断系统调用编号if(1 << num == p->mask)。虽然似乎也能通过测试,但这样写有巨大的问题:如果需要trace多个调用,如:0011,使用== 无论是 0001 还 0010都无法正确识别,而是用&运算则可以识别

Sysinfo (moderate)

在本次作业中,你将添加一个系统调用 sysinfo,用于收集运行系统的信息。 系统调用需要一个参数:指向 struct sysinfo 的指针(参见 kernel/sysinfo.h)。

内核应填写该结构体的字段:freemem 字段应设置为可用内存的字节数,nproc 字段应设置为进程state不是 UNUSED 的进程总数

提供了一个测试程序 sysinfotest;如果打印出 "sysinfotest: OK”即为成功

提示:

  • 在 Makefile 的 UPROGS 中添加 $U/_sysinfotest

  • 运行 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 中添加一个函数

大致思路:sysinfo调用使用copyout取出内核才能访问的关键信息(内存、进程数)

源码阅读

int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)

功能:将src地址起的len字节数据传入虚拟地址dstva在页表中对应的物理地址

sys_fstat() (kernel/sysfile.c) , filestat() (kernel/file.c)

用户调用函数:user/user.h:int fstat(int fd, struct stat*);

触发系统调用:uint64 sys_fstat(void)

系统调用 sys_fstat 获取并检查参数、调用处理函数 int filestat(struct file *f, uint64 addr)

filestat 中使用copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0 将st传入p的页表中

完成实验

函数声明

user中添加sysinfo函数和结构体声明

user/usys.pl 中添加一个存根:entry("sysinfo");

kernel/syscall.h 中添加系统调用编号,``#define SYS_sysinfo 23`

kernel/syscall.c中添加数组

extern uint64 sys_sysinfo(void);

[SYS_sysinfo] sys_sysinfo,

sysinfo系统调用

uint64
sys_sysinfo(void)
{
  uint64 addr; // user space's struct sysinfo adress
  struct sysinfo info;
  struct proc *p = myproc();

  if(argaddr(0, &addr) < 0)
    return -1;

  info.freemem = count_free_memory();
  info.nproc = count_proc_not_UNUSED();

  if(copyout(p->pagetable, addr, (char*)&info, sizeof(info)) < 0)
    return -1;

	return 0;
}

辅助函数

uint64 count_free_memory(void){
  uint64 count = 0;
  struct run *r;
  r = kmem.freelist;
  while(r){
    count++;
    r = r->next;
  }
  return count * PGSIZE;
}
// count the number of proc's state is not UNUSED 
uint64 count_proc_not_UNUSED(void){
  uint64 count = 0;
  struct proc *p;
  
  for(p = proc; p < &proc[NPROC]; p++){
    if(p->state != UNUSED)
      count++;
  }

  return count;
}

实验结果:

sysinfotest
make grade

实验总结

不同于第一次实验的单纯使用系统调用,第二次实验开始深入内核,开始手写系统调用和相关的辅助函数。第一小题操作都在内核中完成,在熟悉了系统调用后,第二小题在第一题的基础上要求使用copyout将内核数据传入用户空间,所以这样来看实验的难度安排还是十分科学的。做完后对用户进行系统调用的细节有了比较清晰的认知。

总体来看这两个实验都不算很难,但依旧比较费事。花了点时间了解系统调用函数的声明方式,也花了很多时间在理解系统调用的参数获取上,理解了这点收获也是比较大的,在之后的实验中也可以快速使用。

一个比较简单的过程:

用户函数在用户空间调用系统调用函数,引发陷阱,进入陷阱时系统会将寄存器、函数参数等重要信息保存在陷阱帧(trapframe)中,随后进入内核,在内核中,系统调用函数访问内核数据,如果需要则通过copyout将内核数据传入用户空间

posted @ 2024-10-11 14:51  zySail  阅读(18)  评论(0)    收藏  举报