薪火相传

因为中途才开始写博客,一些配置上的问题可能没讲清楚。目前只是自己做一个记录吧。

如果能帮到你理解这个lab就好啦

环境:WSL + xv6

LAB2:https://docs.qq.com/slide/DR2VtU3Fvb2hGWEN0

实验准备

切换到本次实验的环境分支下

1
2
3
4
git commit -am lab0
git fetch
git checkout syscall
make clean

随后按要求修改相应 kerneluser 文件夹下的文件,以任务1的 procnum 为例,其余同理。

kernel/syscall.h 中,添加一个宏定义

1
#define SYS_procnum 22

然后在 kernel/syscall.c 指定系统调用的主体函数,即第22号System call会调用 sys_procnum 这个指针指向的函数

1
2
3
4
5
6
extern uint64 sys_procnum(void);

static uint64 (*syscalls[])(void) = {
//...
[SYS_procnum] sys_procnum,
}

kernel/sysproc.c 中添加具体实现的主体函数(在后面会介绍)

1
2
3
4
5
uint64
sys_procnum(void)
{
//implementation of sys_procnum
}

user/usys.pl 中添加系统调用的存根

1
entry("procnum");

具体而言,就是 Makefile 会调用这个 Perl 脚本,生成一段汇编码在 usys.S 中,这是 user.h 中定义的函数实际的实现,即调用的地方。

点进 usys.S 可以看到诸如 li a7, SYS_fork 这种,还记得之前把 SYS_fork 宏定义为了数字吧,就是在这里派用场。

为了能够让用户程序访问到 procnum 系统调用,我们需要在 user/user.h 中声明该调用:

1
2
// system calls
int procnum(int*);

你可能会注意到这里有一个参数列表上的不匹配,会发现所有 kernel/sysproc.c 中的系统调用实现形参列表都是void。

这是因为 procnum() 这样的函数是用户级别的函数,而 sys_procnum() 是内核级别的函数,用户空间和内核空间有不同的数据访问规则和隔离。

前者需要将参数在用户空间打包成适当的数据结构,后者会通过辅助函数 argaddr()argint() 等获取用户空间的参数到内核空间,并在其中进行合法与安全性检查,确保不会引发内核的错误。

可以看看其它system call的实现方法,比如wait(),是怎么获取参数列表的

1
2
int argaddr(int n, uint64 *addr);
int argint(int n, int *ip);

任务1:process counting

  • 系统调用功能 procnum:统计系统总进程数

在user文件夹下添加检验程序 procnum.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "kernel/types.h"
#include "kernel/riscv.h"
#include "kernel/sysinfo.h"
#include "user/user.h"

int
main(int argc, char *argv[])
{
if(argc >= 2) {
fprintf(2, "procnum: Too many arguments\n");
exit(1);
}

int num = -1;

if (procnum(&num) < 0) {
fprintf(2, "procnum failed!\n");
exit(1);
}

printf("Number of process: %d\n", num);
exit(0);
}

在makefile中添加编译项

1
2
3
UPROGS=\
...
$U/_procnum\

完成 kernel/sysproc.c 中函数的具体实现。我整体是对着 sys_wait() 函数学的。

先用 argaddr() 获取传过来的参数,是变量num的地址

1
2
3
4
5
6
7
uint64
sys_procnum(void)
{
uint64 p;
argaddr(0, &p);
return procnum(p);
}

sys_wait() 函数一样,我们把实现放在了 kernel/proc.c 中,记得在 defs.h 中先定义函数

因为内核空间和用户空间存在隔离,所以只能使用 copyout() 这种辅助函数实现修改用户空间的变量,而不能直接拿指针去操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Count the total number of processes in the system.
int
procnum(uint64 addr)
{
struct proc *pp; // 声明一个指向struct proc的指针pp,用于遍历进程表中的进程。
int proc_num = 0; // 统计进程数
struct proc *p = myproc(); //获取当前进程的指针并存储在p中。
for(pp = proc; pp < &proc[NPROC]; pp++){ //遍历整个进程表,查找子进程。
if(pp->state != 0) { // 只要进程状态不是unused
proc_num++;
}
}
if(addr != 0 && copyout(p->pagetable, addr, (char *)&proc_num, sizeof(proc_num)) < 0) {
// 这个比较复杂,,首先检查 addr 是否为非零值。
// 如果 addr 不为零就调用 copyout 函数,将数据从内核空间复制到用户空间。
// p 是当前进程的指针,p->pagetable 存储了当前进程的页表。页表是一种数据结构,用于将虚拟地址映射到物理地址,以便访问内存中的数据。
// addr 是用户程序提供的目标地址,数据将被复制到这里
// (char *)&proc_num 是要复制的源数据的地址。需要以char*的形式传递源数据的地址
// sizeof(pp->xstate) 是要复制的数据的大小,以字节为单位。
return -1;
}
return proc_num;
}

其余几项任务也差不多,照猫画虎。

下面的部分是我很久之后参照着实验报告补的,可能并不全面,仅供参考

任务2:Free Memory Counting

要统计空闲内存块的总字节数数,可以直接看在 kalloc.c 中的 kmem.freelist,遍历可以获得空闲“页数”,最后记得答案乘以 PGSIZE

为了操作定义在 kalloc.c 中的 kmem,可以在 kalloc.c 中添加一个函数

1
2
3
4
5
6
7
8
9
10
11
int
get_freemem(void)
{
int freemem_num = 0; // 统计空闲内存数
struct run *mem = kmem.freelist;
while (mem) {
freemem_num++;
mem = mem->next;
}
return freemem_num * PGSIZE;
}

最后在系统调用时调用 get_freemem() 这个函数,和实验 1 一样我还是把实现放在了proc.c 中。记得把这些函数都在头文件 defs.h 中定义一下。

sysproc.c :

1
2
3
4
5
6
7
uint64 
sys_freemem(void)
{
uint64 p;
argaddr(0, &p);
return freemem(p);
}

proc.c :

把答案写回 num

1
2
3
4
5
6
7
8
9
10
int
freemem(uint64 addr)
{
struct proc *p = myproc();
int freemem_num = get_freemem();
if(addr != 0 && copyout(p->pagetable, addr, (char *)&freemem_num, sizeof(freemem_num)) < 0) {
return -1;
}
return freemem_num;
}

任务3:System call tracing

这次 trace.c 已经给好了在 user 文件夹里

Trace 的用法不太一样,它传入一个参数 mask,每一个二进制位表示跟踪某一个system call。若某一 system call 处于被跟踪状态,则执行它时会输出相关信息

跟着 https://pdos.csail.mit.edu/6.S081/2022/labs/syscall.html 中的提示,我们还是先同样在 sysproc.c 中添
加系统调用 sys_trace(),它负责接收参数 mask 到当前进程。这要求我们在 proc.h 中,对
每一个进程添加一个参数 mask。

1
2
3
4
5
6
// Per-process state 
struct proc {
//...
char name[16]; // Process name (debugging)
int mask; // denote which syscall is being traced
};

随后,我们需要修改 fork,让每一个子进程也继承到 mask。

1
2
3
4
5
6
7
8
9
int 
fork(void)
{
// ...
// copy the trace_mask state
np->mask = p->trace_mask;
//...
return pid;
}

最后,在 syscall.c 中修改 void syscall(void),每次调用时检查所有的 system call,若对应 mask 位=1,则打印相关信息,格式和要求中相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void 
syscall(void)
{
int num;
struct proc *p = myproc();
char* syscall_name;
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
// Use num to lookup the system call function for num, call it,
// and store its return value in p->trapframe->a0
p->trapframe->a0 = syscalls[num]();
// 遍历所有 syscall
if ((p->mask & (1 << num)) != 0) { // 若处于跟踪状态;
// 打印相关信息
syscall_name = syscall_names[num];
printf("%d: syscall %s -> %d\n", p->pid, syscall_name, p->trapframe->a0);
}
}
else {
printf("%d %s: unknown sys call %d\n", p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}