本文为 MIT 6.S081 2020 操作系统 实验一解析。
MIT 6.S081课程前置基础参考: 基于RISC-V搭建操作系统系列
在上一个实验中,您使用系统调用编写了一些实用程序。在本实验室中,您将向xv6添加一些新的系统调用,这将帮助您了解它们是如何工作的,并使您了解xv6内核的一些内部结构。您将在以后的实验室中添加更多系统调用。
Attention:
要开始本章实验,请将代码切换到syscall分支:
$ git fetch
$ git checkout syscall
$ make clean
如果运行make grade,您将看到测试分数的脚本无法执行trace和sysinfotest。您的工作是添加必要的系统调用和存根(stubs)以使它们工作。
YOUR JOB:
fork
系统调用,程序调用trace(1 << SYS_fork)
,其中SYS_fork
是kernel/syscall.h
中的系统调用编号。xv6
内核,以便在每个系统调用即将返回时打印出一行。我们提供了一个用户级程序版本的trace,它运行另一个启用了跟踪的程序(参见user/trace.c
)。完成后,您应该看到如下输出:
$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
$
$ 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
$
$ grep hello README
$
$ trace 2 usertests forkforkfork
usertests starting
test forkforkfork: 407: syscall fork -> 408
408: syscall fork -> 409
409: syscall fork -> 410
410: syscall fork -> 411
409: syscall fork -> 412
410: syscall fork -> 413
409: syscall fork -> 414
411: syscall fork -> 415
...
$
如果程序的行为如上所示,则解决方案是正确的(尽管进程ID可能不同)
提示:
$U/_trace
make qemu
,您将看到编译器无法编译user/trace.c,因为系统调用的用户空间存根还不存在:将系统调用的原型添加到user/user.h,存根添加到user/usys.pl,以及将系统调用编号添加到kernel/syscall.h,Makefile调用perl脚本user/usys.pl,它生成实际的系统调用存根user/usys.S,这个文件中的汇编代码使用RISC-V的ecall
指令转换到内核。一旦修复了编译问题(注:如果编译还未通过,尝试先make clean
,再执行make qemu
),就运行trace 32 grep hello README
;但由于您还没有在内核中实现系统调用,执行将失败。sys_trace()
函数,它通过将参数保存到proc
结构体(请参见kernel/proc.h)里的一个新变量中来实现新的系统调用。从用户空间检索系统调用参数的函数在kernel/syscall.c中,您可以在kernel/sysproc.c中看到它们的使用示例。fork()
(请参阅kernel/proc.c)将跟踪掩码从父进程复制到子进程。syscall()
函数以打印跟踪输出。您将需要添加一个系统调用名称数组以建立索引。本实验中在暴露给用户的user库中已经提供好了相关的trace程序让用户进行调用:
//user/trace.c
#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];
//参数个数不小于3个,确保系统调用号是合法数字
if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){
fprintf(2, "Usage: %s mask command\n", argv[0]);
exit(1);
}
//将第一个参数转换为整数,作为系统调用号传入trace函数---该系统调用函数需要我们提供对应的实现
if (trace(atoi(argv[1])) < 0) {
fprintf(2, "%s: trace failed\n", argv[0]);
exit(1);
}
//nargv数组持有要追踪的命令
for(i = 2; i < argc && i < MAXARG; i++){
nargv[i-2] = argv[i];
}
//执行命令完成系统调用过程追踪
exec(nargv[0], nargv);
exit(0);
}
我们需要做的是提供trace系统调用的具体实现,步骤如下:
在Makefile的UPROGS中添加$U/_trace
将系统调用原型添加到user/user.h头文件中
2. 将存根添加到user/usys.pl , 这段perl调用由makefile文件调用,生成实际的系统调用存根user/usys.S
3. 将系统调用编号添加到kernel/syscall.h中
4. 执行make clean 和 make qemu 命令,查看usys.S是否生成,是否符合我们的预期
5. 尝试执行trace 32 grep hello README命令,此时由于我们还没有在内核中提供trace系统调用的具体实现,所以这里执行会失败
// kernel/proc.h
struct proc {
// ...
int trace_mask; // trace系统调用参数
};
// kernel/sysproc.c
uint64
sys_trace(void)
{
// 获取系统调用的参数
argint(0, &(myproc()->trace_mask));
return 0;
}
//kernel/proc.c
int
fork(void)
{
// ...
safestrcpy(np->name, p->name, sizeof(p->name));
//将trace_mask拷贝到子进程
np->trace_mask = p->trace_mask;
pid = np->pid;
// ...
return pid;
}
void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7; // 系统调用编号,参见书中4.3节
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num](); // 执行系统调用,然后将返回值存入a0
// 系统调用是否匹配 -- 位运算判断
//如果我们要追踪read,那么trace_mask的值为32,也就是10000
//假如当前系统调用号为5,那么1左移五位为: 10000
//此时相与得到1,说明是我们需要追踪的系统调用,则进行打点记录
if ((1 << num) & p->trace_mask)
printf("%d: syscall %s -> %d\n", p->pid, syscalls_name[num], p->trapframe->a0);
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
这里需要注意一点: 我们是通过位运算来判断当前是否需要对某个系统调用进行追踪的,例如: 如果要追踪read系统调用,由于read系统调用号为5,所以我们将二进制第五位设置为1,也就是32。
// ...
extern uint64 sys_trace(void);
static uint64 (*syscalls[])(void) = {
// ...
[SYS_trace] sys_trace,
};
static char *syscalls_name[] = {
[SYS_fork] "fork",
[SYS_exit] "exit",
[SYS_wait] "wait",
[SYS_pipe] "pipe",
[SYS_read] "read",
[SYS_kill] "kill",
[SYS_exec] "exec",
[SYS_fstat] "fstat",
[SYS_chdir] "chdir",
[SYS_dup] "dup",
[SYS_getpid] "getpid",
[SYS_sbrk] "sbrk",
[SYS_sleep] "sleep",
[SYS_uptime] "uptime",
[SYS_open] "open",
[SYS_write] "write",
[SYS_mknod] "mknod",
[SYS_unlink] "unlink",
[SYS_link] "link",
[SYS_mkdir] "mkdir",
[SYS_close] "close",
[SYS_trace] "trace",
};
实现步骤总共两步:
系统调用追踪思路:
trace_mask就是一个位图,每个进程执行系统调用时,再获取当前系统调用号,通过移位得到对应的位图,与自身的trace_mask位图相与,判断得到结果是否为1,如果是说明当前系统调用号被监听了,需要输出对应的打点信息。
很重要的一点是,只要通过trace父进程创建的子进程才会被设置trace_mask。
YOUR JOB:
提示:
struct sysinfo;
int sysinfo(struct sysinfo *);
一旦修复了编译问题,就运行sysinfotest;但由于您还没有在内核中实现系统调用,执行将失败。
本实验中在暴露给用户的user库中已经提供好了相关的sinfo程序让用户进行调用:
//user/sysinfotest.c
void
sinfo(struct sysinfo *info) {
if (sysinfo(info) < 0) {
printf("FAIL: sysinfo failed");
exit(1);
}
}
struct sysinfo;
int sysinfo(struct sysinfo *);
entry("sysinfo");
#define SYS_sysinfo 23
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
内存是使用链表进行管理的,因此遍历kmem中的空闲链表就能够获取所有的空闲内存,如下
void
freebytes(uint64 *dst)
{
*dst = 0;
struct run *p = kmem.freelist; // 用于遍历
acquire(&kmem.lock);
while (p) {
// 统计空闲页数,乘上页大小 PGSIZE 就是空闲的内存字节数
*dst += PGSIZE;
p = p->next;
}
release(&kmem.lock);
}
xv6 中,空闲内存页的记录方式是,将空闲内存页本身直接用作链表节点,形成一个空闲页链表,每次需要分配,就把链表根部对应的页分配出去。每次需要回收,就把这个页作为新的根节点,把原来的 freelist 链表接到后面。注意这里是直接使用空闲页本身作为链表节点,所以不需要使用额外空间来存储空闲页链表,在 kalloc() 里也可以看到,分配内存的最后一个阶段,是直接将 freelist 的根节点地址(物理地址)返回出去了:
// kernel/kalloc.c
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
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; // 把空闲页链表的根节点返回出去,作为内存页使用(长度是 4096)
}
常见的记录空闲页的方法有:空闲表法、空闲链表法、位示图法(位图法)、成组链接法。这里 xv6 采用的是空闲链表法。
遍历proc数组,统计处于活动状态的进程即可,循环的写法参考scheduler函数
void
procnum(uint64 *dst)
{
*dst = 0;
struct proc *p;
// 不需要锁进程 proc 结构,因为我们只需要读取进程列表,不需要写
for (p = proc; p < &proc[NPROC]; p++) {
// 不是 UNUSED 的进程位,就是已经分配的
if (p->state != UNUSED)
(*dst)++;
}
}
void freebytes(uint64 *dst);
void procnum(uint64 *dst);
uint64
sys_sysinfo(void)
{
struct sysinfo info;
freebytes(&info.freemem);
procnum(&info.nproc);
// a0寄存器作为系统调用的参数寄存器,从中取出存放 sysinfo 结构的用户态缓冲区指针
uint64 dstaddr;
argaddr(0, &dstaddr);
// 使用 copyout,结合当前进程的页表,获得进程传进来的指针(逻辑地址)对应的物理地址
// 然后将 &sinfo 中的数据复制到该指针所指位置,供用户进程使用。
if (copyout(myproc()->pagetable, dstaddr, (char *)&info, sizeof info) < 0)
return -1;
return 0;
}
kernel/sysproc.c中记得引入sysinfo结构体定义所在的头文件:
//sysinfo.h具体定义在kernel/sysinfo.h文件中
#include "sysinfo.h"
struct sysinfo {
uint64 freemem; // amount of free memory (bytes)
uint64 nproc; // number of process
};
感兴趣的小伙伴可以去做一下可选的挑战: