MIT 6.S081 Lab2:System Calls | 系统调用

前置准备

主要内容为自己实现系统调用,或者优化已有的系统调用。

根据课程官网的要求,需要阅读完教材的第二章\(Operating \space system \space organization\)。并且读懂下列源代码:kernel/proc.h, kernel/defs.h, kernel/entry.S, kernel/main.c, user/initcode.S, user/init.c, 简略了解kernel/proc.c, kernel/exec.c

相关笔记参考: lecture 2 Notes

util分支所有内容都上传仓库后,执行下列命令来切换分支

1
2
3
git fetch
git checkout syscall
make clean

此外,新建一个syscall_dev分支来进行实际实验。

1
git checkout -b syscall_dev

syscall_dev中每通过一个作业的测试,提交(git commit)你的代码,并将所做的修改合并(git merge)到util中,然后提交(git push)到github。

1
2
3
4
5
git add .
git commit -m "xxxxxxxx"
git checkout syscall
git merge syscall_dev
git push github syscall:syscall

System call tracing (moderate)

Statement

在本作业中,你将添加一项系统调用跟踪功能,这可能会在以后的实验调试中有所帮助。你将创建一个新的跟踪系统调用来控制跟踪。它应该接受一个参数,即一个"掩码", 该掩码的位数指定了要跟踪的系统调用。例如,要跟踪 fork 系统调用,程序会调用 trace(1<<SYS_fork),其中 SYS_fork 是 kernel/syscall.h 中的系统调用编号。你必须修改 xv6 内核,以便在每个系统调用即将返回时,如果掩码中设置了系统调用编号,就打印出一行。该行应包含进程 ID、系统调用名称和返回值;无需打印系统调用参数。跟踪系统调用应启用对调用该调用的进程及其随后分叉的子进程的跟踪,但不应影响其他进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ 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
$
$ 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
...
$

在第一个样例中,trace 只在 grep 中追踪 read 系统调用。\(32\)\(1 << sys_read\) 的结果。 在第二个样例中,trace 追踪所有在grep中的系统调用,\(2147483647\)\(31\) 位二进制位都置1。 在第三个样例中, 进程并没有进行追踪。 在第四个样例中,trace 最终所有后续的fork系统调用。

如果程序正确,应该得到一致的输出(进程号可能有所出入,但其他都应该一致)。

Hints

  • Makefile 中的UPROGS 添加 $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 指令过渡到内核。解决编译问题后,运行 trace 32 grep hello README;由于尚未在内核中实现系统调用,所以会失败),将父进程的跟踪掩码复制到子进程。
  • kernel/sysproc.c 中添加一个 sys_trace()函数,通过在 proc 结构(参见 kernel/proc.h)中的一个新变量中记住参数来实现新的系统调用。从用户空间获取系统调用参数的函数在 kernel/syscall.c 中,你可以在 kernel/sysproc.c 中看到使用这些函数的示例。
  • 修改 fork()(参见 kernel/proc.c),将父进程的跟踪掩码复制到子进程。
  • 修改 kernel/syscall.c 中的 syscall() 函数,以打印跟踪输出。您需要添加一个系统调用名称数组作为索引。

Analysis & Solution

前文末尾(传送门)已经说过了 xv6 进行系统调用的过程,这里就略过了。

提示\(1\)和提示\(2\)没什么好说的。直接看kernel/sysproc.c文件。可以看到其他系统调用是怎么实现的。下面以exit来分析。

1
2
3
4
5
6
uint64 sys_exit(void) {
int n;
if (argint(0, &n) < 0) return -1;
exit(n);
return 0; // not reached
}

可以看到,首先从trapfram->a0寄存器拿出第一个参数(由argint()完成)。然后把更多实现细节放到了exit函数中来实现。可以看出,系统调用基本遵循这个流程:在sys_xxxx中处理参数、互斥锁等失误。如果还有复杂的功能,则放到具体xxxx函数里面实现。

我们的sys_trace只需要将p->trace_mask置为p->trapframe->a0的值即可。所以有:在kernel/sysproc.c中添加下面代码:

1
2
3
4
5
6
7
int sys_trace(void) {
int mask;
if (argint(0, &mask) < 0) return -1;

myproc()->trace_mask = mask;
return 0;
}

然后打开kernel/syscall.c, 找到syscall()这个函数。所有系统调用都是通过syscall函数来调用的。根据提示,我们只需要将p->trace_mask拿出来,然后看系统调用号这一位是否为\(1\),如果为\(1\),则输出信息即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static char* syscall_name[] = 
{"#", // to make index start from 1;
"fork", "exit", "wait", "pipe", "read", "kill",
"exec", "fstat", "chdir", "dup", "getpid", "sbrk",
"sleep", "uptime", "open", "write", "mknod", "unlink",
"link", "mkdir", "close", "trace"};

void syscall(void) {
int num;
struct proc* p = myproc();

num = p->trapframe->a7;

if (num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscal## Submit lab

新建`time.txt`文件,输入一个整数表明完成所有实验的耗时。

运行`make grade`, 得分为100
![](/img/6.S081/Lab-01/grade.png)

运行`make handin`, 根据提示将`API KEY`填入。s -> %d\n", p->pid, syscall_name[num], p->trapframe->a0);
}
} else {

为了子进程也能继续追踪,我们看一眼`fork`函数。可以看到,`fork`函数其实就是新创建了一个`struct proc* np`,然后把父进程的所有东西都复制给子进程。同理,我们把`trace_mask`也复制给子进程即可。

```C
// 在合适位置添加
np->trace_mask = p->trace_mask;

运行结果:

sysinfo (moderate)

Statement

在本作业中,你将添加一个系统调用 sysinfo,用于收集运行系统的信息。系统调用需要一个参数:指向 struct sysinfo 的指针(参见 kernel/sysinfo.h)。内核应填写该结构体的字段:freemem 字段应设置为可用内存的字节数## Submit lab

新建time.txt文件,输入一个整数表明完成所有实验的耗时。

运行make grade, 得分为100。

运行make handin, 根据提示将API KEY填入。,nproc 字段应设置为状态不是 UNUSED 的进程数。我们提供了一个测试程序 sysinfotest;如果打印出 sysinfotest: OK,则视为通过。

Hints

  • 在 Makefile 的 UPROGS 中添加 $U/_sysinfotest。
  • 运行 make qemu。将提示 user/sysinfotest.c 无法编译。添加系统调 sysinfo,步骤与前面的作业相同。在 user/user.h 中声明 sysinfo() 的原型,需## Submit lab

新建time.txt文件,输入一个整数表明完成所有实验的耗时。

运行make grade, 得分为100。

运行make handin, 根据提示将API KEY填入。 解决编译问题后,运行 sysinfotest;由于内核中尚未实现系统调用,所以会失败。 - sysinfo 需要将 struct sysinfo 复制到用户空间;有关如何使用 copyout() 进行复制的示例,请参见 sys_fstat()(kernel/sysfile.c)filestat()(kernel/file.c) - 要收集可用内存量,请在 kernel/kalloc.c 中添加一个函数。 - 要收集进程数,请在 kernel/proc.c 中添加一个函数。

Analysis & Solution

先按照上一个任务的方法加入一个 sysinfo 系统调用. 这里在 kernel 文件夹下再写一个 sysinfo.c 来实现这个系统调用. 在 MakefileOBJS 加入 $K/sysinfo.o \(因为我在sysinfo.c中实现,所以要链接这个文件。如果直接在kernel/proc.c中实现就不用).

filestat()函数能得知copyout函数的功能,加上sys_fstat()中虚拟地址是从a0读出来的。所以很容易能写出下面代码。

sysinfo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include "types.h"
#include "riscv.h"
#include "defs.h"
#include "date.h"
#include "param.h"
#include "memlayout.h"
#include "spinlock.h"
#include "proc.h"
#include "sysinfo.h"

extern uint64 freemem();
extern uint32 num_proc();

uint64 sys_sysinfo(void) {
struct sysinfo info;
struct proc* p = myproc();

uint64 addr;
if (argaddr(0, &addr) < 0) return -1;

info.freemem = freemem();
info.nproc = num_proc();

if (copyout(p->pagetable, addr, (char*)&info, sizeof(info)) < 0)
return -1;

return 0;
}

然后是freemem()num_proc()

kernel/kalloc.c中添加,看一下前面的kalloc()函数,在看一下struct kmem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct run {
struct run* next;
};

struct {
struct spinlock lock;
struct run* freelist;
} kmem;

// 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;
}

其中freelisk是一个链表,存储所有空闲内存页面的地址。所以我们只需要遍历这个链表,每一项都加上PGSIZE即可。

freemem:

1
2
3
4
5
6
7
8
9
10
11
12
uint64 freemem() {
struct run* r;
acquire(&kmem.lock);
r = kmem.freelist;

uint64 tot = 0;
for (;r;r = r->next) tot += PGSIZE;

release(&kmem.lock);

return tot;
}

然后翻kernel/proc.c中的procdump函数可以知道,proc数组中存放的是所有进程控制块的地址,所以遍历proc即可。

kernel/proc.c中添加:

1
2
3
4
5
6
7
8
uint32 num_proc() {
uint32 tot = 0;
struct proc* p;
for (p = proc;p < &proc[NPROC];p ++) {
if (p->state != UNUSED) tot ++;
}
return tot;
}

运行结果:

Submit lab

新建time.txt文件,输入一个整数表明完成所有实验的耗时。

运行make grade, 得分为100。

运行make handin, 根据提示将API KEY填入。


MIT 6.S081 Lab2:System Calls | 系统调用
https://acmicpc.top/2024/02/25/MIT-6.S081-lab02/
作者
江欣婷
发布于
2024年2月25日
许可协议