Lab2: system calls

上周打了一场数模国赛,打前元气超人,打完葛优躺平。Lab2只有两个小实验,聚焦于如何创建系统调用命令,也并不是那么简单,实验文档戳这

alt

❖ Coding


☑︎ System call tracing (moderate)

我们需要创建一个trace系统调用来追踪一个命令中指定的系统调用,并打印出相应的返回值。

$ trace 2147483647 grep hello README
4: syscall trace -> 0
4: syscall exec -> 3
4: syscall open -> 3
4: syscall read -> 1023
4: syscall read -> 966
4: syscall read -> 70
4: syscall read -> 0
4: syscall close -> 0

指令格式:trace mask cmd。mask用来指定需要追踪的系统调用,在操作系统中每个系统调用都会有自己唯一的编号SYS_syscall(在kernel/syscall.h中的宏定义),比如说fork的编号就是SYS_fork,如果mask&(1<<SYS_syscall)!=0就说明需要追踪该syscall,需要打印出其返回值,本质上如果假设 代表其二进制低 位数字的话,那么 就代表需要追踪SYS_syscall= 的系统调用。

那么我们就需要在进程结构体proc中加入int mask,注意到一个进程中执行不同系统调用是通过fork来实现的,本质上又创建了一个proc实体,因此在fork的实现(kernel/proc.c)中我们需要实现mask的传递:

np->mask = p->mask;

那么接下来就可以打印信息pid: syscall name -> return_value。我们可以在每个系统调用都必经的函数syscall(kernel/syscall.c)里面实现,首先观察syscall函数:

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]();
  }
  else
  {
    printf("%d %s: unknown sys call %d\n",p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

可以看出p里面存储了我们需要的pid,但p->name并不是我们想要的系统调用名称,注意到fork实现中(kernel/proc.c:304)是直接拷贝原proc的name,即name里存储的其实是进程名:

safestrcpy(np->name, p->name, sizeof(p->name));

这里需要注意进程与线程之间的差别。虽然fork完后也是一个proc结构体,但事实上只是p->name进程下的一个线程而已。进程是对运行程序的封装,是系统进行资源调度和分配的基本单位,各进程之间相互隔离不共享数据,实现了操作系统的并发性;而线程则是一个进程中的子任务,是CPU调度的最小单位,一个进程中的所有线程共享该进程的数据资源。那么每个命令就是一个进程,里面会执行多个系统调用,每个系统调用都是一个线程。

虽然p里面没有线程名称的直接信息,但肯定有其他相关信息。注意到

p->trapframe->a0 = syscalls[num]();

这里调用了syscalls[num],然后将返回值传递给了a0寄存器(RISC-V的C规范是把返回值放在a0中)。syscalls是一个无符号整型指针数组,即函数指针。那么可以肯定这个num将是一个重要的突破口,我们只要按照syscalls中系统调用的顺序定义一个字符串数组sysname,sysname[num]就是系统调用的名称了。同时p->trapframe->a0就是函数返回值。

107 + │ extern uint64 sys_trace(void);

 132 + │ [SYS_trace]   sys_trace, //static uint64 (*syscalls[])(void)

 136 + │ char *sysname[] = {
 137 + │ [SYS_fork]    "fork",
 138 + │ [SYS_exit]    "exit",
 139 + │ [SYS_wait]    "wait",
 140 + │ [SYS_pipe]    "pipe",
 141 + │ [SYS_read]    "read",
 142 + │ [SYS_kill]    "kill",
 143 + │ [SYS_exec]    "exec",
 144 + │ [SYS_fstat]   "stat",
 145 + │ [SYS_chdir]   "chdir",
 146 + │ [SYS_dup]     "dup",
 147 + │ [SYS_getpid]  "getpid",
 148 + │ [SYS_sbrk]    "sbrk",
 149 + │ [SYS_sleep]   "sleep",
 150 + │ [SYS_uptime]  "uptime",
 151 + │ [SYS_open]    "open",
 152 + │ [SYS_write]   "write",
 153 + │ [SYS_mknod]   "mknod",
 154 + │ [SYS_unlink]  "unlink",
 155 + │ [SYS_link]    "link",
 156 + │ [SYS_mkdir]   "mkdir",
 157 + │ [SYS_close]   "close",
 158 + │ [SYS_trace]   "trace",
 159   │ };
 160   │ 
 161   │ void
 162   │ syscall(void)
 163   │ {
 164   │   int num;
 165   │   struct proc *p = myproc();
 166   │ 
 167   │   num = p->trapframe->a7;
 168   │   if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
 169   │     p->trapframe->a0 = syscalls[num]();
 170 + │     if(1<<num & p->mask)
 171 + │         printf("%d: syscall %s -> %d\n",p->pid,sysname[num],p->trapframe->a0);
 172   │   } else {
 173   │     printf("%d %s: unknown sys call %d\n",
 174   │             p->pid, p->name, num);
 175   │     p->trapframe->a0 = -1;
 176   │   }
 177   │ }

o对,我们甚至还没实现trace函数💦。你可能会觉得上面的做法已经间接实现了trace,但事实上trace是在用户模式(user mode)调用的,因此传入的参数还在用户空间,而系统调用都是在内核空间内执行的,为了操作系统不受进程影响两者是分隔的。

alt

用户态的trace函数已经帮我们实现:

#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int
main(int argc, char *argv[])
{
  int i;
  char *nargv[MAXARG];

  if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){
    fprintf(2, "Usage: %s mask command\n", argv[0]);
    exit(1);
  }

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

  for(i = 2; i < argc && i < MAXARG; i++){
    nargv[i-2] = argv[i];
  }
  exec(nargv[0], nargv);
  exit(0);
}

不难看出关键函数是int trace(int),那我们需要先在user/user.h中声明该系统调用,同时在user/usys.pl(生成汇编文件user/usys.S)中加入entry("trace")使trace函数可以进入内核模式,并在kernel/syscall.h中添加一个系统调用号,整个系统调用过程如下所示

  1. user/user.h:用户态程序调用跳板函数 trace()

  2. user/usys.S:跳板函数 trace 使用 CPU 提供的 ecall 指令,进入内核模式

  3. kernel/syscall.c:到达内核模式统一执行系统调用处理函数syscall,所有系统调用都会跳到这里来处理

  4. kernel/syscall.c:syscall根据跳板传进来的系统调用编号,查询 syscalls[] 表,找到对应的内核函数并调用

  5. kernel/sysproc.c:到达 sys_trace 函数,执行具体内核操作

接下来只需在kernel/sysproc.c中实现sys_trace,该函数只要把用户空间的mask拿进来就行,cmd在执行时事实上可以认为是trace的多个子线程,因此mask会一直传递下去。

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

实现一个系统调用sysinfo,它接受一个指向struct sysinfo的指针,然后向这个结构体写入剩余空间的字节数和状态非”UNUSED”的进程数。

───────┬─────────────────────────────────────────────────────────
       │ File: kernel/sysinfo.h
───────┼─────────────────────────────────────────────────────────
   1   │ struct sysinfo {
   2   │   uint64 freemem;   // amount of free memory (bytes)
   3   │   uint64 nproc;     // number of process
   4   │ };
───────┴─────────────────────────────────────────────────────────

首先我们需要实现统计剩余空间字节数和已使用进程数的函数。关于空间分配的实现在kernel/kalloc.c,注意到kmem中有一项freelist链表,然后看kfree函数

46   │ void
  47   │ kfree(void *pa)
  48   │ {
  49   │   struct run *r;
  50   │ 
  51   │   if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
  52   │     panic("kfree");
  53   │ 
  54   │   // Fill with junk to catch dangling refs.
  55   │   memset(pa, 1, PGSIZE);
  56   │ 
  57   │   r = (struct run*)pa;
  58   │ 
  59   │   acquire(&kmem.lock);
  60   │   r->next = kmem.freelist;
  61   │   kmem.freelist = r;
  62   │   release(&kmem.lock);
  63   │ }

kfree函数将pa地址开始PGSIZE大小的空间都初始化为1,然后将pa挂到freelist的头上,再将freelist指向pa,而kalloc函数则是每次都使用freelist的第一块PGSIZE空间。说明freelist是可用空间的链表,而且每次分配的最小单元是PGSIZE字节,那么我们只要数一下freelist的元素个数然后乘个PGSIZE即可。

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

为防止竞态条件产生,在统计时需要先给kmem上锁。

对于统计状态为非”UNUSED”的进程数也不难,注意到kernel/proc.c中

11   │ struct proc proc[NPROC];

可知最大进程数即为NPROC,那么我们只要遍历proc,然后依次检查状态即可。

uint64
count_process(void)
{
  uint64 cnt = 0;
  for (int i = 0; i < NPROC; ++i)
    if (proc[i].state != UNUSED)
      ++cnt;
  return cnt;
}

最后我们只要将这个sysinfo结构体拷贝至用户空间即可。首先通过argaddr函数获取用户态int sysinfo(struct sysinfo *)传入的sysinfo虚地址,然后结合该进程的页表pagetable可以得到该地址对应的物理地址,最后将内核中的sysinfo结构体拷贝过去。

uint64
sys_sysinfo(void)
{
  struct proc *p = myproc();
  struct sysinfo sf;
  uint64 addr;
  sf.freemem = count_freemem();
  sf.nproc = count_process();
  if (argaddr(0, &addr) < 0 || copyout(p->pagetable, addr, (char *)&sf, sizeof(sf)) < 0)
    return -1;
  return 0;
}

argaddr第一个参数0是指从a0寄存器获取,我特地回去翻了一下计组PPT

alt

果然,a0、a1存储函数参数,as I guess。

其余声明之类的与上面的trace类似。另外别忘了在kernel/sysproc.c中加入

9 + │ #include "kernel/sysinfo.h"

如果一切正常,在xv6启动后输入sysinfotest会输出OK。

$ sysinfotest
sysinfotest: start
sysinfotest: OK

❖ Reference


[1] MIT 6.S081 2020 Lab2 system calls讲解

[2] [mit6.s081] 笔记 Lab2: System calls | 系统调用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值