6.S081 的第二个实验,system calls,虽然是好久之前做的了,但是一直没把它放到博客里面。今天略无聊,就弄了一下。BTW,第三个实验我到现在都还没开始呢。
system call
在完成实验之前,我们可以先了解一下系统调用的相关的代码。
首先在user/usys.pl
中包含生成系统调用汇编的 Perl 代码,其核心为下面这段代码:
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("fork");
会得到一下汇编:
fork:
li a7, SYS_fork
ecall
ret
从xv6-book
中,我们可以知道:
ecall
是rsic-v
中用来从用户态转到特权态的指令a7
用来保存要调用的系统调用号。SYS_fork
在kernel/syscall.h
中被定义。
在ecall
被调用时,RISC-V 的 CPU 会设置一些寄存器(如将pc
保存在sepc
),然后将stvec
保存到pc
中,这时,CPU
就会执行stvec
所指向的代码了,stvec
在返回用户态时会被设置成kernel/trampoline.S
中uservec
(见kernel/trap.c#L100
).
uservec
的主要做了以下几件事情:
- 交换
a0
和sscratch
寄存器,而sscratch
是进程用户空间的trapframe
的虚拟地址,此时已经将a0
保存到sscratch
寄存器中了。 - 保存寄存器到
trapframe
中(包括a0
) - 从
trapframe
中恢复内核栈、切换页表 - 跳转到
usertrap
中
usertrap
(kernel/trap.c#32
)主要做了以下几件事:
- 检验是否从用户态进入(通过
sstatus
确认)。 - 设置
stvec
为kernelvec
,保存用户态pc
(在调用ecall
时被保存到了sepc
中了)。 - 关中断
- 执行
syscall
进行系统调用 - 调用
usertrapret
返回
前面的步骤已经将用户态转换成内核态,并将需要保存的信息保存到对应的位置,并将栈和页表等切换成内核所使用的栈和页表了。
syscall
(kernel/syscall.c#133
)的代码如下:
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;
}
}
可以看到在syscall
中,它从a7
取出system call number
,并调用了对应的handler
,然后把返回值保存到用户态的a0
中。
系统调用返回的过程有点像是刚才的逆过程,uservec
和usertrap
分别对应userret
和usertrapret
。这里不在赘述。
总结以下,当我们调用fork()
时会发生的事情。
- 通过
usys.pl
生成的汇编fork()
将参数放到a7
中,并调用ecall
- 调用
ecall
时,CPU 会执行一些操作(保存用户态pc
到sepc
,拷贝stvec
到pc
中) - 由于
stvec
在用户态时指向uservec
,所以会执行uservec
来保存寄存器、切换页表、恢复内核堆栈,并跳转到usertrap
usertrap
会将stvec
修改成kernelvec
,关中断调用syscall
syscall
会根据a7
中保存的system call number
来调用对应的handler
,执行完后返回值保存在a0
中。usertrap
末尾调用usertrapret
,而usertrapret
会跳转到userret
,这两个过程会逆向uservec
和usertrap
的操作。
trace
由于前面已经分析过调用系统调用的过程了,但是添加一个系统调用还是有所不同的,我们要进行以下步骤:
- 在
kernel/syscall.h
中设置系统调用号,如#define SYS_trace 22
- 在
kernel/syscall.c
中的syscalls
数组中设置对应的 handler 函数,同时需要在该数组的上方加入相关的函数声明,如extern unit64 sys_trace(void);
- 在
kernel/sysproc.c
中实现对应的 handler 函数,如sys_trace
函数 - 在
user/user.h
中添加函数声明,如int trace(int);
- 在
user/usys.pl
添加对应的entry
已生成相应系统调用的汇编,如entry("trace");
添加完成后,我们可以就需要实现kernel/sysproc.c
中sys_trace
的实现了。
首先,题目要求我们能够通过trace
系统调用来设置哪些系统调用会被trace
,而且trace
的参数也已经提示我们要用1 << SYS_*
的方式来设置mask
了,mask
应该由trace
命令来设置。trace
应该是只时对某个进程有效的,所以这个mask
应该与进程绑定,所以我们可以在进程proc
结构体中加入一个新的变量tracemask
。然后sys_trace
只需要获取用户态传入的参数,并设置进程的tracemask
即可。
sys_trace
的实现如下:
uint64
sys_trace(void)
{
int mask;
if (argint(0, &mask) < 0)
return -1;
myproc()->tracemask = mask;
return 0;
}
从上面关于系统调用的分析可以知道,每个系统调用的 handler 都会由syscall
来调用,所以我们可以在修改这个函数来实现每次系统调用 handler 返回后都输出对应的信息。pid
和系统调用返回值都可以直接获取到,而系统调用名称则需要自己手动写一个字符串数组来映射。下面是实现代码:
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]();
if (!!(1 << num & p->tracemask)) {
printf("%d: syscall %s -> %d\n", p->pid, syscall_name[num], p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
由于题目要求某个进程被trace
后,其子进程也要被trace
,所以我们只需要在fork
中简单的添加np->tracemask = p->tracemask
即可。
sysinfo
这里要增加一个系统调用来实现sysinfo
,因为前面已经描述过给xv6
增加系统调用的过程了,这里不在赘述。
因为sysinfo
需要传入一个struct sysinfo
的指针,然后通过这个指针返回一个struct sysinfo
,这个结构体主要包含两个属性:freemem
和nproc
,即当前空闲内存和当前状态不为UNUSED
的proc
数量。所以这个实验主要要解决三个问题:
- 计算当前空闲内存大小
- 计算当前状态不为
UNUSED
的proc
数量 - 将计算得到的值从内核态拷贝到用户态。
freemem
在kernel/kalloc.c
代码中可以看到:
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
这里有一个全局变量kmem
,她包含了一个名为freelist
的链表指针和保护该链表的锁。从其他相关代码可以看出这个freelist
所代表的链表是空闲页,每一个空闲页的开头都保存着下一个空闲页的位置,由于每一页的大小都是固定的,已知的(由PGSIZE
确定),所以通过遍历一个freelist
可以计算出空闲内存大小。这个要主要的是在遍历链表时,需要想获取锁。实现代码如下:
uint64
kfreemem(void)
{
uint64 size = 0;
struct run *r;
acquire(&kmem.lock);
for(r = kmem.freelist ; r ; r = r->next)
size += PGSIZE;
release(&kmem.lock);
return size;
}
nproc
从kernel/proc.c
可以看出来内核中可用的struct proc
是由一个数组存储的,我们可以简单的遍历这个数组,然后判断每个元素的状态是否为UNUSED
,然后计数即可。代码如下:
uint64
nproc(void)
{
uint64 n = 0;
struct proc *p;
for(p = proc;p < &proc[NPROC]; p++) {
if (p->state != UNUSED)
n++;
}
return n;
}
copyout
由于sys_sysinfo
系统调用读到的地址是用户态的虚拟地址,所以不能在内核直接使用,copyout
函数来将内存中的数据拷贝到用户态。从实验给出的提示可以看出,可以从参考sys_fstat
和filestat
来使用copyout
。由于sys_fstat
只涉及到读取参数和调用filestat
,所以这里只给出filestat
的代码,它的第二个参数就是用户态的地址。
// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
struct proc *p = myproc();
struct stat st;
if(f->type == FD_INODE || f->type == FD_DEVICE){
ilock(f->ip);
stati(f->ip, &st);
iunlock(f->ip);
if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
return -1;
return 0;
}
return -1;
}
上面的代码关键在于copyout(p->pagetable, add, (char*)&st, sizeof(st))
,这里可以看出copyout
是通过进程的页表来计算实地址的位置。
因此,我们可以写出sys_sysinfo
的代码如下:
uint64
sys_sysinfo(void)
{
struct sysinfo info;
struct proc *p = myproc();
uint64 pinfo;
if (argaddr(0, &pinfo) < 0)
return -1;
info.freemem = kfreemem();
info.nproc = nproc();
if (copyout(p->pagetable, pinfo, (char *)&info, sizeof(info)) < 0)
return -1;
return 0;
}
作者:wuxiaobai24
发表日期:12/14/2020
本文首发地址:6.S081 lab2 system calls
版权声明:CC BY NC SA 4.0