前言
一个本硕双非的小菜鸡,备战24年秋招。打算尝试6.S081,将它的Lab逐一实现,并记录期间心酸历程。
代码下载
官方网站:6.S081官方网站
安装方式:
通过 APT 安装 (Debian/Ubuntu)
确保你的 debian 版本运行的是 “bullseye” 或 “sid”(在 ubuntu 上,这可以通过运行 cat /etc/debian_version 来检查),然后运行:
sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu
(“buster”上的 QEMU 版本太旧了,所以你必须单独获取。
qemu-system-misc 修复
此时此刻,似乎软件包 qemu-system-misc 收到了一个更新,该更新破坏了它与我们内核的兼容性。如果运行 make qemu 并且脚本在 qemu-system-riscv64 -machine virt -bios none -kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 之后出现挂起
,
则需要卸载该软件包并安装旧版本:
$ sudo apt-get remove qemu-system-misc
$ sudo apt-get install qemu-system-misc=1:4.2-3ubuntu6
在 Arch 上安装
sudo pacman -S riscv64-linux-gnu-binutils riscv64-linux-gnu-gcc riscv64-linux-gnu-gdb qemu-arch-extra
测试您的安装
若要测试安装,应能够检查以下内容:
$ riscv64-unknown-elf-gcc --version
riscv64-unknown-elf-gcc (GCC) 10.1.0
...
$ qemu-system-riscv64 --version
QEMU emulator version 5.1.0
您还应该能够编译并运行 xv6: 要退出 qemu,请键入:Ctrl-a x。
# in the xv6 directory
$ make qemu
# ... lots of output ...
init: starting sh
$
一、System call tracing(moderate)
在本作业中,您将添加一个系统调用跟踪功能,该功能可能会在以后调试实验时对您有所帮助。您将创建一个新的trace系统调用来控制跟踪。它应该有一个参数,这个参数是一个整数“掩码”(mask),它的比特位指定要跟踪的系统调用。例如,要跟踪fork系统调用,程序调用trace(1 << SYS_fork),其中SYS_fork是kernel/syscall.h中的系统调用编号。如果在掩码中设置了系统调用的编号,则必须修改xv6内核,以便在每个系统调用即将返回时打印出一行。该行应该包含进程id、系统调用的名称和返回值;您不需要打印系统调用参数。trace系统调用应启用对调用它的进程及其随后派生的任何子进程的跟踪,但不应影响其他进程。
$ 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
...
$
提示:
- 在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指令转换到内核。一旦修复了编译问题(注:如果编译还未通过,尝试先make clean,再执行make qemu),就运行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()函数以打印跟踪输出。您将需要添加一个系统调用名称数组以建立索引。
虽然题目中说需要切换工作分支,但是可以不这么做。换了分支还容易丢,只需要手动在/user文件夹里加上trace.c和sysinfotest.c两个缺乏的文件即可。
还要去/kernel文件夹里加入一个sysinfo.h文件
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];
//保证trace的参数不少于三个,并且跟踪的系统调用号在0-99之间
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);
}
//保存待跟踪程序的名称和参数
for(i = 2; i < argc && i < MAXARG; i++){
nargv[i-2] = argv[i];
}
//运行待跟踪的程序
exec(nargv[0], nargv);
exit(0);
}
sysinfotest.c:
#include "kernel/types.h"
#include "kernel/riscv.h"
#include "kernel/sysinfo.h"
#include "user/user.h"
void
sinfo(struct sysinfo *info) {
if (sysinfo(info) < 0) {
printf("FAIL: sysinfo failed");
exit(1);
}
}
//
// use sbrk() to count how many free physical memory pages there are.
//
int
countfree()
{
uint64 sz0 = (uint64)sbrk(0);
struct sysinfo info;
int n = 0;
while(1){
if((uint64)sbrk(PGSIZE) == 0xffffffffffffffff){
break;
}
n += PGSIZE;
}
sinfo(&info);
if (info.freemem != 0) {
printf("FAIL: there is no free mem, but sysinfo.freemem=%d\n",
info.freemem);
exit(1);
}
sbrk(-((uint64)sbrk(0) - sz0));
return n;
}
void
testmem() {
struct sysinfo info;
uint64 n = countfree();
sinfo(&info);
if (info.freemem!= n) {
printf("FAIL: free mem %d (bytes) instead of %d\n", info.freemem, n);
exit(1);
}
if((uint64)sbrk(PGSIZE) == 0xffffffffffffffff){
printf("sbrk failed");
exit(1);
}
sinfo(&info);
if (info.freemem != n-PGSIZE) {
printf("FAIL: free mem %d (bytes) instead of %d\n", n-PGSIZE, info.freemem);
exit(1);
}
if((uint64)sbrk(-PGSIZE) == 0xffffffffffffffff){
printf("sbrk failed");
exit(1);
}
sinfo(&info);
if (info.freemem != n) {
printf("FAIL: free mem %d (bytes) instead of %d\n", n, info.freemem);
exit(1);
}
}
void
testcall() {
struct sysinfo info;
if (sysinfo(&info) < 0) {
printf("FAIL: sysinfo failed\n");
exit(1);
}
if (sysinfo((struct sysinfo *) 0xeaeb0b5b00002f5e) != 0xffffffffffffffff) {
printf("FAIL: sysinfo succeeded with bad argument\n");
exit(1);
}
}
void testproc() {
struct sysinfo info;
uint64 nproc;
int status;
int pid;
sinfo(&info);
nproc = info.nproc;
pid = fork();
if(pid < 0){
printf("sysinfotest: fork failed\n");
exit(1);
}
if(pid == 0){
sinfo(&info);
if(info.nproc != nproc+1) {
printf("sysinfotest: FAIL nproc is %d instead of %d\n", info.nproc, nproc+1);
exit(1);
}
exit(0);
}
wait(&status);
sinfo(&info);
if(info.nproc != nproc) {
printf("sysinfotest: FAIL nproc is %d instead of %d\n", info.nproc, nproc);
exit(1);
}
}
int
main(int argc, char *argv[])
{
printf("sysinfotest: 0 start\n");
testcall();
printf("sysinfotest: 1\n");
testmem();
printf("sysinfotest: 2\n");
testproc();
printf("sysinfotest: OK\n");
exit(0);
}
sysinfo.h文件
struct sysinfo {
uint64 freemem; // amount of free memory (bytes)
uint64 nproc; // number of process
};
解析:
想要实现上述例程,根据提示在目标文件中模仿着写就行。
在Makefile的UPROGS中添加$U/_trace。
首先需要在user/user.h 中添加
int trace(int);
然后在 user/usys.pl 中添加:
entry(“trace”);
最后在 kernel/syscall.h 中添加:
#define SYS_trace 22
这三项写完之后我们可以看到可以编译通过了,但是没有在内核中实现系统调用,执行将失败
然后进行下一步,在kernel/sysproc.c中添加一个sys_trace()函数
这个函数的作用是通过将参数保存到proc结构体里的一个新变量中来实现新的系统调用。
则先对kernel/proc.h中的结构体proc进行新变量的添加
int trace_mask;
这个结构体其实就是记录每个进程状态的结构体。
然后编写sys_trace()函数,核心就是给我们刚才的新变量赋值。(仿写就好)
uint64
sys_trace(void)
{
int mask;
if(argint(0, &mask) < 0)
return -1;
myproc()->trace_mask = mask;
return 0;
}
注:argint函数从trap页面读出int,这个trap是RISC-V的重要的控制寄存器,具体看文章吧。
接下来去kernel/proc.c修改fork(),目标是将跟踪掩码从父进程复制到子进程
很简单,加一句就成
safestrcpy(np->name, p->name, sizeof(p->name));
/*添加*/
np->trace_mask = p->trace_mask;
pid = np->pid;
最后修改kernel/syscall.c中的syscall()函数以打印跟踪输出。需要添加一个系统调用名称数组以建立索引。
按照格式先把引用变量加上
.......
extern uint64 sys_uptime(void);
extern uint64 sys_trace(void);
static uint64 (*syscalls[])(void) = {
......
[SYS_close] sys_close,
[SYS_trace] sys_trace,
};
再加上系统调用名称数组,对应的成员就是上面里的成员。
char *syscall_name[22] = {"fork", "exit", "wait", "pipe", "read",
"kill", "exec", "fstat", "chdir", "dup",
"getpid", "sbrk", "sleep", "uptime", "open",
"write", "mknod", "unlink", "link", "mkdir",
"close", "trace"};
在成功执行了对应的系统调用之后,判断这个系统调用是否需要trace打印
f(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
if (p->trace_mask & (1 << num))
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;
}
完成,还是比较成功的!
二、Sysinfo(moderate)
在这个作业中,您将添加一个系统调用sysinfo,它收集有关正在运行的系统的信息。系统调用采用一个参数:一个指向struct sysinfo的指针(参见kernel/sysinfo.h)。内核应该填写这个结构的字段:freemem字段应该设置为空闲内存的字节数,nproc字段应该设置为state字段不为UNUSED的进程数。我们提供了一个测试程序sysinfotest;如果输出“sysinfotest: OK”则通过。
提示:
在Makefile的UPROGS中添加$U/_sysinfotest
当运行make qemu时,user/sysinfotest.c将会编译失败,遵循和上一个作业一样的步骤添加sysinfo系统调用。要在user/user.h中声明sysinfo()的原型,需要预先声明struct sysinfo的存在:
struct sysinfo;
int sysinfo(struct sysinfo *);
一旦修复了编译问题,就运行sysinfotest;但由于您还没有在内核中实现系统调用,执行将失败。
sysinfo需要将一个struct sysinfo复制回用户空间;请参阅sys_fstat()(kernel/sysfile.c)和filestat()(kernel/file.c)以获取如何使用copyout()执行此操作的示例。
要获取空闲内存量,请在kernel/kalloc.c中添加一个函数
要获取进程数,请在kernel/proc.c中添加一个函数。
解析:
还是一样的操作:
在Makefile的UPROGS中添加$U/_sysinfotest。
首先在user/user.h加入函数声明
struct stat;
struct rtcdate;
struct sysinfo;
......
void *memcpy(void *, const void *, uint);
int trace(int);
int sysinfo(struct sysinfo *);
然后在 user/usys.pl 中添加:
entry(“sysinfo”);
最后在 kernel/syscall.h 中添加:
#define SYS_sysinfo 23
这三项写完之后我们可以看到可以编译通过了,但是没有在内核中实现系统调用,执行将失败
接下来可以先写sysinfo这块,也可以先搞定取空闲内存量和获取进程数这两个函数。我先从后往前写了
空闲内存的函数在kernel/kalloc.c中添加。根据该文件的代码以及我对内存管理的浅薄理解:内存分配底层是通过链表实现的(具体详见侯捷老师的内存管理课程)。可以参考kalloc函数去写。
kalloc函数:
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
//核心在这里:从 kmem 结构的 freelist 成员中获取一个指向空闲内存块的指针,并将其赋值给 r。如果 r 不为空(即该空闲内存块存在),则更新 kmem.freelist 指向 r 的下一个内存块。这样,r 所指向的内存块就被从空闲列表中移除了。
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;
}
问题变得简单了,求这个freelist链表有多长。
则:
uint64
freemem(void)
{
struct run *r;
uint64 nums = 0;
acquire(&kmem.lock);
r = kmem.freelist;
while (r) {
r = r->next;
nums++;
}
release(&kmem.lock);
// 注意内存分配是按页大小分配的,即4096个字节,所以我们输出的时候要乘4096,这样才是空闲的字节数
return nums * PGSIZE;
}
接下来在在kernel/proc.c中添加获取进程数函数
可以参考同文下的allocproc函数,主要是遍历进程控制块数组
uint64
collectproc()
{
struct proc *p;
uint64 nums = 0;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
//如果进程块已使用,就++
if(p->state != UNUSED) {
n++;
}
release(&p->lock);
}
return nums;
}
这两个函数需要添加到kernel/defs.h中
//kernel/kalloc.c
uint64 freemem(void);
//kernel/proc.c
uint64 collectproc();
最后写系统调用函数sys_sysinfo
//记着加头文件
#include "sysinfo.h"
uint64
sys_sysinfo(void)
{
struct sysinfo info;
uint64 addr;
argaddr(0, &addr);
info.freemem = freemem();
info.nproc = procnum();
//将 info 结构的内容复制到用户空间的地址 addr
if(copyout(myproc()->pagetable, addr, (char *)&info, sizeof(info)) < 0)
return -1;
return 0;
}
忘记按着上个题目配置了。。。
在syscall.c中添加新的系统调用,跟上题一样
......
extern uint64 sys_trace(void);
extern uint64 sys_sysinfo(void);
.......
[SYS_trace] sys_trace,
[SYS_sysinfo] sys_sysinfo,
.......
char *syscall_name[23] = {"fork", "exit", "wait", "pipe", "read",
"kill", "exec", "fstat", "chdir", "dup",
"getpid", "sbrk", "sleep", "uptime", "open",
"write", "mknod", "unlink", "link", "mkdir",
"close", "trace", "sysinfo"};
.......
最后也是成功输出