lab2 是关于系统调用的,很有意思,带我简单领略了如何进行系统调用。

# System call tracing

任务就是跟踪系统调用,如果内核执行了系统调用就打印追踪的结果。

首先,需要在 user.h 中定义这么一个 trace 函数,用户可以执行。

int trace(int);

然后,在 usys.pl 添加一个 entry:

sub entry {
    my $name = shift;
    print ".global $name\n";
    print "${name}:\n";
    print " li a7, SYS_${name}\n";
    print " ecall\n";
    print " ret\n";
}
entry("sysinfo")

这个脚本运行后会生成 usys.S 汇编文件,里面定义了每个 system call 的用户态跳板函数:

 trace:		# 定义用户态跳板函数
 li a7, SYS_trace	# 将系统调用 id 存入 a7 寄存器
 ecall				# ecall,调用 system call ,跳到内核态的统一系统调用处理函数 syscall()  (syscall.c)
 ret

注意这里的 li a7, SYS_trace ,xv6 将系统调用的调用函数 ID 存入 a7 寄存器中,接着执行 ecall 进行系统调用。到这里,用户态已经完成了自己的工作了。

很显然, ecall 让 xv6 执行系统调用,xv6 做的应该是从寄存器 a7 中去除系统调用函数的 id 号,然后检索找到这个 id 号对应的系统调用函数,接着再执行这个系统调用函数。

事实上,xv6 正是这么做的,在内核的 sys_call.c 文件中定义了如何进行系统调用:

首先这个文件用 extern 关键字声明了,这些系统调用函数,例如:

// 片段
extern uint64 sys_unlink(void);
extern uint64 sys_wait(void);
extern uint64 sys_write(void);
extern uint64 sys_uptime(void);
extern uint64 sys_trace(void);

然后声明了一个数组,数组的索引是系统调用函数的 id,value 就是这些调用函数:

static uint64 (*syscalls[])(void) = {
// 片段
[SYS_fork]    sys_fork,
[SYS_exit]    sys_exit,
[SYS_wait]    sys_wait,
[SYS_pipe]    sys_pipe,
[SYS_read]    sys_read,
[SYS_kill]    sys_kill,
}

最后就是 syscall 函数,再次说明, ecall 实际上做的就是将进程切换到内核态,并且调用该 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]();
    check_trace(num, p); // 添加 check trace,进行追踪
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

果然, syscall 首先从寄存器 a7 中拿出了 user 放在 a7 中的系统调用 id,然后判断这个 id 是否合法,如果合法就执行该系统调用函数,并把调用结果放在 a0

check_trace 中我们会判断是否需要最终该调用。

现在我们终于明白了,整个系统调用的过程了:

  1. 进程在 start 后进入 user mode,会保存系统调用函数的 id,但是它并不知道这些系统调用的代码,它只是把函数 id 号放在寄存器 a7 中以及把系统调用的参数放在 a0-a6 寄存器中,然后通过 ecall 告知内核,请帮我执行这个系统调用函数。
  2. 内核收到 ecall 后,会把进程的模式修改为内核模式,然后在进程的内核栈执行 syscall。 syscall 从寄存器 a7 中拿到该调用函数 id,检查这个 id 号是否合法,如果合法就执行对应的函数,今后把调用返回的结果放在 a0

对于 trace 该系统调用,我们还没做的就是添加该系统调用 id,以及实现该系统调用函数。

因此,我们接下来需要在 syscall.h 中注册该 id:

#define SYS_trace  22

然后,在 sysproc.c 中实现 uint64 sys_trace(void) :

uint64
sys_trace(void){
  int n;
  if(argint(0, &n) < 0){
    return -1;
  }   
  return trace(n);  // trace
}

接着在 proc.c 中添加 int trace(int) :

int
trace(int mask)
{
    struct proc *p = myproc();
    p->mask = mask;
    return 0; 
}

至此,整个系统调用过程我们已经完全搞定了!

# 遇到的坑

在刚开始,我也遇到了一个坑,比如在 proc.c 中定义好了 trace 函数却没法在 sysproc.c 中调用。

原因是我没有在头文件中声明该函数,只需要在 defs.h 文件中添加:

int trace(int);

就可以使用该函数了,如果不想在头文件中添加,也可以在 sysproc.c 中使用 extern 关键字来声明:

extern int trace(int);

这里有一个文章,详细描述了 extern 和头文件的关系:

https://www.runoob.com/w3cnote/extern-head-h-different.html

# Sysinfo

这个没什么好说了,按照刚才的步骤一步步实现该调用就行了,我的代码

void unusedproc(void* u){
  struct proc* p;
  uint s = 0;
  
  for(p=proc; p < &proc[NPROC] ; p++){
    if(p->state != UNUSED){
      s++;
    }
  }
  *(uint64*)u = s;
}
// 检查有有多少个已用 proc
void freemem(void *f)
{
  acquire(&kmem.lock);
  uint64 s = 0;
  struct run *r = kmem.freelist;
  while(r){
    s+=4096;
    r = r->next;
  }
  *(uint64*)f = s; // 指针强转
  release(&kmem.lock);
}
// 检查剩余空间

系统的剩余空间是已 freelist 这样一个链表形式进行保存的,每当进程申请空间就在这个空闲空间链表中拿下一个节点,free 就是把节点加到这个 freelist 中:

void *
kalloc(void)
{
  struct run *r;
  acquire(&kmem.lock); // 分配内存需要加锁
  r = kmem.freelist;
  if(r)
    kmem.freelist = r->next;
  release(&kmem.lock);
  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}

上面的就是分配内存空间代码,kv6 每个节点表示 4096B 大小。

以及 sysproc.c 代码:

uint64
sys_sysinfo(void){
  struct sysinfo info;
  struct proc *p = myproc();
  uint64 add; // user pointer to struct stat
  if(argaddr(0, &add) < 0)
    return -1;
  freemem(&info.freemem);
  unusedproc(&info.nproc);
  if(copyout(p->pagetable, add, (char *)&info, sizeof(info)) < 0)return -1;
    
  return 0;
}
更新于

请我喝[茶]~( ̄▽ ̄)~*

Kalice 微信支付

微信支付

Kalice 支付宝

支付宝