MIT 6.S081 Lab2 system calls

379 阅读4分钟

#Head BLog

本人掘金的专栏文章链接,欢迎阅读!

  1. MIT-6.S081 xv6-labs-2020
  2. MIT-6.S081 xv6 book
  3. 启智好文
  4. C++
  5. Linux

这是本人的 CSDN 博客之分类专栏链接,欢迎点击阅读!

  1. MIT-6.S081 xv6-labs-2020
  2. MIT-6.S081 xv6 book
  3. 启智好文
  4. C++
  5. Linux

#Source

  1. MIT-6.S081 2020 课程官网
  2. Lab2: system calls 实验主页
  3. MIT-6.S081 2020 xv6 book

#My Code

  1. Lab2: system calls 的 GitHub
  2. xv6-labs-2020 的 GitHub 总目录

#Motivation

Lab2: system calls 主要是想让我们给 xv6 添加几个新的系统调用,在添加的过程中可以学习到一些 kernel 与 user 的交互规则

在开始实验之前,一定要阅读 xv6-6.S081 的第二章节 Operating system organization 及第四章节 Traps and device drivers 的第三小节 Traps from user space 和第四小节 Timer interrupts

#System call tracing (moderate)

#Motivation

主要是为了跟踪 shell 命令是否调用了指定的系统调用,并且还需知道系统调用的返回值。比如,

trace 32 grep hello README

跟踪 ,

grep hello README

因为 32=2532=2^5,5 对应 SYS_read ,所以只关心(跟踪)第 5 个系统调用,具体案例可见 Lab2: system calls 实验主页

#define SYS_read 5

其中第二个案例,

trace 2147483647 grep hello README

为什么 2147483647 可以跟踪所有系统调用,没搞明白,这或许并不重要,重要的是熟悉系统调用的流程!

#Solution

#S1 - 添加 sys_trace

首先需要了解 xv6 代码的组织结构,kernel/syscall.h 下保存着所有的系统调用编号,需要为 trace 系统调用添加新的编号,

#define SYS_trace 22

kernel/syscall.c 特别需要注意的是,函数指针数组 static uint64 (*syscalls[])(void) ,它将所有系统调用全部序列化,便于 syscall(void) 调用

在添加新的系统调用 trace 时,需要在 kernel/syscall.c 中添加额外的声明,

extern uint64 sys_trace(void);

接着在 kernel/syspro.c 中添加 sys_trace(void) 的具体实现,

uint64
sys_trace(void)
{
  int mask = 1;

  if(argint(0, &mask) < 0) {
    return -1;
  }
    
  myproc()->mask = mask;

  return 0;
}

其中需要注意 argint(0, &mask) 函数其实就是读取进程地址空间中 trapframe 的 0 号寄存器的值,argint(int, int*)argraw(int) 的具体实现见 kernel/syscall.c

最后需要在 user/user.h 中添加新的系统调用的声明,trace 的调用接口见 user/trace.c

int trace(int);

以及在 user/usys.pl 中添加 Perl 命令,

entry("trace");

MakefileUPROGS 选项中添加,

$U/_trace

完成上述添加任务之后,才能编译成功

#S2 - 设计 trace 系统调用

无编译问题后开始设计 trace 的算法,首先需要跳转到 user/trace.c 中明确 trace 是如何被调用的。当程序运行到 user/trace.c 的第 17 行时,

 if (trace(atoi(argv[1])) < 0) {

进程已从用户态进入到内核态了,在状态切换的过程中,进程将 trace 对应的 SYS_trace 系统编号写入进程地址空间的 trapframe 的 7 号寄存器中,具体见 user/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";
}

翻译一下,第 1 行 ,

sub entry

来到 user 转 kernel 的起始点,之后的几句话都是为状态转换做准备,尤其,

print " li a7, SYS_${name}\n";

将系统调用编号写入 trapframe 的 a7 寄存器中,然后调用 ecall 指令进入内核

需要注意一个细节,trace 接口是有函数参数和返回值的,而 syscall 是无参无返的。我大胆猜测,trace 算法的大致流程可能如下,

int trace(int mask) 
{
    uint64 x;	/** mask = 2^x */ 
  
    p->trapframe->a7 = x;
    p->trapframe->a0 = mask;
    
    syscall();
    
    return 1;
}

因为该函数是不接受参数的,所以我们只能通过地址空间传递参数,即将系统调用的编号写入寄存器中。如此,在执行 syscall(void) 时,通过,

num = p->trapframe->a7;

就能获取用户态的需求(调用哪个系统调用)。根据 trace 的算法目的, syscall() 中调用了 sys_trace() ,修改后的代码如下,

void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  /** 获取系统调用编号,具体见user/usys.pl的line13 */
  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    /** 进程的trapframe的a0寄存器存放系统调用的返回值 */
    p->trapframe->a0 = syscalls[num]();

    /** 输出我所关心的系统调用元数据 */
    if((1<<num) & p->mask) {
      printf("%d: syscall %s -> %d\n", p->pid, syscalls2strs[num], p->trapframe->a0);
    }
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

另外,还需在 fork() 的具体实现中添加 mask 标记位,指明哪个系统调用是我所关心的,

int
fork(void)
{
  ...
  np->state = RUNNABLE;

  /** 添加mask标记位 */
  np->mask = p->mask;

  release(&np->lock);
	...
}

必须将 mask 标记位告诉进程控制块(PCB),因为用户输入 trace 命令之后,xv6 会 fork 一个新的进程,让新进程接管跟踪事宜。新进程必须记录 mask 标记位(写入地址空间),只有这样,后续才有可能将标记位传入 kernel

#Result

可以通过,

./grade-lab-syscall trace

来验证程序是否正确

#Sysinfo (moderate)

#Motivation

主要是为了记录下 xv6 当前的系统信息,包括正在使用的进程数和空闲的内存块数(以 byte 为单位)

#Solution

#S0 - Makefile 等初始化配置

Lab: System call tracing 一样,在实现具体业务逻辑之前需要配置环境,根据 Lab2: system calls 给的提示进行设置

首先在 Makefile 文件的 UPROGS 字段中追加,

$U/_sysinfotest

然后在 user/user.h 中添加 sysinfo 函数声明,

struct sysinfo;
int sysinfo(struct sysinfo *);

同时还要在 kernel/syscall.hkernel/syscall.c 中添加声明和定义(见 Lab: System call tracing )以及追加 user/usys.pl

#S1 - 统计进程数和空闲内存块

完成配置,能够顺利编译之后正式进入具体业务逻辑实现环节

首先解决统计 xv6 正在使用的进程数问题,具体表现为遍历整个 proc 数组(添加在 kernel/proc.c 中) ,对其中的每个进程的状态进行判断和累计,代码如下,

uint64
nProcs()
{
  uint64 nums = 0;
  int i;
  
  for(i=0; i<NPROC; i++) {
    struct proc* p = &proc[i];
    
    acquire(&p->lock);
    if(p->state != UNUSED) 
      nums++;
    release(&p->lock);
  }

  return nums;
} 

接着解决统计 xv6 空闲的内存块数问题,xv6 有一个空闲链表,具体定义如下,

struct run {
  struct run *next;
};

struct {
  struct spinlock lock;
  struct run *freelist;
} kmem;

具体表现为顺着 freelist 遍历完所有空闲节点,即可完成统计工作。需要注意的是在操作 kmem 时需要加锁,具体代码如下,

uint64
nFreeMems(void)
{
  uint64 nums = 0;

  struct run* ptr = kmem.freelist;
  struct spinlock* lock = &kmem.lock;
  
  acquire(lock);
  while(ptr) {
    nums++;
    ptr = ptr->next;
  }
  release(lock);

  return nums*PGSIZE;
}

最后还需在 kernel/defs.h 中添加 nProcs()nFreeMems() 函数声明

在完成统计的具体任务之后我们需要将其组合起来,在 kernel/sysproc.c 中编写 sys_sysinfo 系统调用,具体代码如下,

uint64
sys_sysinfo(void)
{
  struct proc* proc = myproc();
  struct sysinfo sysinfo;
  uint64 addr;

  if(argaddr(0, &addr) != SYS_OK) 
    return SYS_ERROR;

  sysinfo.nproc = nProcs();
  sysinfo.freemem = nFreeMems();

  if(copyout(proc->pagetable, addr, (char*)&sysinfo, sizeof(sysinfo)) != SYS_OK)
    return SYS_ERROR;

  return SYS_OK;
}

首先获取 proc 进程,然后抓取正在使用的进程数和空闲块数,将结果写入 sysinfo 结构体中,最后通过 copyout() 函数将获取的参数从 kernel 传回 user 。其中 copyout() 的大致功能就是将 sysinfo 结构体写入 pagetable 的指定偏移 addr

#Result

手动进入 qemu,

make qemu
$sysinfotest

或运行脚本,

./grade-lab-syscall sysinfo

#Reference

  1. 知乎 - MIT 6.S081 2020 Lab2 system calls讲解