「实验记录」MIT 6.S081 Lab2 system calls

I. Source

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

II. My Code

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

III. Motivation

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

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

IV. System call tracing (moderate)

i. Motivation

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

trace 32 grep hello README

跟踪 ,

grep hello README

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

#define SYS_read 5

其中第二个案例,

trace 2147483647 grep hello README

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

ii. 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

iii. Result

可以通过,

./grade-lab-syscall trace

来验证程序是否正确

V. Sysinfo (moderate)

i. Motivation

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

ii. Solution

S1 - 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

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

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

首先解决统计 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

iii. Result

手动进入 qemu,

make qemu
$sysinfotest

或运行脚本,

./grade-lab-syscall sysinfo

qemu 退出命令,

ctrl + a ... x

VI. Reference

  1. 知乎 - MIT 6.S081 2020 Lab2 system calls讲解
  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值