Code & Func

6.S081 lab2 system calls

2020-12-14
OS
6.S081
xv6
RISC-V
最后更新:2024-09-19
11分钟
2035字

6.S081 的第二个实验,system calls,虽然是好久之前做的了,但是一直没把它放到博客里面。今天略无聊,就弄了一下。BTW,第三个实验我到现在都还没开始呢。

system call

在完成实验之前,我们可以先了解一下系统调用的相关的代码。

首先在user/usys.pl中包含生成系统调用汇编的 Perl 代码,其核心为下面这段代码:

1
sub entry {
2
my $name = shift;
3
print ".global $name\n";
4
print "${name}:\n";
5
print " li a7, SYS_${name}\n";
6
print " ecall\n";
7
print " ret\n";
8
}

可以看出来这是一个创建汇编的代码,例如调用entry("fork");会得到一下汇编:

1
fork:
2
li a7, SYS_fork
3
ecall
4
ret

xv6-book中,我们可以知道:

  • ecallrsic-v中用来从用户态转到特权态的指令
  • a7用来保存要调用的系统调用号。
  • SYS_forkkernel/syscall.h中被定义。

ecall被调用时,RISC-V 的 CPU 会设置一些寄存器(如将pc保存在sepc),然后将stvec保存到pc中,这时,CPU就会执行stvec所指向的代码了,stvec在返回用户态时会被设置成kernel/trampoline.Suservec(见kernel/trap.c#L100).

uservec的主要做了以下几件事情:

  1. 交换a0sscratch寄存器,而sscratch是进程用户空间的trapframe的虚拟地址,此时已经将a0保存到sscratch寄存器中了。
  2. 保存寄存器到trapframe中(包括a0
  3. trapframe中恢复内核栈、切换页表
  4. 跳转到usertrap

usertrapkernel/trap.c#32)主要做了以下几件事:

  1. 检验是否从用户态进入(通过sstatus确认)。
  2. 设置stveckernelvec,保存用户态pc(在调用ecall时被保存到了sepc中了)。
  3. 关中断
  4. 执行syscall进行系统调用
  5. 调用usertrapret返回

前面的步骤已经将用户态转换成内核态,并将需要保存的信息保存到对应的位置,并将栈和页表等切换成内核所使用的栈和页表了。

syscallkernel/syscall.c#133)的代码如下:

1
void
2
syscall(void)
3
{
4
int num;
5
struct proc *p = myproc();
6
7
num = p->trapframe->a7;
8
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
9
p->trapframe->a0 = syscalls[num]();
10
} else {
11
printf("%d %s: unknown sys call %d\n",
12
p->pid, p->name, num);
13
p->trapframe->a0 = -1;
14
}
15
}

可以看到在syscall中,它从a7取出system call number,并调用了对应的handler,然后把返回值保存到用户态的a0中。

系统调用返回的过程有点像是刚才的逆过程,uservecusertrap分别对应userretusertrapret。这里不在赘述。

总结以下,当我们调用fork()时会发生的事情。

  1. 通过usys.pl生成的汇编fork()将参数放到a7中,并调用ecall
  2. 调用ecall时,CPU 会执行一些操作(保存用户态pcsepc,拷贝stvecpc中)
  3. 由于stvec在用户态时指向uservec,所以会执行uservec来保存寄存器、切换页表、恢复内核堆栈,并跳转到usertrap
  4. usertrap会将stvec修改成kernelvec,关中断调用syscall
  5. syscall会根据a7中保存的system call number来调用对应的handler,执行完后返回值保存在a0中。
  6. usertrap末尾调用usertrapret,而usertrapret会跳转到userret,这两个过程会逆向uservecusertrap的操作。

trace

由于前面已经分析过调用系统调用的过程了,但是添加一个系统调用还是有所不同的,我们要进行以下步骤:

  1. kernel/syscall.h中设置系统调用号,如#define SYS_trace 22
  2. kernel/syscall.c中的syscalls数组中设置对应的 handler 函数,同时需要在该数组的上方加入相关的函数声明,如extern unit64 sys_trace(void);
  3. kernel/sysproc.c中实现对应的 handler 函数,如sys_trace函数
  4. user/user.h中添加函数声明,如int trace(int);
  5. user/usys.pl添加对应的entry已生成相应系统调用的汇编,如entry("trace");

添加完成后,我们可以就需要实现kernel/sysproc.csys_trace的实现了。

首先,题目要求我们能够通过trace系统调用来设置哪些系统调用会被trace,而且trace的参数也已经提示我们要用1 << SYS_*的方式来设置mask了,mask应该由trace命令来设置。trace应该是只时对某个进程有效的,所以这个mask应该与进程绑定,所以我们可以在进程proc结构体中加入一个新的变量tracemask。然后sys_trace只需要获取用户态传入的参数,并设置进程的tracemask即可。

sys_trace的实现如下:

1
uint64
2
sys_trace(void)
3
{
4
int mask;
5
if (argint(0, &mask) < 0)
6
return -1;
7
myproc()->tracemask = mask;
8
return 0;
9
}

从上面关于系统调用的分析可以知道,每个系统调用的 handler 都会由syscall来调用,所以我们可以在修改这个函数来实现每次系统调用 handler 返回后都输出对应的信息。pid和系统调用返回值都可以直接获取到,而系统调用名称则需要自己手动写一个字符串数组来映射。下面是实现代码:

1
void
2
syscall(void)
3
{
4
int num;
5
struct proc *p = myproc();
6
7
num = p->trapframe->a7;
8
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
9
p->trapframe->a0 = syscalls[num]();
10
if (!!(1 << num & p->tracemask)) {
11
printf("%d: syscall %s -> %d\n", p->pid, syscall_name[num], p->trapframe->a0);
12
}
13
} else {
14
printf("%d %s: unknown sys call %d\n",
15
p->pid, p->name, num);
3 collapsed lines
16
p->trapframe->a0 = -1;
17
}
18
}

由于题目要求某个进程被trace后,其子进程也要被trace,所以我们只需要在fork中简单的添加np->tracemask = p->tracemask即可。

sysinfo

这里要增加一个系统调用来实现sysinfo,因为前面已经描述过给xv6增加系统调用的过程了,这里不在赘述。

因为sysinfo需要传入一个struct sysinfo的指针,然后通过这个指针返回一个struct sysinfo,这个结构体主要包含两个属性:freememnproc,即当前空闲内存和当前状态不为UNUSEDproc数量。所以这个实验主要要解决三个问题:

  1. 计算当前空闲内存大小
  2. 计算当前状态不为UNUSEDproc数量
  3. 将计算得到的值从内核态拷贝到用户态。

freemem

kernel/kalloc.c代码中可以看到:

1
struct run {
2
struct run *next;
3
};
4
5
struct {
6
struct spinlock lock;
7
struct run *freelist;
8
} kmem;

这里有一个全局变量kmem,她包含了一个名为freelist的链表指针和保护该链表的锁。从其他相关代码可以看出这个freelist所代表的链表是空闲页,每一个空闲页的开头都保存着下一个空闲页的位置,由于每一页的大小都是固定的,已知的(由PGSIZE确定),所以通过遍历一个freelist可以计算出空闲内存大小。这个要主要的是在遍历链表时,需要想获取锁。实现代码如下:

1
uint64
2
kfreemem(void)
3
{
4
uint64 size = 0;
5
struct run *r;
6
acquire(&kmem.lock);
7
8
for(r = kmem.freelist ; r ; r = r->next)
9
size += PGSIZE;
10
release(&kmem.lock);
11
return size;
12
}

nproc

kernel/proc.c可以看出来内核中可用的struct proc是由一个数组存储的,我们可以简单的遍历这个数组,然后判断每个元素的状态是否为UNUSED,然后计数即可。代码如下:

1
uint64
2
nproc(void)
3
{
4
uint64 n = 0;
5
struct proc *p;
6
for(p = proc;p < &proc[NPROC]; p++) {
7
if (p->state != UNUSED)
8
n++;
9
}
10
return n;
11
}

copyout

由于sys_sysinfo系统调用读到的地址是用户态的虚拟地址,所以不能在内核直接使用,copyout函数来将内存中的数据拷贝到用户态。从实验给出的提示可以看出,可以从参考sys_fstatfilestat来使用copyout。由于sys_fstat只涉及到读取参数和调用filestat,所以这里只给出filestat的代码,它的第二个参数就是用户态的地址。

1
// Get metadata about file f.
2
// addr is a user virtual address, pointing to a struct stat.
3
int
4
filestat(struct file *f, uint64 addr)
5
{
6
struct proc *p = myproc();
7
struct stat st;
8
9
if(f->type == FD_INODE || f->type == FD_DEVICE){
10
ilock(f->ip);
11
stati(f->ip, &st);
12
iunlock(f->ip);
13
if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
14
return -1;
15
return 0;
3 collapsed lines
16
}
17
return -1;
18
}

上面的代码关键在于copyout(p->pagetable, add, (char*)&st, sizeof(st)),这里可以看出copyout是通过进程的页表来计算实地址的位置。

因此,我们可以写出sys_sysinfo的代码如下:

1
uint64
2
sys_sysinfo(void)
3
{
4
struct sysinfo info;
5
struct proc *p = myproc();
6
uint64 pinfo;
7
8
if (argaddr(0, &pinfo) < 0)
9
return -1;
10
11
info.freemem = kfreemem();
12
info.nproc = nproc();
13
14
15
if (copyout(p->pagetable, pinfo, (char *)&info, sizeof(info)) < 0)
3 collapsed lines
16
return -1;
17
return 0;
18
}
本文标题:6.S081 lab2 system calls
文章作者:wuxiaobai24
发布时间:2020-12-14