MIT6.s081-2020 Lab2 System Calls

MIT6.s081-2020 Lab2 System Calls

lab 2文档

准备

git checkout syscall
make clean

System call trace

引言

在本任务中,您将添加一个系统调用跟踪功能,该功能可能会在以后调试实验时帮助您。您将创建一个新的trace将控制跟踪的系统调用。它应该采用一个参数,一个整数“掩码”,其位指定要跟踪的系统调用。例如,要跟踪 fork 系统调用,程序调用trace(1 << SYS_fork), 在哪里SYS_fork是来自的系统调用号kernel/syscall.h. 如果掩码中设置了系统调用的编号,则必须修改 xv6 内核以在每个系统调用即将返回时打印出一行。该行应包含进程id、系统调用的名称和返回值;您不需要打印系统调用参数。这trace系统调用应该启用对调用它的进程以及它随后派生的任何子进程的跟踪,但不应影响其他进程。

实现

这一部分是要求我们实现一个系统调用的跟踪,按照HINT一步步做下去即可。
HINT1:在Makefile的UPROGS中添加$U/_trace
打开目录可以发现已经提供了一个user/trace.c文件,先在Makefile里添加一下即可。
user/trace.c

if (trace(atoi(argv[1])) < 0) {
    fprintf(2, "%s: trace failed\n", argv[0]);
    exit(1);
}

可以看到trace.c里面调用了trace()方法(来自user.h),trace接收一个int参数,返回int,并且如果执行失败返回负值。
然后这个trace()函数的作用是,输入参数是掩码,因为int是32位最多能表示31个系统调用,被置为1的那个系统调用位就会被跟踪并打印信息。
HINT2:在user/user.h中添加系统调用的原型,在user/usys.pl中添加一个占位,并在kernel/syscall.h中添加一个系统调用号。
Makefile调用perl脚本user/usys.pl来生成user/usys.Susys.S也就是实际的系统调用代码(stub),它使用RISC-V的ecall指令来进入内核态。修复了编译问题后运行trace 32 grep hello README依然执行失败因为你还没有在内核中实现这个系统调用。
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";
}

ecall: xv6是基于RISC-V指令集的,在RISV-V指令集中ecall代表系统调用。

li: 是一个加载立即数到寄存器的指令,格式是li rd, immediate,表示x[rd] = immediate,将常量加载到 x[rd]中。

SYS_${name}的含义是在kernel/syscall.h中#define SYS_${name} xxx的系统调用号,这样就可以告诉ecall要调用几号系统调用。
user/user.h中添加函数原型。

int trace(int);

user/usys.pl中添加trace的占位。

entry("trace");

syscall.h里添加系统调用号。

#define SYS_close  21
#define SYS_trace  22 // NEW

HINT3:在kernel/syspro.c中添加sys_trace()函数,sys_trace()通过在proc struct的新变量中存储参数来实现这个新的系统调用。从用户空间检索系统调用参数的函数位于kernel/syscall.c中,可以在kernel/sysproc.c中看到这些函数的使用示例。
查看kernel/syscall.c中函数可以发现:
syscall.c中的argint()函数的实现如下,trapframe是的用户进程陷入(trap)内核之前的寄存器等上下文信息。

// Fetch the nth 32-bit system call argument.
int argint(int n, int *ip) {
  *ip = argraw(n);
  return 0;
}

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;
}

因此需要在proc结构体(在kernel/proc.h)里面添加一个新变量用来存储这个跟踪号,然后在fork()的时候把结构体里新增加的变量也给复制过去,这样就达到了传参的目的。我们新的变量是不需要加锁的,因为只会被自己所在的进程使用,所以放在char name[16]之后
修改kernel/proc.h

// Per-process state
struct proc {
  // ...
  // these are private to the process, so p->lock need not be held.
  // ,,
  char name[16];               // Process name (debugging)
  int trace_mask;              // trace mask for debugging
};

打开kernel/sysproc.c
该文件的其他系统调用函数的参数都是void,因为获取参数使用的是argcint()方法,argint()用于读取在a0-a5寄存器中中传递的系统调用参数。
结合这个文件里其他函数我们能看出myproc()函数可以获取当前进程的struct proc, 也就是PCB。添加sys_trace(void)函数。

// add mask to proc's trace_mask
uint64 sys_trace(void) {
  int n;
  if(argint(0, &n) < 0) {
    return -1;
  }
  myproc()->trace_mask = n;
  return 0;
}

HINT4:修改fork()来实现“从父进程复制trace mask到子进程”的功能,fork位于kernel/proc.c
因此需要在kernel/proc.c的fork()函数里加一句np->trace_mask = p -> trace_mask;即可。
HINT5:修改kernel/syscall.c中的syscall()函数来把trace打印出来。你需要添加一个以系统调用号为索引的存储系统调用名字的数组。
kernel/syscall.c里实现我们的打印操作,文档里说打印出的一行应当包括:进程id,系统调用的名字以及系统调用的返回值,不需要打印系统的调用的参数。
文档里让我们自己搞一个可以用系统调用号索引系统调用名称的数组,参考static uint64 (*syscalls[])(void)的写法,我们写个这样的东西出来。
extern表明这个函数来自外部文件。

extern uint64 sys_trace(void);

static uint64 (*syscalls[])(void) = {
// ...
  [SYS_close]   sys_close,
  [SYS_trace]   sys_trace, // NEW
};

static char* syscall_names[] = {
  [SYS_fork]    "fork",
  [SYS_exit]    "exit",
  [SYS_wait]    "wait",
  [SYS_pipe]    "pipe",
  [SYS_read]    "read",
  [SYS_kill]    "kill",
  [SYS_exec]    "exec",
  [SYS_fstat]   "fstat",
  [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",
  [SYS_sysinfo] "info",
};

void syscall(void)
{
  int num;
  struct proc *p = myproc();
  char* syscall_name;       // NEW

  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->trapframe->a0 = syscalls[num]();
    if ((p->trace_mask & (1 << num)) != 0) {                                   // NEW
        syscall_name = syscall_names[num];                                     // NEW
        printf("%d: syscall %s -> %d", p->pid, syscall_name, p->trapframe->a0); // NEW
    }                                                                          // NEW
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

结果

实验测试:
在这里插入图片描述

Sysinfo

引言

在本次任务中,您将添加一个系统调用,sysinfo,收集有关正在运行的系统的信息。系统调用有一个参数:一个指向struct sysinfo (看kernel/sysinfo.h)。内核应该填写这个结构的字段:freemem字段应设置为可用内存的字节数,并且nproc 字段应设置为其进程的数量state 不是UNUSED. 我们提供测试程序sysinfotest; 如果它打印“sysinfotest:OK”,你就通过了这个任务。

实现

因此本部分内容也可以按上述trace流程一样,按照HINT一步步做下去即可。
HINT1:添加$U/_sysinfotest到 Makefile 中的 UPROGS
HINT2:运行make qemuuser/sysinfotest.c将无法编译。添加系统调用 sysinfo,步骤与之前的分配相同。声明 sysinfo() 的原型在user/user.h你需要预先声明存在struct sysinfo

    struct sysinfo;
    int sysinfo(struct sysinfo *);

修改user/user.h:需要在user.h里再声明一次struct sysinfo的原因是:在参数列表里的参数是看不见本文件外面定义的struct的。

struct stat;
struct sysinfo;
struct rtcdate;

// system calls
// ...
int sysinfo(struct sysinfo *);

usys.pl

entry("sysinfo");

syscall.h

#define SYS_sysinfo 23

syscall.c

extern uint64 sys_info(void);
static uint64 (*syscalls[])(void) = {
// ...
[SYS_sysinfo] sys_info,
};

static char* syscall_names[] = {
// ...
  [SYS_sysinfo] "sys_info",
};

修复编译问题后,运行 sysinfotest;它会失败,因为你还没有在内核中实现系统调用。
HINT3:sysinfo 需要复制一个struct sysinfo返回用户空间;看sys_fstat()(kernel/sysfile.c) 和filestat()(kernel/file.c) 有关如何使用的示例copyout().
按照HINT,阅读sys_fstat()函数和filestat()函数。

// **kernel/sysfile.c**
uint64
sys_fstat(void)
{
  struct file *f;
  uint64 st; // user pointer to struct stat

  if(argfd(0, 0, &f) < 0 || argaddr(1, &st) < 0)
    return -1;
  return filestat(f, st);
}
// **kernel/file.c**
// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
  struct proc *p = myproc();
  struct stat st;
  
  if(f->type == FD_INODE || f->type == FD_DEVICE){
    ilock(f->ip);
    stati(f->ip, &st);
    iunlock(f->ip);
    if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
      return -1;
    return 0;
  }
  return -1;
}

由上述可以发现,当进行内核态数据到用户态数据的拷贝时,将会用到copyout其中参数包含:线程的页表,拷贝对象的目的地址,拷贝对象地址以及对象所占用的空间大小。
HINT4:要收集可用内存量,请添加一个函数kernel/kalloc.c
阅读了一下了一下kernel/kalloc.ckfree()kalloc(),内存是被用空闲列表freelist的形式管理的,且freelist是链表的头结点,当free一块内存的时候,freelist头插这块被free的内存,当申请一块内存的时候,freelist的头部被拿走。并且会更新头部的位置为先前头部节点的下一个节点。
因此,要获得空闲内存的大小只需要遍历一下freelist看看有多少个空闲的内存块就可以了。

uint64 free_mem(void) {
  struct run *r;
  uint64 count = 0;
  acquire(&kmem.lock);
  r = kmem.freelist;
  while(r) {
    r = r->next;
    count++;
  }
  release(&kmem.lock);

  return count * PGSIZE;

}

HINT5:要收集进程数,请添加一个函数kernel/proc.c
kernel/proc.callocproc()函数可以看出,这里的进程用的是一个进程表proc的结构来管理的,这个进程表就是struct proc构成的数组(struct proc proc[NPROC];
此外在kernle/proc.h中声明了enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
所以我们仿照allocproc()函数来实现我们的统计进程数量的函数即可:

uint64 proc_num(void) {
  struct proc *p;
  uint64 count = 0;
  for(p = proc; p < &proc[NPROC]; p++) {
    acquire(&p->lock);
    if(p->state != UNUSED) {
      count++;
    }
    release(&p->lock);
  }
  return count;

}

为了使得在kernel/sysproc.c 能够调用上述两个方法,需要将函数声明添加至kernel/defs.h

//kalloc.c
//...
void            kinit(void);
uint64          free_mem(void);

//proc,c
//...
void            procdump(void);
uint64          proc_num(void);

最后,需要在kernel/sysproc.c中实现sys_info()函数:

uint64 sys_info(void) {
  struct sysinfo info;
  uint64 addr;
  info.freemem = kfreemem();
  info.nproc = proc_num();
  
  if(argaddr(0, &addr) < 0) {
    return -1;
  }
  // copy info(kernel space) to addr(user space)
  if (copyout(myproc()->pagetable, addr, (char *)&info, sizeof(info)) < 0) {
    return -1;
  } else {
    return 0;
  }
}

为什么能通过argaddr()函数获得用户空间那边被复制的地址呢?因为调用的时候的形式是sysinfo(struct sysinfo *info)的形式,传参了info的地址。并且argaddr()函数输入参数中的第一个参数指的是获取寄存器a0中数据,第二个参数指的是用户空间保存地址。

结果

实验测试:
在这里插入图片描述

总结

因为这个Lab2的两个部分都是moderate难度的缘故,一路顺着HINT的提示做下来很流畅。
做完System call tracing部分粗浅地理解了xv6中系统调用的过程。在做完Sysinfo部分后算是初步窥探了一点xv6 中内存管理和进程管理的内容,此外还意识到需要利用copyout()函数将一块内存从内核态copy到用户态。
在此过程中也遇到一些问题在此做出笔记:

  • 现象:在测试sysinfo中输出painc:acquire
  • 原因:请求锁未释放
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值